Skip to content

fix(wallet): await for wallet settle before resolving cip30 utxo LW-12197 #1578

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

Merged
merged 1 commit into from
Jan 31, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
21 changes: 17 additions & 4 deletions packages/wallet/src/cip30.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -84,6 +84,16 @@ export type CallbackConfirmation = {
getCollateral?: GetCollateralCallback;
};

const firstValueFromTimed = <T>(observable$: Observable<T>, 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;
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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$);
Expand Down Expand Up @@ -583,7 +596,7 @@ const baseCip30WalletApi = (

const getPubStakeKeys = async (
wallet$: Observable<ObservableWallet>,
filter: Cardano.StakeCredentialStatus.Registered | Cardano.StakeCredentialStatus.Unregistered
filterCredentialStatus: Cardano.StakeCredentialStatus.Registered | Cardano.StakeCredentialStatus.Unregistered
) => {
const wallet = await firstValueFrom(wallet$);
return firstValueFrom(
Expand All @@ -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))
Expand Down
19 changes: 18 additions & 1 deletion packages/wallet/test/integration/cip30mapping.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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);
Expand Down
Loading