Skip to content

Commit

Permalink
feat: add ObservableWallet.handles$ that emits own handles
Browse files Browse the repository at this point in the history
- PersonalWallet has a new optional prop in the constructor: handlePolicyIds
- Add some handle-related data to mockProviders in `util-dev`, update affected tests
  • Loading branch information
mkazlauskas committed Jun 6, 2023
1 parent 8a8b8b6 commit 1c3b532
Show file tree
Hide file tree
Showing 12 changed files with 290 additions and 14 deletions.
22 changes: 19 additions & 3 deletions packages/util-dev/src/mockProviders/mockAssetProvider.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Asset, Cardano } from '@cardano-sdk/core';
import { handleAssetId, handleAssetName, handleFingerprint, handlePolicyId } from './mockData';

export const asset = {
export const asset: Asset.AssetInfo = {
assetId: Cardano.AssetId('659f2917fb63f12b33667463ee575eeac1845bbc736b9c0bbc40ba8254534c41'),
fingerprint: Cardano.AssetFingerprint('asset1rjklcrnsdzqp65wjgrg55sy9723kw09mlgvlc3'),
history: [
Expand All @@ -9,17 +10,32 @@ export const asset = {
transactionId: Cardano.TransactionId('886206542d63b23a047864021fbfccf291d78e47c1e59bd4c75fbc67b248c5e8')
}
],
mintOrBurnCount: 5,
name: Cardano.AssetName('54534c41'),
nftMetadata: null,
policyId: Cardano.PolicyId('7eae28af2208be856f7a119668ae52a49b73725e326dc16579dcc373'),
quantity: 1000n,
supply: 1000n,
tokenMetadata: null
} as Asset.AssetInfo;
};

export const handleAssetInfo: Asset.AssetInfo = {
assetId: handleAssetId,
fingerprint: handleFingerprint,
mintOrBurnCount: 1,
name: handleAssetName,
policyId: handlePolicyId,
quantity: 1n,
supply: 1n
};

export const mockAssetProvider = () => ({
getAsset: jest.fn().mockResolvedValue(asset),
getAssets: jest.fn().mockResolvedValue([asset]),
getAssets: jest
.fn()
.mockImplementation(async ({ assetIds }) =>
assetIds.map((assetId: Cardano.AssetId) => (assetId === handleAssetId ? handleAssetInfo : asset))
),
healthCheck: jest.fn().mockResolvedValue({ ok: true })
});

Expand Down
10 changes: 8 additions & 2 deletions packages/util-dev/src/mockProviders/mockChainHistoryProvider.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as AssetId from '../assetId';
import { Cardano, Paginated } from '@cardano-sdk/core';
import { currentEpoch, ledgerTip, stakeKeyHash } from './mockData';
import { currentEpoch, handleAssetId, ledgerTip, stakeKeyHash } from './mockData';
import { somePartialStakePools } from '../createStubStakePoolProvider';
import delay from 'delay';

