UNPKG

@ledgerhq/coin-tezos

Version:
142 lines (129 loc) 4.62 kB
// SPDX-FileCopyrightText: © 2026 LEDGER SAS // SPDX-License-Identifier: Apache-2.0 import type { TezosCoinConfig, TezosContext } from '../config' import type { APIAccount, APIManagerAccount, APIUnstakeRequest } from '../network/types' import type { Cursor, Page, Stake } from '@ledgerhq/coin-module-framework/api/types' import { log } from '@ledgerhq/logs' import { hasManagerKey } from '../network/types' import { createTzktApi } from '../network/tzkt' import { partitionNativeBalance } from '../utils' import { STAKING_UID_PREFIX } from './positionUid' export { isDelegationPosition, isFinalizablePosition, isStakePosition, isUnstakingPosition, } from './positionUid' export function fetchUnstakeRequests( config: TezosCoinConfig, address: string, account: APIAccount ): Promise<APIUnstakeRequest[]> { if (!hasManagerKey(account)) return Promise.resolve([]) return (account.unstakedBalance ?? 0) > 0 ? createTzktApi(config).getUnstakeRequests(address) : Promise.resolve([]) } function unstakeRequestToStake(address: string, req: APIUnstakeRequest): Stake | null { // TzKT's status filter is unreliable; never build a position for a finalized request. if (req.status !== 'pending' && req.status !== 'finalizable') { log('coin:tezos', 'unstakeRequestToStake: dropping non-active unstake request', { requestId: req.id, status: req.status, }) return null } if (req.actualAmount <= 0) { log('coin:tezos', 'unstakeRequestToStake: dropping non-positive unstake request', { requestId: req.id, actualAmount: req.actualAmount, }) return null } const createdAt = new Date(req.firstTime) if (!Number.isFinite(createdAt.getTime())) { log('coin:tezos', 'unstakeRequestToStake: dropping unstake request with invalid firstTime', { requestId: req.id, firstTime: req.firstTime, }) return null } const isFinalizable = req.status === 'finalizable' const prefix = isFinalizable ? STAKING_UID_PREFIX.finalizable : STAKING_UID_PREFIX.unstaking return { uid: `${prefix}${req.id}`, address, ...(req.baker?.address && { delegate: req.baker.address }), state: isFinalizable ? 'inactive' : 'deactivating', createdAt, asset: { type: 'native' }, amount: BigInt(req.actualAmount), actions: [], } } export function buildStakesForAccount( address: string, account: APIManagerAccount, unstakeRequests: APIUnstakeRequest[] ): Stake[] { const balance = BigInt(account.balance ?? 0) const stakedBalance = BigInt(account.stakedBalance ?? 0) // Account-level unstaked total (the source validateIntent's gate uses), not the summed per-request // positions below — so spendable stays gate-consistent even if a request is dropped or its fetch fails. const unstakedBalance = BigInt(account.unstakedBalance ?? 0) const delegateAddress = account.delegate?.address const unstakeStakes: Stake[] = [] for (const req of unstakeRequests) { const stake = unstakeRequestToStake(address, req) if (stake) unstakeStakes.push(stake) } const stakes: Stake[] = [] if (delegateAddress) { const { spendable: delegated, locked } = partitionNativeBalance( balance, stakedBalance, unstakedBalance ) if (locked < stakedBalance + unstakedBalance) { log('coin:tezos', 'buildStakesForAccount: balance < staked + unstaked, clamping to 0', { address, balance: balance.toString(), stakedBalance: stakedBalance.toString(), unstakedBalance: unstakedBalance.toString(), }) } stakes.push({ uid: `${STAKING_UID_PREFIX.delegation}${address}`, address, delegate: delegateAddress, state: 'active', asset: { type: 'native' }, amount: delegated, actions: [], }) } if (stakedBalance > 0n) { stakes.push({ uid: `${STAKING_UID_PREFIX.stake}${address}`, address, ...(delegateAddress && { delegate: delegateAddress }), state: 'active', asset: { type: 'native' }, amount: stakedBalance, actions: [], }) } stakes.push(...unstakeStakes) return stakes } export async function getStakes( context: TezosContext, address: string, _cursor?: Cursor ): Promise<Page<Stake>> { const config = await context.config() const accountInfo = await createTzktApi(config).getAccountByAddress(address) if (!hasManagerKey(accountInfo)) return { items: [] } const unstakeRequests = await fetchUnstakeRequests(config, address, accountInfo) return { items: buildStakesForAccount(address, accountInfo, unstakeRequests) } }