Skip to content

Commit

Permalink
feat(wallet): add Wallet.assets$
Browse files Browse the repository at this point in the history
  • Loading branch information
mkazlauskas committed Nov 22, 2021
1 parent 4312d15 commit e08022e
Show file tree
Hide file tree
Showing 11 changed files with 151 additions and 15 deletions.
29 changes: 21 additions & 8 deletions packages/wallet/src/SingleAddressWallet.ts
@@ -1,4 +1,14 @@
import { AddressType, GroupedAddress, KeyManager } from './KeyManagement';
import {
AssetProvider,
Cardano,
NetworkInfo,
ProtocolParametersRequiredByWallet,
StakePoolSearchProvider,
WalletProvider,
coreToCsl
} from '@cardano-sdk/core';
import { Assets } from '.';
import {
Balance,
BehaviorObservable,
Expand All @@ -11,6 +21,7 @@ import {
TransactionalTracker,
TransactionsTracker,
coldObservableProvider,
createAssetsTracker,
createBalanceTracker,
createDelegationTracker,
createTransactionsTracker,
Expand All @@ -19,14 +30,6 @@ import {
sharedDistinctEpoch
} from './services';
import { BehaviorSubject, Subject, combineLatest, from, lastValueFrom, map, mergeMap, take } from 'rxjs';
import {
Cardano,
NetworkInfo,
ProtocolParametersRequiredByWallet,
StakePoolSearchProvider,
WalletProvider,
coreToCsl
} from '@cardano-sdk/core';
import { InitializeTxProps, Wallet } from './types';
import { InputSelector, defaultSelectionConstraints, roundRobinRandomImprove } from '@cardano-sdk/cip2';
import { Logger, dummyLogger } from 'ts-log';
Expand All @@ -45,6 +48,7 @@ export interface SingleAddressWalletDependencies {
readonly keyManager: KeyManager;
readonly walletProvider: WalletProvider;
readonly stakePoolSearchProvider: StakePoolSearchProvider;
readonly assetProvider: AssetProvider;
readonly inputSelector?: InputSelector;
readonly logger?: Logger;
}
Expand All @@ -69,6 +73,7 @@ export class SingleAddressWallet implements Wallet {
addresses$: BehaviorSubject<GroupedAddress[]>;
protocolParameters$: TrackerSubject<ProtocolParametersRequiredByWallet>;
genesisParameters$: TrackerSubject<Cardano.CompactGenesis>;
assets$: TrackerSubject<Assets>;
name: string;

constructor(
Expand All @@ -85,6 +90,7 @@ export class SingleAddressWallet implements Wallet {
walletProvider,
stakePoolSearchProvider,
keyManager,
assetProvider,
logger = dummyLogger,
inputSelector = roundRobinRandomImprove()
}: SingleAddressWalletDependencies
Expand Down Expand Up @@ -139,6 +145,13 @@ export class SingleAddressWallet implements Wallet {
walletProvider
});
this.balance = createBalanceTracker(this.protocolParameters$, this.utxo, this.delegation);
this.assets$ = new TrackerSubject(
createAssetsTracker({
assetProvider,
balanceTracker: this.balance,
retryBackoffConfig
})
);
}
initializeTx(props: InitializeTxProps): Promise<TxInternals> {
return lastValueFrom(
Expand Down
43 changes: 43 additions & 0 deletions packages/wallet/src/services/AssetsTracker.ts
@@ -0,0 +1,43 @@
import { AssetProvider, Cardano } from '@cardano-sdk/core';
import { Assets } from '../types';
import { Balance, TransactionalTracker } from './types';
import { RetryBackoffConfig } from 'backoff-rxjs';
import { coldObservableProvider } from './util';
import { distinct, from, mergeMap, of, scan, startWith } from 'rxjs';

export const createGetAssetProvider =
(assetProvider: AssetProvider, retryBackoffConfig: RetryBackoffConfig) => (assetId: Cardano.AssetId) =>
coldObservableProvider(
() => assetProvider.getAsset(assetId),
retryBackoffConfig,
of(true) // fetch only once
);
export type GetAssetProvider = ReturnType<typeof createGetAssetProvider>;

export interface AssetsTrackerProps {
balanceTracker: TransactionalTracker<Balance>;
assetProvider: AssetProvider;
retryBackoffConfig: RetryBackoffConfig;
}

interface AssetsTrackerInternals {
getAssetProvider?: GetAssetProvider;
}

export const createAssetsTracker = (
{ assetProvider, balanceTracker, retryBackoffConfig }: AssetsTrackerProps,
{ getAssetProvider = createGetAssetProvider(assetProvider, retryBackoffConfig) }: AssetsTrackerInternals = {}
) =>
balanceTracker.total$.pipe(
mergeMap(({ assets }) => from(Object.keys(assets || {}))),
distinct(),
mergeMap((assetId) => getAssetProvider(assetId)),
scan(
(assets, asset) => ({
...assets,
[asset.assetId]: asset
}),
{} as Assets
),
startWith({} as Assets)
);
1 change: 1 addition & 0 deletions packages/wallet/src/services/index.ts
Expand Up @@ -3,5 +3,6 @@ export * from './BalanceTracker';
export * from './UtxoTracker';
export * from './DelegationTracker';
export * from './TransactionsTracker';
export * from './AssetsTracker';
export * from './TransactionError';
export * from './types';
3 changes: 3 additions & 0 deletions packages/wallet/src/types.ts
Expand Up @@ -16,6 +16,8 @@ export interface FinalizeTxProps {
readonly body: Cardano.TxBodyAlonzo;
}

export type Assets = Partial<Record<Cardano.AssetId, Cardano.Asset>>;

export interface Wallet {
name: string;
readonly balance: TransactionalTracker<Balance>;
Expand All @@ -27,6 +29,7 @@ export interface Wallet {
readonly networkInfo$: BehaviorObservable<NetworkInfo>;
readonly protocolParameters$: BehaviorObservable<ProtocolParametersRequiredByWallet>;
readonly addresses$: BehaviorObservable<GroupedAddress[]>;
readonly assets$: BehaviorObservable<Assets>;
initializeTx(props: InitializeTxProps): Promise<TxInternals>;
finalizeTx(props: TxInternals): Promise<Cardano.NewTxAlonzo>;
submitTx(tx: Cardano.NewTxAlonzo): Promise<void>;
Expand Down
13 changes: 10 additions & 3 deletions packages/wallet/test/SingleAddressWallet.test.ts
@@ -1,8 +1,8 @@
/* eslint-disable max-len */
import * as mocks from './mocks';
import { AssetId, createStubStakePoolSearchProvider } from '@cardano-sdk/util-dev';
import { Cardano } from '@cardano-sdk/core';
import { KeyManagement, SingleAddressWallet } from '../src';
import { createStubStakePoolSearchProvider } from '@cardano-sdk/util-dev';
import { firstValueFrom, skip } from 'rxjs';

describe('SingleAddressWallet', () => {
Expand All @@ -11,6 +11,7 @@ describe('SingleAddressWallet', () => {
const rewardAccount = mocks.stakeKeyHash;
let keyManager: KeyManagement.KeyManager;
let walletProvider: mocks.ProviderStub;
let assetProvider: mocks.MockAssetProvider;
let wallet: SingleAddressWallet;

beforeEach(async () => {
Expand All @@ -20,12 +21,13 @@ describe('SingleAddressWallet', () => {
password: '123'
});
walletProvider = mocks.mockWalletProvider();
assetProvider = mocks.mockAssetProvider();
const stakePoolSearchProvider = createStubStakePoolSearchProvider();
keyManager.deriveAddress = jest.fn().mockReturnValue({
address,
rewardAccount
});
wallet = new SingleAddressWallet({ name }, { keyManager, stakePoolSearchProvider, walletProvider });
wallet = new SingleAddressWallet({ name }, { assetProvider, keyManager, stakePoolSearchProvider, walletProvider });
});

afterEach(() => wallet.shutdown());
Expand Down Expand Up @@ -77,10 +79,15 @@ describe('SingleAddressWallet', () => {
expect(rewardAccounts[0].delegatee).toBeUndefined();
expect(rewardAccounts[0].rewardBalance.total).toBe(mocks.rewards);
});
it('"addresses"', () => {
it('"addresses$"', () => {
expect(wallet.addresses$.value[0].address).toEqual(address);
expect(wallet.addresses$.value[0].rewardAccount).toEqual(rewardAccount);
});
it('"assets$"', async () => {
expect(await firstValueFrom(wallet.assets$)).toEqual({
[AssetId.TSLA]: mocks.asset
});
});
});

describe('creating transactions', () => {
Expand Down
7 changes: 6 additions & 1 deletion packages/wallet/test/e2e/SingleAddressWallet.test.ts
@@ -1,7 +1,7 @@
import { Cardano } from '@cardano-sdk/core';
import { SingleAddressWallet, StakeKeyStatus, Wallet } from '../../src';
import { assetProvider, keyManager, poolId1, poolId2, stakePoolSearchProvider, walletProvider } from './config';
import { distinctUntilChanged, filter, firstValueFrom, map, merge, mergeMap, skip, tap, timer } from 'rxjs';
import { keyManager, poolId1, poolId2, stakePoolSearchProvider, walletProvider } from './config';

const faucetAddress =
'addr_test1qqr585tvlc7ylnqvz8pyqwauzrdu0mxag3m7q56grgmgu7sxu2hyfhlkwuxupa9d5085eunq2qywy7hvmvej456flknswgndm3';
Expand Down Expand Up @@ -62,6 +62,7 @@ describe('SingleAddressWallet', () => {
wallet = new SingleAddressWallet(
{ name: 'Test Wallet' },
{
assetProvider,
keyManager,
stakePoolSearchProvider,
walletProvider
Expand All @@ -76,6 +77,10 @@ describe('SingleAddressWallet', () => {
expect(wallet.addresses$.value![0].address.startsWith('addr')).toBe(true);
});

it('has assets$', async () => {
expect(typeof (await firstValueFrom(wallet.assets$))).toBe('object');
});

test('balance & transaction', async () => {
const stakeKeyDeposit = BigInt((await firstValueFrom(wallet.protocolParameters$)).stakeKeyDeposit);
const initialTotalBalance = await firstValueFrom(wallet.balance.total$);
Expand Down
11 changes: 9 additions & 2 deletions packages/wallet/test/e2e/config.ts
@@ -1,20 +1,27 @@
import { blockfrostProvider } from '@cardano-sdk/blockfrost';
import { blockfrostAssetProvider, blockfrostProvider } from '@cardano-sdk/blockfrost';
import { createInMemoryKeyManager } from '../../src/KeyManagement';
import { createStubStakePoolSearchProvider } from '@cardano-sdk/util-dev';

const networkId = Number.parseInt(process.env.NETWORK_ID || '');
if (Number.isNaN(networkId)) throw new Error('NETWORK_ID not set');
const isTestnet = networkId === 0;

export const walletProvider = (() => {
const walletProviderName = process.env.WALLET_PROVIDER;
if (walletProviderName === 'blockfrost') {
const projectId = process.env.BLOCKFROST_API_KEY;
if (!projectId) throw new Error('BLOCKFROST_API_KEY not set');
return blockfrostProvider({ isTestnet: networkId === 0, projectId });
return blockfrostProvider({ isTestnet, projectId });
}
throw new Error(`WALLET_PROVIDER unsupported: ${walletProviderName}`);
})();

export const assetProvider = (() => {
const projectId = process.env.BLOCKFROST_API_KEY;
if (!projectId) throw new Error('BLOCKFROST_API_KEY not set (for assetProvider)');
return blockfrostAssetProvider({ isTestnet, projectId });
})();

export const keyManager = (() => {
const mnemonicWords = (process.env.MNEMONIC_WORDS || '').split(' ');
if (mnemonicWords.length === 0) throw new Error('MNEMONIC_WORDS not set');
Expand Down
4 changes: 3 additions & 1 deletion packages/wallet/test/integration/withdrawal.test.ts
Expand Up @@ -2,7 +2,7 @@ import { Cardano } from '@cardano-sdk/core';
import { KeyManagement, SingleAddressWallet, SingleAddressWalletProps, TransactionFailure } from '../../src';
import { createStubStakePoolSearchProvider } from '@cardano-sdk/util-dev';
import { firstValueFrom } from 'rxjs';
import { mockWalletProvider } from '../mocks';
import { mockAssetProvider, mockWalletProvider } from '../mocks';

const walletProps: SingleAddressWalletProps = { name: 'some-wallet' };
const networkId = Cardano.NetworkId.mainnet;
Expand All @@ -17,7 +17,9 @@ describe('integration/withdrawal', () => {
keyManager = KeyManagement.createInMemoryKeyManager({ mnemonicWords, networkId, password });
const walletProvider = mockWalletProvider();
const stakePoolSearchProvider = createStubStakePoolSearchProvider();
const assetProvider = mockAssetProvider();
wallet = new SingleAddressWallet(walletProps, {
assetProvider,
keyManager,
stakePoolSearchProvider,
walletProvider
Expand Down
1 change: 1 addition & 0 deletions packages/wallet/test/mocks/index.ts
@@ -1,2 +1,3 @@
export * from './mockWalletProvider';
export * from './testKeyManager';
export * from './mockAssetProvider';
16 changes: 16 additions & 0 deletions packages/wallet/test/mocks/mockAssetProvider.ts
@@ -0,0 +1,16 @@
import { Cardano } from '@cardano-sdk/core';

export const asset = {
assetId: '659f2917fb63f12b33667463ee575eeac1845bbc736b9c0bbc40ba8254534c41',
fingerprint: 'asset...',
history: [{ action: Cardano.AssetProvisioning.Mint, quantity: 1000n, transactionId: 'some-tx-id...' }],
name: 'TSLA',
policyId: '659f2917fb63f12b33667463ee575eeac1845bbc736b9c0bbc40ba82',
quantity: 1000n
} as Cardano.Asset;

export const mockAssetProvider = () => ({
getAsset: jest.fn().mockResolvedValue(asset)
});

export type MockAssetProvider = ReturnType<typeof mockAssetProvider>;
38 changes: 38 additions & 0 deletions packages/wallet/test/services/AssetsTracker.test.ts
@@ -0,0 +1,38 @@
import { AssetId } from '@cardano-sdk/util-dev';
import { AssetsTrackerProps, Balance, TransactionalTracker, createAssetsTracker } from '../../src/services';
import { Cardano } from '@cardano-sdk/core';
import { createTestScheduler } from '../testScheduler';
import { of } from 'rxjs';

describe('createAssetsTracker', () => {
it('fetches asset info for every asset in total balance', () => {
createTestScheduler().run(({ cold, expectObservable }) => {
const balanceTracker = {
total$: cold('a-b-c', {
a: {} as Balance,
b: { assets: { [AssetId.TSLA]: 1n } as Cardano.TokenMap } as Balance,
c: {
assets: {
[AssetId.TSLA]: 1n,
[AssetId.PXL]: 2n
} as Cardano.TokenMap
} as Balance
})
} as unknown as TransactionalTracker<Balance>;
const asset1 = { assetId: AssetId.TSLA, name: 'TSLA' } as Cardano.Asset;
const asset2 = { assetId: AssetId.PXL, name: 'PXL' } as Cardano.Asset;
const getAssetProvider = jest.fn().mockReturnValueOnce(of(asset1)).mockReturnValueOnce(of(asset2));
const target$ = createAssetsTracker({ balanceTracker } as AssetsTrackerProps, { getAssetProvider });
expectObservable(target$).toBe('a-b-c', {
a: {},
b: {
[AssetId.TSLA]: asset1
},
c: {
[AssetId.TSLA]: asset1,
[AssetId.PXL]: asset2
}
});
});
});
});

0 comments on commit e08022e

Please sign in to comment.