Press n or j to go to the next uncovered block, b, p or k for the previous block.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 | 42x 42x 42x 42x 42x 42x 126x 2x 2x 2x 2x 42x 42x 125x 126x 214x 42x 30x 30x 30x 42x 30x 30x 33x 33x 2x 2x 2x 28x 42x 125x 126x 126x 5x 126x 125x 125x 157x 30x 30x | /* eslint-disable unicorn/no-nested-ternary */ import { Cardano, DRepInfo, RewardAccountInfoProvider } from '@cardano-sdk/core'; import { EMPTY, Observable, combineLatest, concat, distinctUntilChanged, filter, firstValueFrom, map, merge, mergeMap, of, startWith, switchMap, tap } from 'rxjs'; import { Logger } from 'ts-log'; import { RetryBackoffConfig } from 'backoff-rxjs'; import { TxInFlight } from '../types'; import { WalletStores } from '../../persistence'; import { blockingWithLatestFrom } from '@cardano-sdk/util-rxjs'; import { pollProvider } from '../util'; import isEqual from 'lodash/isEqual.js'; export type ObservableDrepInfoProvider = (drepIds: Cardano.DRepID[]) => Observable<DRepInfo[]>; const affectsRewardAccount = (rewardAccount: Cardano.RewardAccount) => (tx: Cardano.OnChainTx): boolean => { const stakeCredentialHash = Cardano.RewardAccount.toHash(rewardAccount); const hasRelevantStakeKeyCertificate = Cardano.stakeKeyCertificates(tx.body.certificates).some( (cert) => cert.stakeCredential.hash === stakeCredentialHash ); const hasRelevantCertificate = hasRelevantStakeKeyCertificate || tx.body.certificates?.some( // eslint-disable-next-line complexity (cert) => ((cert.__typename === Cardano.CertificateType.MIR || cert.__typename === Cardano.CertificateType.StakeDelegation || cert.__typename === Cardano.CertificateType.StakeVoteDelegation || cert.__typename === Cardano.CertificateType.VoteDelegation) && cert.stakeCredential?.hash === stakeCredentialHash) || (cert.__typename === Cardano.CertificateType.PoolRegistration && cert.poolParameters.rewardAccount === rewardAccount) || cert.__typename === Cardano.CertificateType.PoolRetirement || ((cert.__typename === Cardano.CertificateType.RegisterDelegateRepresentative || cert.__typename === Cardano.CertificateType.UnregisterDelegateRepresentative) && cert.dRepCredential.hash === stakeCredentialHash) || (cert.__typename === Cardano.CertificateType.StakeVoteDelegation && cert.stakeCredential.hash === stakeCredentialHash) ); return ( hasRelevantCertificate || tx.body.withdrawals?.some((withdrawal) => withdrawal.stakeAddress === rewardAccount) || false ); }; export const createRewardAccountInfoProvider = ({ epoch$, externalTrigger$, rewardAccountInfoProvider, retryBackoffConfig, logger }: { rewardAccountInfoProvider: RewardAccountInfoProvider; epoch$: Observable<Cardano.EpochNo>; externalTrigger$: Observable<void>; retryBackoffConfig: RetryBackoffConfig; logger: Logger; }) => (rewardAccount: Cardano.RewardAccount): Observable<Cardano.RewardAccountInfo> => pollProvider({ equals: isEqual, logger, retryBackoffConfig, sample: async () => rewardAccountInfoProvider.rewardAccountInfo(rewardAccount, await firstValueFrom(epoch$)), trigger$: merge(epoch$, externalTrigger$) }); export type ObservableRewardAccountInfoProvider = ReturnType<typeof createRewardAccountInfoProvider>; const nextRewardBalance = (rewardAccount: Cardano.RewardAccount, txsInFlight: TxInFlight[]): bigint | null => { const hasWithdrawal = txsInFlight.some((tx) => tx.body.withdrawals?.some((withdrawal) => withdrawal.stakeAddress === rewardAccount) ); // rewards must be spent in full, or not at all return hasWithdrawal ? 0n : null; }; const nextDeposit = ( rewardAccount: Cardano.RewardAccount, txsInFlight: TxInFlight[], depositAmount: Cardano.Lovelace ): Cardano.Lovelace | null => { const stakeCredentialHash = Cardano.RewardAccount.toHash(rewardAccount); // try to find the last relevant certificate to take effect for (let txIndex = txsInFlight.length - 1; txIndex >= 0; txIndex--) { const certificates = txsInFlight[txIndex].body.certificates || []; for (let certIndex = certificates.length - 1; certIndex >= 0; certIndex--) { const certificate = certificates[certIndex]; if ( certificate.__typename === Cardano.CertificateType.StakeDeregistration && certificate.stakeCredential.hash === stakeCredentialHash ) { return 0n; } Iif ( (certificate.__typename === Cardano.CertificateType.StakeRegistration || certificate.__typename === Cardano.CertificateType.StakeRegistrationDelegation || certificate.__typename === Cardano.CertificateType.StakeVoteRegistrationDelegation) && certificate.stakeCredential.hash === stakeCredentialHash ) { return depositAmount; } } } return null; }; export const createRewardAccountsTracker = ({ rewardAccountAddresses$, rewardAccountInfoProvider, store, newTransaction$, protocolParameters$, transactionsInFlight$ }: { rewardAccountAddresses$: Observable<Cardano.RewardAccount[]>; store: WalletStores['rewardAccountInfo']; rewardAccountInfoProvider: ObservableRewardAccountInfoProvider; newTransaction$: Observable<Cardano.OnChainTx>; protocolParameters$: Observable<Pick<Cardano.ProtocolParameters, 'stakeKeyDeposit'>>; transactionsInFlight$: Observable<TxInFlight[]>; }) => rewardAccountAddresses$.pipe( // TODO: // eslint-disable-next-line sonarjs/cognitive-complexity switchMap((rewardAccounts) => combineLatest( rewardAccounts.map((rewardAccount) => concat( store.getValues([rewardAccount]).pipe(mergeMap((values) => (values.length > 0 ? of(values[0]) : EMPTY))), newTransaction$.pipe(filter(affectsRewardAccount(rewardAccount)), startWith(null)).pipe( switchMap(() => rewardAccountInfoProvider(rewardAccount).pipe( tap((rewardAccountInfo) => store.setValue(rewardAccount, rewardAccountInfo).subscribe()), blockingWithLatestFrom(protocolParameters$), switchMap(([rewardAccountInfo, { stakeKeyDeposit }]) => transactionsInFlight$.pipe( map((txsInFlight): Cardano.RewardAccountInfo => { if (txsInFlight.length === 0) return rewardAccountInfo; const nextDepositValue = nextDeposit(rewardAccount, txsInFlight, BigInt(stakeKeyDeposit)); return { ...rewardAccountInfo, credentialStatus: typeof nextDepositValue === 'bigint' ? nextDepositValue > 0n ? Cardano.StakeCredentialStatus.Registering : Cardano.StakeCredentialStatus.Unregistering : rewardAccountInfo.credentialStatus, // this ensures that rewards and deposit are not being spent twice if chaining transactions // as well as updates balance with deposit while transaction is in flight deposit: nextDepositValue ?? rewardAccountInfo.deposit, rewardBalance: nextRewardBalance(rewardAccount, txsInFlight) ?? rewardAccountInfo.rewardBalance // this may be extended to update dRepDelegatee and delegatee // based on pending transaction rather than waiting for tx confirmation }; }) // do not emit when transaction is removed from transactionsInFlight$ due to seeing this tx on chain; // this will be unsubscribed due to outer switchMap that looks for onChain$ tx that affects this reward account // TODO: test how this behaves when inFlight$ emits at the same time as newTransaction$ // delay(1) ) ) ) ) ) ).pipe(distinctUntilChanged<Cardano.RewardAccountInfo>(isEqual)) ) ) ) ); |