Skip to content

Commit 8b1e755

Browse files
authored
Merge pull request #752 from input-output-hk/feat/track-own-handles
feat: discover own handles in PersonalWallet
2 parents c7c3523 + 1c3b532 commit 8b1e755

File tree

15 files changed

+304
-27
lines changed

15 files changed

+304
-27
lines changed

packages/util-dev/src/mockProviders/mockAssetProvider.ts

+19-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { Asset, Cardano } from '@cardano-sdk/core';
2+
import { handleAssetId, handleAssetName, handleFingerprint, handlePolicyId } from './mockData';
23

3-
export const asset = {
4+
export const asset: Asset.AssetInfo = {
45
assetId: Cardano.AssetId('659f2917fb63f12b33667463ee575eeac1845bbc736b9c0bbc40ba8254534c41'),
56
fingerprint: Cardano.AssetFingerprint('asset1rjklcrnsdzqp65wjgrg55sy9723kw09mlgvlc3'),
67
history: [
@@ -9,17 +10,32 @@ export const asset = {
910
transactionId: Cardano.TransactionId('886206542d63b23a047864021fbfccf291d78e47c1e59bd4c75fbc67b248c5e8')
1011
}
1112
],
13+
mintOrBurnCount: 5,
1214
name: Cardano.AssetName('54534c41'),
1315
nftMetadata: null,
1416
policyId: Cardano.PolicyId('7eae28af2208be856f7a119668ae52a49b73725e326dc16579dcc373'),
1517
quantity: 1000n,
1618
supply: 1000n,
1719
tokenMetadata: null
18-
} as Asset.AssetInfo;
20+
};
21+
22+
export const handleAssetInfo: Asset.AssetInfo = {
23+
assetId: handleAssetId,
24+
fingerprint: handleFingerprint,
25+
mintOrBurnCount: 1,
26+
name: handleAssetName,
27+
policyId: handlePolicyId,
28+
quantity: 1n,
29+
supply: 1n
30+
};
1931

2032
export const mockAssetProvider = () => ({
2133
getAsset: jest.fn().mockResolvedValue(asset),
22-
getAssets: jest.fn().mockResolvedValue([asset]),
34+
getAssets: jest
35+
.fn()
36+
.mockImplementation(async ({ assetIds }) =>
37+
assetIds.map((assetId: Cardano.AssetId) => (assetId === handleAssetId ? handleAssetInfo : asset))
38+
),
2339
healthCheck: jest.fn().mockResolvedValue({ ok: true })
2440
});
2541

packages/util-dev/src/mockProviders/mockChainHistoryProvider.ts

+8-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import * as AssetId from '../assetId';
22
import { Cardano, Paginated } from '@cardano-sdk/core';
3-
import { currentEpoch, ledgerTip, stakeKeyHash } from './mockData';
3+
import { currentEpoch, handleAssetId, ledgerTip, stakeKeyHash } from './mockData';
44
import { somePartialStakePools } from '../createStubStakePoolProvider';
55
import delay from 'delay';
66

