diff --git a/packages/wallet/src/cip30.ts b/packages/wallet/src/cip30.ts index 380f2407f1d..6cb1a66329f 100644 --- a/packages/wallet/src/cip30.ts +++ b/packages/wallet/src/cip30.ts @@ -17,13 +17,13 @@ import { WalletApiExtension, WithSenderContext } from '@cardano-sdk/dapp-connector'; -import { Cardano, Serialization, coalesceValueQuantities } from '@cardano-sdk/core'; +import { Cardano, Milliseconds, Serialization, coalesceValueQuantities } from '@cardano-sdk/core'; import { Ed25519KeyHashHex, Hash28ByteBase16 } from '@cardano-sdk/crypto'; import { HexBlob, ManagedFreeableScope } from '@cardano-sdk/util'; import { InputSelectionError, InputSelectionFailure } from '@cardano-sdk/input-selection'; import { Logger } from 'ts-log'; import { MessageSender } from '@cardano-sdk/key-management'; -import { Observable, firstValueFrom, from, map, mergeMap, race, throwError } from 'rxjs'; +import { Observable, filter, firstValueFrom, from, map, mergeMap, race, throwError, timeout } from 'rxjs'; import { ObservableWallet } from './types'; import { requiresForeignSignatures } from './services'; import uniq from 'lodash/uniq.js'; @@ -84,6 +84,16 @@ export type CallbackConfirmation = { getCollateral?: GetCollateralCallback; }; +const firstValueFromTimed = (observable$: Observable, timeoutAfter: Milliseconds) => + firstValueFrom( + observable$.pipe( + timeout({ each: timeoutAfter, with: () => throwError(() => new ApiError(APIErrorCode.InternalError, 'Timeout')) }) + ) + ); + +const waitForWalletStateSettle = (wallet: ObservableWallet, syncTimeout = Milliseconds(120_000)) => + firstValueFromTimed(wallet.syncStatus.isSettled$.pipe(filter((isSettled) => isSettled)), syncTimeout); + const mapCallbackFailure = (err: unknown, logger: Logger): false => { logger.error(err); return false; @@ -294,6 +304,7 @@ const baseCip30WalletApi = ( logger.debug('getting balance'); try { const wallet = await firstValueFrom(wallet$); + await waitForWalletStateSettle(wallet); const value = await firstValueFrom(wallet.balance.utxo.available$); return Serialization.Value.fromCore(value).toCbor(); } catch (error) { @@ -332,6 +343,7 @@ const baseCip30WalletApi = ( > => { logger.debug('getting collateral'); const wallet = await firstValueFrom(wallet$); + await waitForWalletStateSettle(wallet); let unspendables = await getSortedUtxos(wallet.utxo.unspendable$); const available = await getSortedUtxos(wallet.utxo.available$); // No available unspendable UTxO @@ -441,6 +453,7 @@ const baseCip30WalletApi = ( const scope = new ManagedFreeableScope(); try { const wallet = await firstValueFrom(wallet$); + await waitForWalletStateSettle(wallet); let utxos = amount ? await selectUtxo(wallet, parseValueCbor(amount).toCore(), !!paginate) : await firstValueFrom(wallet.utxo.available$); @@ -583,7 +596,7 @@ const baseCip30WalletApi = ( const getPubStakeKeys = async ( wallet$: Observable, - filter: Cardano.StakeCredentialStatus.Registered | Cardano.StakeCredentialStatus.Unregistered + filterCredentialStatus: Cardano.StakeCredentialStatus.Registered | Cardano.StakeCredentialStatus.Unregistered ) => { const wallet = await firstValueFrom(wallet$); return firstValueFrom( @@ -595,7 +608,7 @@ const getPubStakeKeys = async ( credentialStatus === Cardano.StakeCredentialStatus.Registering ? Cardano.StakeCredentialStatus.Registered : Cardano.StakeCredentialStatus.Unregistered; - return filter === status; + return filterCredentialStatus === status; }) ), map((keys) => keys.map(({ publicStakeKey }) => publicStakeKey)) diff --git a/packages/wallet/test/integration/cip30mapping.test.ts b/packages/wallet/test/integration/cip30mapping.test.ts index 601e48cb970..a70b88bde9c 100644 --- a/packages/wallet/test/integration/cip30mapping.test.ts +++ b/packages/wallet/test/integration/cip30mapping.test.ts @@ -31,7 +31,7 @@ import { Ed25519KeyHashHex } from '@cardano-sdk/crypto'; import { HexBlob, ManagedFreeableScope } from '@cardano-sdk/util'; import { InMemoryUnspendableUtxoStore, createInMemoryWalletStores } from '../../src/persistence'; import { InitializeTxProps, InitializeTxResult } from '@cardano-sdk/tx-construction'; -import { NEVER, firstValueFrom, of } from 'rxjs'; +import { NEVER, Observable, delay, firstValueFrom, of } from 'rxjs'; import { Providers, createWallet } from './util'; import { address_0_0, address_1_0, rewardAccount_0, rewardAccount_1 } from '../services/ChangeAddress/testData'; import { buildDRepAddressFromDRepKey, signTx, waitForWalletStateSettle } from '../util'; @@ -190,6 +190,23 @@ describe('cip30', () => { ).not.toThrow(); }); + it('subscribes to syncStatus.isSettled$ before utxo.available$', async () => { + const isSettled$ = wallet.syncStatus.isSettled$; + const available$ = wallet.utxo.available$; + let isSettledSubscribedAt: number; + let availableSubscribedAt: number; + wallet.syncStatus.isSettled$ = new Observable((observer) => { + if (!isSettledSubscribedAt) isSettledSubscribedAt = Date.now(); + return isSettled$.pipe(delay(1)).subscribe(observer); + }); + wallet.utxo.available$ = new Observable((observer) => { + if (!availableSubscribedAt) availableSubscribedAt = Date.now(); + return available$.subscribe(observer); + }); + await api.getUtxos(context); + expect(isSettledSubscribedAt!).toBeLessThan(availableSubscribedAt!); + }); + describe('with "amount" argument', () => { const getUtxoFiltered = async (coins: Cardano.Lovelace, tslaQuantity?: bigint, paginate?: Paginate) => { const filterAmountValue = new Serialization.Value(coins);