Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add lido staking #47

Merged
merged 12 commits into from
Jun 19, 2024
3 changes: 2 additions & 1 deletion Gem/Assets/ViewModels/AssetDataViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,8 @@ struct AssetDataViewModel {
Chain.celestia.assetId,
Chain.solana.assetId,
Chain.sui.assetId,
Chain.smartChain.assetId
Chain.smartChain.assetId,
// Chain.ethereum.assetId disabled
].contains(asset.id) {
return true
}
Expand Down
14 changes: 12 additions & 2 deletions Gem/Stake/Services/StakeService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ struct StakeService {
.filter { $0.isActive && !$0.name.isEmpty }
}

func getRecipientAddress(chain: StakeChain?, validatorId: String?) -> String? {
func getRecipientAddress(chain: StakeChain?, type: AmountType, validatorId: String?) -> String? {

guard let id = validatorId else {
return nil
Expand All @@ -41,7 +41,17 @@ struct StakeService {
return id
case .smartChain:
return StakeHub.address
case .ethereum, .none:
case .ethereum:
// for Lido, it's stETH token address or withdrawal queue
switch type {
case .stake:
return LidoContract.address
case .unstake, .withdraw:
return LidoContract.withdrawal
default:
fatalError()
}
case .none:
fatalError()
}
}
Expand Down
4 changes: 2 additions & 2 deletions Gem/Transfer/ViewsModels/AmountViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import BigInt
import Components
import Store
import SwiftUI
import Gemstone
import GemstonePrimitives

enum AmountType: Equatable, Hashable {
Expand Down Expand Up @@ -158,7 +159,6 @@ class AmounViewModel: ObservableObject {

var minimumValue: BigInt {
let stakeChain = amountRecipientData.data.asset.chain.stakeChain

switch amountRecipientData.type {
case .stake:
return BigInt(StakeConfig.config(chain: stakeChain!).minAmount)
Expand Down Expand Up @@ -205,7 +205,7 @@ class AmounViewModel: ObservableObject {
}

func getTransferData(value: BigInt) throws -> TransferData {
let recipientAddress = stakeService.getRecipientAddress(chain: amountRecipientData.data.asset.chain.stakeChain, validatorId: currentValidator?.id)
let recipientAddress = stakeService.getRecipientAddress(chain: amountRecipientData.data.asset.chain.stakeChain, type: amountRecipientData.type, validatorId: currentValidator?.id)

// make sure validator address is correct
// FIXME. Refactor and add tests
Expand Down
3 changes: 2 additions & 1 deletion Gem/Transfer/ViewsModels/ConfirmTransferViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,8 @@ class ConfirmTransferViewModel: ObservableObject {
block: input.block,
token: input.token,
utxos: input.utxos,
messageBytes: input.messageBytes
messageBytes: input.messageBytes,
extra: input.extra
)
return try signer.sign(input: input)
}
Expand Down
3 changes: 2 additions & 1 deletion Gem/Wallet/Services/BalanceService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,8 @@ public class BalanceService: BalancerUpdater {
return balances.compactMap { (balance: AssetBalance) in
guard
let asset = assets[balance.assetId.identifier],
let total = try? formatter.double(from: balance.balance.total, decimals: asset.decimals.asInt) else {
let total = try? formatter.double(from: balance.balance.total(asset.chain.includeStakedBalance), decimals: asset.decimals.asInt)
else {
return nil
}
let price = prices[balance.assetId.identifier]
Expand Down
4 changes: 4 additions & 0 deletions Packages/Blockchain/Sources/Aptos/AptosService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ extension AptosService: ChainBalanceable {
public func tokenBalance(for address: String, tokenIds: [AssetId]) async throws -> [AssetBalance] {
[]
}

public func getStakeBalance(address: String) async throws -> AssetBalance {
fatalError()
}
}

// MARK: - ChainFeeCalculateable
Expand Down
4 changes: 4 additions & 0 deletions Packages/Blockchain/Sources/Bitcoin/BitcoinService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,10 @@ extension BitcoinService: ChainBalanceable {
public func tokenBalance(for address: String, tokenIds: [AssetId]) async throws -> [AssetBalance] {
return []
}

public func getStakeBalance(address: String) async throws -> AssetBalance {
fatalError()
}
}

// MARK: - ChainFeeCalculateable
Expand Down
79 changes: 36 additions & 43 deletions Packages/Blockchain/Sources/BnbSmartChain/SmartChainService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import SwiftHTTPClient
import WalletCore

public class SmartChainService {

let provider: Provider<EthereumProvider>
let stakeHub: StakeHub

Expand All @@ -16,34 +15,30 @@ public class SmartChainService {
}
}

extension SmartChainService {
public func coinBalance(for address: String) async throws -> AssetBalance {
async let balanceCall = try provider.request(.balance(address: address))
.mapResult(BigIntable.self).value
async let delegationsCall = try getStakeDelegations(address: address)
extension SmartChainService: ChainStakable {
func getMaxElectedValidators() async throws -> UInt16 {
let params = [
"to":StakeHub.address,
"data": stakeHub.encodeMaxElectedValidators()
]

let (balance, delegations) = try await(balanceCall, delegationsCall)
let staked = delegations.filter { $0.state == .active }.map { $0.balanceValue }.reduce(0, +)
let pending = delegations.filter { $0.state == .undelegating }.map { $0.balanceValue }.reduce(0, +)
return AssetBalance(
assetId: Chain.smartChain.assetId,
balance: Balance(
available: balance,
staked: staked,
pending: pending
)
)
let result = try await provider.request(.call(params)).map(as: JSONRPCResponse<String>.self).result
guard
let data = Data(hexString: result),
let value = UInt16(EthereumAbiValue.decodeUInt256(input: data))
else {
throw AnyError("Unable to get validators")
}
return value
}
}

extension SmartChainService: ChainStakable {
public func getValidators(apr: Double) async throws -> [DelegationValidator] {
public func getValidators(apr _: Double) async throws -> [DelegationValidator] {
let limit = try await getMaxElectedValidators()
let params = [
"to":StakeHub.reader,
"data": stakeHub.encodeValidatorsCall(offset: 0, limit: limit)
"to": StakeHub.reader,
"data": stakeHub.encodeValidatorsCall(offset: 0, limit: limit),
]

let result = try await provider.request(.call(params)).map(as: JSONRPCResponse<String>.self).result
guard let data = Data(hexString: result) else {
return []
Expand All @@ -60,26 +55,24 @@ extension SmartChainService: ChainStakable {
return delegations + undelegations
}

func getMaxElectedValidators() async throws -> UInt16 {
let params = [
"to":StakeHub.address,
"data": stakeHub.encodeMaxElectedValidators()
]

let result = try await provider.request(.call(params)).map(as: JSONRPCResponse<String>.self).result
guard
let data = Data(hexString: result),
let value = UInt16(EthereumAbiValue.decodeUInt256(input: data))
else {
throw AnyError("Unable to get validators")
}
return value
public func getStakeBalance(address: String) async throws -> AssetBalance {
let delegations = try await getStakeDelegations(address: address)
let staked = delegations.filter { $0.state == .active }.map { $0.balanceValue }.reduce(0, +)
let pending = delegations.filter { $0.state == .undelegating }.map { $0.balanceValue }.reduce(0, +)
return AssetBalance(
assetId: Chain.smartChain.assetId,
balance: Balance(
available: .zero,
staked: staked,
pending: pending
)
)
}

func getDelegations(address: String, limit: UInt16) async throws -> [DelegationBase] {
let params = [
"to":StakeHub.reader,
"data": try stakeHub.encodeDelegationsCall(address: address, limit: limit)
let params = try [
"to": StakeHub.reader,
"data": stakeHub.encodeDelegationsCall(address: address, limit: limit),
]
let result = try await provider.request(.call(params)).mapResult(String.self)
guard let data = Data(hexString: result) else {
Expand All @@ -89,9 +82,9 @@ extension SmartChainService: ChainStakable {
}

func getUndelegations(address: String, limit: UInt16) async throws -> [DelegationBase] {
let params = [
"to":StakeHub.reader,
"data": try stakeHub.encodeUndelegationsCall(address: address, limit: limit)
let params = try [
"to": StakeHub.reader,
"data": stakeHub.encodeUndelegationsCall(address: address, limit: limit),
]
let result = try await provider.request(.call(params)).mapResult(String.self)
guard let data = Data(hexString: result) else {
Expand Down
8 changes: 6 additions & 2 deletions Packages/Blockchain/Sources/ChainService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -107,14 +107,18 @@ extension ChainService: ChainTransactionStateFetchable {

extension ChainService: ChainStakable {
public func getValidators(apr: Double) async throws -> [DelegationValidator] {
return try await Self.service(chain: chain, with: url)
try await Self.service(chain: chain, with: url)
.getValidators(apr: apr)
}

public func getStakeDelegations(address: String) async throws -> [DelegationBase] {
return try await Self.service(chain: chain, with: url)
try await Self.service(chain: chain, with: url)
.getStakeDelegations(address: address)
}

public func getStakeBalance(address: String) async throws -> AssetBalance {
try await Self.service(chain: chain, with: url).getStakeBalance(address: address)
}
}

// MARK: - ChainTokenable
Expand Down
77 changes: 44 additions & 33 deletions Packages/Blockchain/Sources/Cosmos/CosmosService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -132,45 +132,19 @@ extension CosmosService: ChainBalanceable {
.noble:

async let getBalance = getBalance(address: address)
async let getDelegations = getDelegations(address: address)
async let getUnboundingDelegations = getUnboundingDelegations(address: address)
async let getRewards = getRewards(address: address)

let (balances, delegations, unboundingDelegations, rewards) = try await (
async let getStakeBalance = getStakeBalance(address: address)

let (balances, stakeBalance) = try await (
getBalance,
getDelegations,
getUnboundingDelegations,
getRewards
getStakeBalance
)

let balance = balances.balances.filter ({ $0.denom == denom.rawValue }).compactMap { BigInt($0.amount) }.reduce(0, +)

let delegationsBalance = delegations
.filter { $0.balance.denom == denom.rawValue }
.compactMap { BigInt($0.balance.amount) }
.reduce(0, +)

let unboundingDelegationsBalance = unboundingDelegations.compactMap {
$0.entries.compactMap { BigInt($0.balance)}.reduce(0, +)
}.reduce(0, +)

let rewardsBalance = rewards
.map {
$0.reward
.filter { $0.denom == denom.rawValue }
.compactMap { BigInt($0.amount) }
.reduce(0, +)
}
.reduce(0, +)

return Primitives.AssetBalance(
return AssetBalance(
assetId: chain.chain.assetId,
balance: Balance(
available: balance,
staked: delegationsBalance,
pending: unboundingDelegationsBalance,
rewards: rewardsBalance
)
available: balance
).merge(stakeBalance.balance)
)
}
}
Expand All @@ -182,6 +156,43 @@ extension CosmosService: ChainBalanceable {
return AssetBalance(assetId: assetId, balance: Balance(available: BigInt(stringLiteral: balance)))
}
}

public func getStakeBalance(address: String) async throws -> AssetBalance {
let denom = chain.denom
async let getDelegations = getDelegations(address: address)
async let getUnboundingDelegations = getUnboundingDelegations(address: address)
async let getRewards = getRewards(address: address)

let (delegations, unboundingDelegations, rewards) = try await(getDelegations, getUnboundingDelegations, getRewards)

let delegationsBalance = delegations
.filter { $0.balance.denom == denom.rawValue }
.compactMap { BigInt($0.balance.amount) }
.reduce(0, +)

let unboundingDelegationsBalance = unboundingDelegations.compactMap {
$0.entries.compactMap { BigInt($0.balance)}.reduce(0, +)
}.reduce(0, +)

let rewardsBalance = rewards
.map {
$0.reward
.filter { $0.denom == denom.rawValue }
.compactMap { BigInt($0.amount) }
.reduce(0, +)
}
.reduce(0, +)

return AssetBalance(
assetId: chain.chain.assetId,
balance: Balance(
available: .zero, // will be merged into
staked: delegationsBalance,
pending: unboundingDelegationsBalance,
rewards: rewardsBalance
)
)
}
}

// MARK: - ChainFeeCalculateable
Expand Down
Loading
Loading