@@ -129,7 +129,13 @@ export const queryTransactionsResult: Paginated<Cardano.HydratedTx> = {
129129
address: Cardano.PaymentAddress(
130130
'addr_test1qq585l3hyxgj3nas2v3xymd23vvartfhceme6gv98aaeg9muzcjqw982pcftgx53fu5527z2cj2tkx2h8ux2vxsg475q2g7k3g'
131131
),
132-
value: { assets: new Map([[AssetId.TSLA, 1n]]), coins: 5_000_000n }
132+
value: {
133+
assets: new Map([
134+
[AssetId.TSLA, 1n],
135+
[handleAssetId, 1n]
136+
]),
137+
coins: 5_000_000n
138+
}
133139
}
134140
],
135141
validityInterval: {

packages/util-dev/src/mockProviders/mockData.ts

+9
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,15 @@ export const stakeKeyHash = Cardano.RewardAccount.toHash(rewardAccount);
55

66
export const rewardAccountBalance = 33_333n;
77

8+
export const handlePolicyId = Cardano.PolicyId('f0ff48bbb7bbe9d59a40f1ce90e9e9d0ff5002ec48f232b49ca0fb9a');
9+
export const handle = 'bob';
10+
export const handleAssetId = Cardano.AssetId.fromParts(
11+
handlePolicyId,
12+
Cardano.AssetName(Buffer.from('bob').toString('hex'))
13+
);
14+
export const handleAssetName = Cardano.AssetName(Buffer.from(handle, 'utf8').toString('hex'));
15+
export const handleFingerprint = Cardano.AssetFingerprint('asset1f0azzptnr8dghzjh7egqvdjmt33e3lz5uy59th');
16+
817
export const ledgerTip = {
918
blockNo: Cardano.BlockNo(1_111_111),
1019
hash: Cardano.BlockId('10d64cc11e9b20e15b6c46aa7b1fed11246f437e62225655a30ea47bf8cc22d0'),

packages/util-dev/src/mockProviders/mockUtxoProvider.ts

+2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import * as AssetId from '../assetId';
22
import { Cardano, UtxoProvider } from '@cardano-sdk/core';
3+
import { handleAssetId } from './mockData';
34
import delay from 'delay';
45

56
export const utxo: Cardano.Utxo[] = [
@@ -106,6 +107,7 @@ export const utxo: Cardano.Utxo[] = [
106107
'addr_test1qq585l3hyxgj3nas2v3xymd23vvartfhceme6gv98aaeg9muzcjqw982pcftgx53fu5527z2cj2tkx2h8ux2vxsg475q2g7k3g'
107108
),
108109
value: {
110+
assets: new Map([[handleAssetId, 1n]]),
109111
coins: 9_825_963n
110112
}
111113
}

packages/wallet/src/PersonalWallet/PersonalWallet.ts

+23-2
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import {
2626
createAssetsTracker,
2727
createBalanceTracker,
2828
createDelegationTracker,
29+
createHandlesTracker,
2930
createProviderStatusTracker,
3031
createSimpleConnectionStatusTracker,
3132
createTransactionsTracker,
@@ -55,6 +56,7 @@ import {
5556
import {
5657
Assets,
5758
FinalizeTxProps,
59+
HandleInfo,
5860
ObservableWallet,
5961
SignDataProps,
6062
SyncStatus,
@@ -77,13 +79,15 @@ import {
7779
map,
7880
mergeMap,
7981
switchMap,
80-
tap
82+
tap,
83+
throwError
8184
} from 'rxjs';
8285
import { Cip30DataSignature } from '@cardano-sdk/dapp-connector';
8386
import {
8487
GenericTxBuilder,
8588
InitializeTxProps,
8689
InitializeTxResult,
90+
InvalidConfigurationError,
8791
TxBuilderDependencies,
8892
finalizeTx,
8993
initializeTx
@@ -100,6 +104,10 @@ import isEqual from 'lodash/isEqual';
100104
export interface PersonalWalletProps {
101105
readonly name: string;
102106
readonly polling?: PollingConfig;
107+
/**
108+
* If set, will track and emit own handles on PersonalWallet.handles$ observable
109+
*/
110+
readonly handlePolicyIds?: Cardano.PolicyId[];
103111
readonly retryBackoffConfig?: RetryBackoffConfig;
104112
}
105113

@@ -193,6 +201,7 @@ export class PersonalWallet implements ObservableWallet {
193201
readonly protocolParameters$: TrackerSubject<Cardano.ProtocolParameters>;
194202
readonly genesisParameters$: TrackerSubject<Cardano.CompactGenesis>;
195203
readonly assetInfo$: TrackerSubject<Assets>;
204+
readonly handles$: Observable<HandleInfo[]>;
196205
readonly fatalError$: Subject<unknown>;
197206
readonly syncStatus: SyncStatus;
198207
readonly name: string;
@@ -212,7 +221,8 @@ export class PersonalWallet implements ObservableWallet {
212221
retryBackoffConfig = {
213222
initialInterval: Math.min(pollInterval, 1000),
214223
maxInterval
215-
}
224+
},
225+
handlePolicyIds
216226
}: PersonalWalletProps,
217227
{
218228
txSubmitProvider,
@@ -451,6 +461,17 @@ export class PersonalWallet implements ObservableWallet {
451461
}),
452462
stores.assets
453463
);
464+
465+
this.handles$ = handlePolicyIds?.length
466+
? createHandlesTracker({
467+
assetInfo$: this.assetInfo$,
468+
handlePolicyIds,
469+
logger: contextLogger(this.#logger, 'handles$'),
470+
tip$: this.tip$,
471+
utxo$: this.utxo.total$
472+
})
473+
: throwError(() => new InvalidConfigurationError('Missing handlePolicyIds option in PersonalWallet'));
474+
454475
this.util = createWalletUtil({
455476
protocolParameters$: this.protocolParameters$,
456477
utxo: this.utxo

packages/wallet/src/services/DelegationTracker/DelegationDistributionTracker.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ import {
1313
switchMap,
1414
withLatestFrom
1515
} from 'rxjs';
16-
import { arrayEquals, delegatedStakeEquals } from '../util';
1716
import { createUtxoBalanceByAddressTracker } from '../BalanceTracker';
17+
import { delegatedStakeEquals, sameArrayItems } from '../util';
1818
import _groupBy from 'lodash/groupBy';
1919
import _map from 'lodash/map';
2020

@@ -103,7 +103,7 @@ export const createDelegationDistributionTracker = ({
103103
);
104104
return delegatedStakes.map((pool, idx) => ({ ...pool, percentage: percentages[idx] }));
105105
}),
106-
distinctUntilChanged((a, b) => arrayEquals(a, b, delegatedStakeEquals)),
106+
distinctUntilChanged((a, b) => sameArrayItems(a, b, delegatedStakeEquals)),
107107
map((delegatedStakes) => new Map(delegatedStakes.map((delegation) => [delegation.pool.id, delegation])))
108108
);
109109
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import { Assets, HandleInfo } from '../types';
2+
import { Cardano } from '@cardano-sdk/core';
3+
import {
4+
EMPTY,
5+
Observable,
6+
combineLatest,
7+
distinctUntilChanged,
8+
map,
9+
mergeMap,
10+
of,
11+
shareReplay,
12+
withLatestFrom
13+
} from 'rxjs';
14+
import { Logger } from 'ts-log';
15+
import { deepEquals, isNotNil } from '@cardano-sdk/util';
16+
import { sameArrayItems, strictEquals } from './util';
17+
import uniqBy from 'lodash/uniqBy';
18+
19+
export interface HandlesTrackerProps {
20+
utxo$: Observable<Cardano.Utxo[]>;
21+
assetInfo$: Observable<Assets>;
22+
tip$: Observable<Cardano.Tip>;
23+
handlePolicyIds: Cardano.PolicyId[];
24+
logger: Logger;
25+
}
26+
27+
const handleInfoEquals = (a: HandleInfo, b: HandleInfo) =>
28+
a.assetId === b.assetId &&
29+
a.resolvedAt.hash === b.resolvedAt.hash &&
30+
deepEquals(a.tokenMetadata, b.tokenMetadata) &&
31+
deepEquals(a.nftMetadata, b.nftMetadata);
32+
33+
export const createHandlesTracker = ({ tip$, assetInfo$, handlePolicyIds, logger, utxo$ }: HandlesTrackerProps) =>
34+
combineLatest([
35+
utxo$.pipe(
36+
map((utxo) =>
37+
utxo.flatMap(([_, txOut]) =>
38+
uniqBy(
39+
[...(txOut.value.assets?.keys() || [])]
40+
.filter((assetId) => handlePolicyIds.some((policyId) => assetId.startsWith(policyId)))
41+
.map((assetId) => ({
42+
handleAssetId: assetId,
43+
txOut
44+
})),
45+
({ handleAssetId }) => handleAssetId
46+
)
47+
)
48+
),
49+
distinctUntilChanged((a, b) => sameArrayItems(a, b, strictEquals)),
50+
withLatestFrom(tip$)
51+
),
52+
assetInfo$
53+
]).pipe(
54+
mergeMap(([[utxo, tip], assets]) => {
55+
const handlesWithAssetInfo = utxo
56+
.map(({ handleAssetId, txOut }): HandleInfo | null => {
57+
const assetInfo = assets.get(handleAssetId);
58+
if (!assetInfo) {
59+
logger.debug(`Asset info not (yet?) found for ${handleAssetId}`);
60+
return null;
61+
}
62+
return {
63+
...assetInfo,
64+
handle: Buffer.from(Cardano.AssetId.getAssetName(handleAssetId), 'hex').toString('utf8'),
65+
hasDatum: !!txOut.datum,
66+
resolvedAddresses: {
67+
cardano: txOut.address
68+
},
69+
resolvedAt: tip
70+
};
71+
})
72+
.filter(isNotNil);
73+
if (utxo.length > 0 && handlesWithAssetInfo.length === 0) {
74+
// AssetInfo is still resolving
75+
return EMPTY;
76+
}
77+
return of(
78+
handlesWithAssetInfo.filter(({ handle, supply }) => {
79+
if (supply > 1n) {
80+
logger.warn(`Omitting handle with supply >1: ${handle}`);
81+
return false;
82+
}
83+
return true;
84+
})
85+
);
86+
}),
87+
distinctUntilChanged((a, b) => sameArrayItems(a, b, handleInfoEquals)),
88+
shareReplay({ bufferSize: 1, refCount: true })
89+
);

packages/wallet/src/services/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,4 @@ export * from './SupplyDistributionTracker';
1414
export * from './SmartTxSubmitProvider';
1515
export * from './KeyAgent';
1616
export * from './AddressDiscovery';
17+
export * from './HandlesTracker';

packages/wallet/src/services/util/equals.ts

+6-6
Original file line numberDiff line numberDiff line change
@@ -4,29 +4,29 @@ import { GroupedAddress } from '@cardano-sdk/key-management';
44

55
export const strictEquals = <T>(a: T, b: T) => a === b;
66

7-
export const arrayEquals = <T>(arrayA: T[], arrayB: T[], itemEquals: (a: T, b: T) => boolean) =>
7+
export const sameArrayItems = <T>(arrayA: T[], arrayB: T[], itemEquals: (a: T, b: T) => boolean) =>
88
arrayA.length === arrayB.length && arrayA.every((a) => arrayB.some((b) => itemEquals(a, b)));
99

10-
export const shallowArrayEquals = <T>(a: T[], b: T[]) => arrayEquals(a, b, strictEquals);
10+
export const shallowArrayEquals = <T>(a: T[], b: T[]) => sameArrayItems(a, b, strictEquals);
1111

1212
export const tipEquals = (a: Cardano.Tip, b: Cardano.Tip) => a.hash === b.hash;
1313

1414
export const txEquals = (a: Cardano.HydratedTx, b: Cardano.HydratedTx) => a.id === b.id;
1515

16-
export const transactionsEquals = (a: Cardano.HydratedTx[], b: Cardano.HydratedTx[]) => arrayEquals(a, b, txEquals);
16+
export const transactionsEquals = (a: Cardano.HydratedTx[], b: Cardano.HydratedTx[]) => sameArrayItems(a, b, txEquals);
1717

1818
export const txInEquals = (a: Cardano.TxIn, b: Cardano.TxIn) => a.txId === b.txId && a.index === b.index;
1919

2020
export const utxoEquals = (a: Cardano.Utxo[], b: Cardano.Utxo[]) =>
21-
arrayEquals(a, b, ([aTxIn], [bTxIn]) => txInEquals(aTxIn, bTxIn));
21+
sameArrayItems(a, b, ([aTxIn], [bTxIn]) => txInEquals(aTxIn, bTxIn));
2222

2323
export const eraSummariesEquals = (a: EraSummary[], b: EraSummary[]) =>
24-
arrayEquals(a, b, (es1, es2) => es1.start.slot === es2.start.slot);
24+
sameArrayItems(a, b, (es1, es2) => es1.start.slot === es2.start.slot);
2525

2626
const groupedAddressEquals = (a: GroupedAddress, b: GroupedAddress) => a.address === b.address;
2727

2828
export const groupedAddressesEquals = (a: GroupedAddress[], b: GroupedAddress[]) =>
29-
arrayEquals(a, b, groupedAddressEquals);
29+
sameArrayItems(a, b, groupedAddressEquals);
3030

3131
export const epochInfoEquals = (a: EpochInfo, b: EpochInfo) => a.epochNo === b.epochNo;
3232

packages/wallet/src/types.ts

+12-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,12 @@
1-
import { Asset, Cardano, EpochInfo, EraSummary, NetworkInfoProvider, TxCBOR } from '@cardano-sdk/core';
1+
import {
2+
Asset,
3+
Cardano,
4+
EpochInfo,
5+
EraSummary,
6+
HandleResolution,
7+
NetworkInfoProvider,
8+
TxCBOR
9+
} from '@cardano-sdk/core';
210
import { BalanceTracker, DelegationTracker, TransactionsTracker, UtxoTracker } from './services';
311
import { Cip30DataSignature } from '@cardano-sdk/dapp-connector';
412
import { GroupedAddress, cip8 } from '@cardano-sdk/key-management';
@@ -37,6 +45,8 @@ export type FinalizeTxProps = Omit<TxContext, 'ownAddresses'> & {
3745
tx: Cardano.TxBodyWithHash;
3846
};
3947

48+
export type HandleInfo = HandleResolution & Asset.AssetInfo;
49+
4050
export interface ObservableWallet {
4151
readonly balance: BalanceTracker;
4252
readonly delegation: DelegationTracker;
@@ -48,6 +58,7 @@ export interface ObservableWallet {
4858
readonly currentEpoch$: Observable<EpochInfo>;
4959
readonly protocolParameters$: Observable<Cardano.ProtocolParameters>;
5060
readonly addresses$: Observable<GroupedAddress[]>;
61+
readonly handles$: Observable<HandleInfo[]>;
5162
/** All owned and historical assets */
5263
readonly assetInfo$: Observable<Assets>;
5364
/**

0 commit comments

Comments
 (0)