Expand Down Expand Up @@ -129,7 +129,13 @@ export const queryTransactionsResult: Paginated<Cardano.HydratedTx> = {
address: Cardano.PaymentAddress(
'addr_test1qq585l3hyxgj3nas2v3xymd23vvartfhceme6gv98aaeg9muzcjqw982pcftgx53fu5527z2cj2tkx2h8ux2vxsg475q2g7k3g'
),
value: { assets: new Map([[AssetId.TSLA, 1n]]), coins: 5_000_000n }
value: {
assets: new Map([
[AssetId.TSLA, 1n],
[handleAssetId, 1n]
]),
coins: 5_000_000n
}
}
],
validityInterval: {
Expand Down
9 changes: 9 additions & 0 deletions packages/util-dev/src/mockProviders/mockData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,15 @@ export const stakeKeyHash = Cardano.RewardAccount.toHash(rewardAccount);

export const rewardAccountBalance = 33_333n;

export const handlePolicyId = Cardano.PolicyId('f0ff48bbb7bbe9d59a40f1ce90e9e9d0ff5002ec48f232b49ca0fb9a');
export const handle = 'bob';
export const handleAssetId = Cardano.AssetId.fromParts(
handlePolicyId,
Cardano.AssetName(Buffer.from('bob').toString('hex'))
);
export const handleAssetName = Cardano.AssetName(Buffer.from(handle, 'utf8').toString('hex'));
export const handleFingerprint = Cardano.AssetFingerprint('asset1f0azzptnr8dghzjh7egqvdjmt33e3lz5uy59th');

export const ledgerTip = {
blockNo: Cardano.BlockNo(1_111_111),
hash: Cardano.BlockId('10d64cc11e9b20e15b6c46aa7b1fed11246f437e62225655a30ea47bf8cc22d0'),
Expand Down
2 changes: 2 additions & 0 deletions packages/util-dev/src/mockProviders/mockUtxoProvider.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import * as AssetId from '../assetId';
import { Cardano, UtxoProvider } from '@cardano-sdk/core';
import { handleAssetId } from './mockData';
import delay from 'delay';

export const utxo: Cardano.Utxo[] = [
Expand Down Expand Up @@ -106,6 +107,7 @@ export const utxo: Cardano.Utxo[] = [
'addr_test1qq585l3hyxgj3nas2v3xymd23vvartfhceme6gv98aaeg9muzcjqw982pcftgx53fu5527z2cj2tkx2h8ux2vxsg475q2g7k3g'
),
value: {
assets: new Map([[handleAssetId, 1n]]),
coins: 9_825_963n
}
}
Expand Down
25 changes: 23 additions & 2 deletions packages/wallet/src/PersonalWallet/PersonalWallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import {
createAssetsTracker,
createBalanceTracker,
createDelegationTracker,
createHandlesTracker,
createProviderStatusTracker,
createSimpleConnectionStatusTracker,
createTransactionsTracker,
Expand Down Expand Up @@ -55,6 +56,7 @@ import {
import {
Assets,
FinalizeTxProps,
HandleInfo,
ObservableWallet,
SignDataProps,
SyncStatus,
Expand All @@ -77,13 +79,15 @@ import {
map,
mergeMap,
switchMap,
tap
tap,
throwError
} from 'rxjs';
import { Cip30DataSignature } from '@cardano-sdk/dapp-connector';
import {
GenericTxBuilder,
InitializeTxProps,
InitializeTxResult,
InvalidConfigurationError,
TxBuilderDependencies,
finalizeTx,
initializeTx
Expand All @@ -100,6 +104,10 @@ import isEqual from 'lodash/isEqual';
export interface PersonalWalletProps {
readonly name: string;
readonly polling?: PollingConfig;
/**
* If set, will track and emit own handles on PersonalWallet.handles$ observable
*/
readonly handlePolicyIds?: Cardano.PolicyId[];
readonly retryBackoffConfig?: RetryBackoffConfig;
}

Expand Down Expand Up @@ -193,6 +201,7 @@ export class PersonalWallet implements ObservableWallet {
readonly protocolParameters$: TrackerSubject<Cardano.ProtocolParameters>;
readonly genesisParameters$: TrackerSubject<Cardano.CompactGenesis>;
readonly assetInfo$: TrackerSubject<Assets>;
readonly handles$: Observable<HandleInfo[]>;
readonly fatalError$: Subject<unknown>;
readonly syncStatus: SyncStatus;
readonly name: string;
Expand All @@ -212,7 +221,8 @@ export class PersonalWallet implements ObservableWallet {
retryBackoffConfig = {
initialInterval: Math.min(pollInterval, 1000),
maxInterval
}
},
handlePolicyIds
}: PersonalWalletProps,
{
txSubmitProvider,
Expand Down Expand Up @@ -451,6 +461,17 @@ export class PersonalWallet implements ObservableWallet {
}),
stores.assets
);

this.handles$ = handlePolicyIds?.length
? createHandlesTracker({
assetInfo$: this.assetInfo$,
handlePolicyIds,
logger: contextLogger(this.#logger, 'handles$'),
tip$: this.tip$,
utxo$: this.utxo.total$
})
: throwError(() => new InvalidConfigurationError('Missing handlePolicyIds option in PersonalWallet'));

this.util = createWalletUtil({
protocolParameters$: this.protocolParameters$,
utxo: this.utxo
Expand Down
89 changes: 89 additions & 0 deletions packages/wallet/src/services/HandlesTracker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { Assets, HandleInfo } from '../types';
import { Cardano } from '@cardano-sdk/core';
import {
EMPTY,
Observable,
combineLatest,
distinctUntilChanged,
map,
mergeMap,
of,
shareReplay,
withLatestFrom
} from 'rxjs';
import { Logger } from 'ts-log';
import { deepEquals, isNotNil } from '@cardano-sdk/util';
import { sameArrayItems, strictEquals } from './util';
import uniqBy from 'lodash/uniqBy';

export interface HandlesTrackerProps {
utxo$: Observable<Cardano.Utxo[]>;
assetInfo$: Observable<Assets>;
tip$: Observable<Cardano.Tip>;
handlePolicyIds: Cardano.PolicyId[];
logger: Logger;
}

const handleInfoEquals = (a: HandleInfo, b: HandleInfo) =>
a.assetId === b.assetId &&
a.resolvedAt.hash === b.resolvedAt.hash &&
deepEquals(a.tokenMetadata, b.tokenMetadata) &&
deepEquals(a.nftMetadata, b.nftMetadata);

export const createHandlesTracker = ({ tip$, assetInfo$, handlePolicyIds, logger, utxo$ }: HandlesTrackerProps) =>
combineLatest([
utxo$.pipe(
map((utxo) =>
utxo.flatMap(([_, txOut]) =>
uniqBy(
[...(txOut.value.assets?.keys() || [])]
.filter((assetId) => handlePolicyIds.some((policyId) => assetId.startsWith(policyId)))
.map((assetId) => ({
handleAssetId: assetId,
txOut
})),
({ handleAssetId }) => handleAssetId
)
)
),
distinctUntilChanged((a, b) => sameArrayItems(a, b, strictEquals)),
withLatestFrom(tip$)
),
assetInfo$
]).pipe(
mergeMap(([[utxo, tip], assets]) => {
const handlesWithAssetInfo = utxo
.map(({ handleAssetId, txOut }): HandleInfo | null => {
const assetInfo = assets.get(handleAssetId);
if (!assetInfo) {
logger.debug(`Asset info not (yet?) found for ${handleAssetId}`);
return null;
}
return {
...assetInfo,
handle: Buffer.from(Cardano.AssetId.getAssetName(handleAssetId), 'hex').toString('utf8'),
hasDatum: !!txOut.datum,
resolvedAddresses: {
cardano: txOut.address
},
resolvedAt: tip
};
})
.filter(isNotNil);
if (utxo.length > 0 && handlesWithAssetInfo.length === 0) {
// AssetInfo is still resolving
return EMPTY;
}
return of(
handlesWithAssetInfo.filter(({ handle, supply }) => {
if (supply > 1n) {
logger.warn(`Omitting handle with supply >1: ${handle}`);
return false;
}
return true;
})
);
}),
distinctUntilChanged((a, b) => sameArrayItems(a, b, handleInfoEquals)),
shareReplay({ bufferSize: 1, refCount: true })
);
1 change: 1 addition & 0 deletions packages/wallet/src/services/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,4 @@ export * from './SupplyDistributionTracker';
export * from './SmartTxSubmitProvider';
export * from './KeyAgent';
export * from './AddressDiscovery';
export * from './HandlesTracker';
13 changes: 12 additions & 1 deletion packages/wallet/src/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
import { Asset, Cardano, EpochInfo, EraSummary, NetworkInfoProvider, TxCBOR } from '@cardano-sdk/core';
import {
Asset,
Cardano,
EpochInfo,
EraSummary,
HandleResolution,
NetworkInfoProvider,
TxCBOR
} from '@cardano-sdk/core';
import { BalanceTracker, DelegationTracker, TransactionsTracker, UtxoTracker } from './services';
import { Cip30DataSignature } from '@cardano-sdk/dapp-connector';
import { GroupedAddress, cip8 } from '@cardano-sdk/key-management';
Expand Down Expand Up @@ -37,6 +45,8 @@ export type FinalizeTxProps = Omit<TxContext, 'ownAddresses'> & {
tx: Cardano.TxBodyWithHash;
};

export type HandleInfo = HandleResolution & Asset.AssetInfo;

export interface ObservableWallet {
readonly balance: BalanceTracker;
readonly delegation: DelegationTracker;
Expand All @@ -48,6 +58,7 @@ export interface ObservableWallet {
readonly currentEpoch$: Observable<EpochInfo>;
readonly protocolParameters$: Observable<Cardano.ProtocolParameters>;
readonly addresses$: Observable<GroupedAddress[]>;
readonly handles$: Observable<HandleInfo[]>;
/** All owned and historical assets */
readonly assetInfo$: Observable<Assets>;
/**
Expand Down
25 changes: 20 additions & 5 deletions packages/wallet/test/PersonalWallet/load.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
AddressDiscovery,
ConnectionStatus,
ConnectionStatusTracker,
HandleInfo,
ObservableWallet,
PersonalWallet,
PollingConfig,
Expand All @@ -29,6 +30,7 @@ import {
UtxoProvider,
coalesceValueQuantities
} from '@cardano-sdk/core';
import { InvalidConfigurationError } from '@cardano-sdk/tx-construction';
import { InvalidStringError } from '@cardano-sdk/util';
import { ReplaySubject, firstValueFrom } from 'rxjs';
import { WalletStores, createInMemoryWalletStores } from '../../src/persistence';
Expand All @@ -39,7 +41,8 @@ import { waitForWalletStateSettle } from '../util';
import delay from 'delay';
import flatten from 'lodash/flatten';

const { currentEpoch, networkInfo, queryTransactionsResult, queryTransactionsResult2 } = mocks;
const { currentEpoch, networkInfo, queryTransactionsResult, queryTransactionsResult2, handleAssetId, handleAssetInfo } =
mocks;

const name = 'Test Wallet';
const address = mocks.utxo[0][0].address!;
Expand Down Expand Up @@ -80,6 +83,7 @@ type CreateWalletProps = {
addressDiscovery?: AddressDiscovery;
asyncKeyAgent?: AsyncKeyAgent;
pollingConfig?: PollingConfig;
handlePolicyIds?: Cardano.PolicyId[];
};

const createWallet = async (props: CreateWalletProps) => {
Expand Down Expand Up @@ -110,7 +114,7 @@ const createWallet = async (props: CreateWalletProps) => {
const stakePoolProvider = createStubStakePoolProvider();

return new PersonalWallet(
{ name, polling: props.pollingConfig },
{ handlePolicyIds: props.handlePolicyIds, name, polling: props.pollingConfig },
{
addressDiscovery: props?.addressDiscovery,
assetProvider,
Expand All @@ -135,7 +139,8 @@ const createWallet = async (props: CreateWalletProps) => {
const assertWalletProperties = async (
wallet: PersonalWallet,
expectedDelegateeId: Cardano.PoolId | undefined,
expectedRewardsHistory = flatten([...mocks.rewardsHistory.values()])
expectedRewardsHistory = flatten([...mocks.rewardsHistory.values()]),
expectedHandles?: Partial<HandleInfo>[]
) => {
expect(wallet.keyAgent).toBeTruthy();
// name
Expand Down Expand Up @@ -177,7 +182,16 @@ const assertWalletProperties = async (
expect(addresses[0].address).toEqual(address);
expect(addresses[0].rewardAccount).toEqual(rewardAccount);
// assets$
expect(await firstValueFrom(wallet.assetInfo$)).toEqual(new Map([[AssetId.TSLA, mocks.asset]]));
expect(await firstValueFrom(wallet.assetInfo$)).toEqual(
new Map([
[AssetId.TSLA, mocks.asset],
[handleAssetId, handleAssetInfo]
])
);
// handles$
await (expectedHandles
? expect(firstValueFrom(wallet.handles$)).resolves.toMatchObject(expectedHandles)
: expect(firstValueFrom(wallet.handles$)).rejects.toThrowError(InvalidConfigurationError));
// inputAddressResolver
expect(typeof wallet.util).toBe('object');
};
Expand Down Expand Up @@ -284,6 +298,7 @@ describe('PersonalWallet load', () => {
await assertWalletProperties(wallet1, somePartialStakePools[0].id);
wallet1.shutdown();
const wallet2 = await createWallet({
handlePolicyIds: [mocks.handlePolicyId],
providers: {
chainHistoryProvider: mocks.mockChainHistoryProvider2(100),
networkInfoProvider: mocks.mockNetworkInfoProvider2(100),
Expand All @@ -292,7 +307,7 @@ describe('PersonalWallet load', () => {
},
stores
});
await assertWalletProperties(wallet2, somePartialStakePools[0].id);
await assertWalletProperties(wallet2, somePartialStakePools[0].id, undefined, [{ handle: mocks.handle }]);
await waitForWalletStateSettle(wallet2);
await assertWalletProperties2(wallet2);
wallet2.shutdown();
Expand Down
Loading

0 comments on commit 1c3b532

Please sign in to comment.