Skip to content

Commit b0c08fd

Browse files
authored
Merge pull request #1192 from input-output-hk/feat/lw-9897-implement-drep-registration-tracker
feat(wallet): implement drep registration tracker
2 parents d53a96e + 9cf346f commit b0c08fd

File tree

13 files changed

+278
-23
lines changed

13 files changed

+278
-23
lines changed

packages/wallet/src/Wallets/BaseWallet.ts

Lines changed: 24 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import {
2828
createAddressTracker,
2929
createAssetsTracker,
3030
createBalanceTracker,
31+
createDRepRegistrationTracker,
3132
createDelegationTracker,
3233
createHandlesTracker,
3334
createProviderStatusTracker,
@@ -89,7 +90,7 @@ import {
8990
import { Bip32Account, GroupedAddress, WitnessedTx, Witnesser, cip8, util } from '@cardano-sdk/key-management';
9091
import { ChangeAddressResolver, InputSelector, roundRobinRandomImprove } from '@cardano-sdk/input-selection';
9192
import { Cip30DataSignature } from '@cardano-sdk/dapp-connector';
92-
import { Ed25519PublicKeyHex } from '@cardano-sdk/crypto';
93+
import { Ed25519PublicKey, Ed25519PublicKeyHex } from '@cardano-sdk/crypto';
9394
import {
9495
GenericTxBuilder,
9596
InitializeTxProps,
@@ -210,6 +211,8 @@ const processOutgoingTx = (input: Cardano.Tx | TxCBOR | OutgoingTx | WitnessedTx
210211
id: input.id
211212
};
212213
};
214+
const getDRepKeyHash = async (dRepKey: Ed25519PublicKeyHex | undefined) =>
215+
dRepKey ? (await Ed25519PublicKey.fromHex(dRepKey).hash()).hex() : undefined;
213216

214217
export class BaseWallet implements ObservableWallet {
215218
#inputSelector: InputSelector;
@@ -254,6 +257,10 @@ export class BaseWallet implements ObservableWallet {
254257
readonly handleProvider: HandleProvider;
255258
readonly changeAddressResolver: ChangeAddressResolver;
256259
readonly publicStakeKeys$: TrackerSubject<PubStakeKeyAndStatus[]>;
260+
readonly governance: {
261+
readonly isRegisteredAsDRep$: Observable<boolean>;
262+
getPubDRepKey(): Promise<Ed25519PublicKeyHex | undefined>;
263+
};
257264
handles$: Observable<HandleInfo[]>;
258265

259266
// eslint-disable-next-line max-statements
@@ -551,7 +558,21 @@ export class BaseWallet implements ObservableWallet {
551558
utxo: this.utxo
552559
});
553560

554-
this.getPubDRepKey().catch(() => void 0);
561+
const getPubDRepKey = async (): Promise<Ed25519PublicKeyHex | undefined> => {
562+
if (isBip32PublicCredentialsManager(this.#publicCredentialsManager)) {
563+
return (await this.#publicCredentialsManager.bip32Account.derivePublicKey(util.DREP_KEY_DERIVATION_PATH)).hex();
564+
}
565+
566+
return undefined;
567+
};
568+
569+
this.governance = {
570+
getPubDRepKey,
571+
isRegisteredAsDRep$: createDRepRegistrationTracker({
572+
historyTransactions$: this.transactions.history$,
573+
pubDRepKeyHash$: from(getPubDRepKey().then(getDRepKeyHash))
574+
})
575+
};
555576

556577
this.#logger.debug('Created');
557578
}
@@ -573,7 +594,7 @@ export class BaseWallet implements ObservableWallet {
573594
witness
574595
}: FinalizeTxProps): Promise<Cardano.Tx> {
575596
const knownAddresses = await firstValueFrom(this.addresses$);
576-
const dRepPublicKey = await this.getPubDRepKey();
597+
const dRepPublicKey = await this.governance.getPubDRepKey();
577598

578599
const context = {
579600
...signingContext,
@@ -781,14 +802,6 @@ export class BaseWallet implements ObservableWallet {
781802
throw new Error('getPubDRepKey is not supported by script wallets');
782803
}
783804

784-
async getPubDRepKey(): Promise<Ed25519PublicKeyHex | undefined> {
785-
if (isBip32PublicCredentialsManager(this.#publicCredentialsManager)) {
786-
return (await this.#publicCredentialsManager.bip32Account.derivePublicKey(util.DREP_KEY_DERIVATION_PATH)).hex();
787-
}
788-
789-
return undefined;
790-
}
791-
792805
async discoverAddresses(): Promise<GroupedAddress[]> {
793806
if (isBip32PublicCredentialsManager(this.#publicCredentialsManager)) {
794807
const addresses = await this.#publicCredentialsManager.addressDiscovery.discover(

packages/wallet/src/cip30.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -564,7 +564,7 @@ const extendedCip95WalletApi = (
564564
logger.debug('getting public DRep key');
565565
try {
566566
const wallet = await firstValueFrom(wallet$);
567-
const dReKey = await wallet.getPubDRepKey();
567+
const dReKey = await wallet.governance.getPubDRepKey();
568568

569569
if (!dReKey) throw new Error('Shared wallet does not support DRep key');
570570

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/* eslint-disable sonarjs/cognitive-complexity */
2+
import * as Crypto from '@cardano-sdk/crypto';
3+
import { Cardano } from '@cardano-sdk/core';
4+
import { Observable, distinctUntilChanged, map, switchMap } from 'rxjs';
5+
import { TrackerSubject } from '@cardano-sdk/util-rxjs';
6+
7+
interface CreateDRepRegistrationTrackerProps {
8+
historyTransactions$: Observable<Cardano.HydratedTx[]>;
9+
pubDRepKeyHash$: Observable<Crypto.Ed25519KeyHashHex | undefined>;
10+
}
11+
12+
interface IsOwnDRepCredentialProps {
13+
certificate: Cardano.Certificate;
14+
dRepKeyHash: Crypto.Ed25519KeyHashHex;
15+
}
16+
17+
const hasOwnDRepCredential = ({ certificate, dRepKeyHash }: IsOwnDRepCredentialProps) =>
18+
'dRepCredential' in certificate &&
19+
certificate.dRepCredential.type === Cardano.CredentialType.KeyHash &&
20+
certificate.dRepCredential.hash === Crypto.Hash28ByteBase16.fromEd25519KeyHashHex(dRepKeyHash);
21+
22+
export const createDRepRegistrationTracker = ({
23+
historyTransactions$,
24+
pubDRepKeyHash$
25+
}: CreateDRepRegistrationTrackerProps): TrackerSubject<boolean> =>
26+
new TrackerSubject(
27+
pubDRepKeyHash$.pipe(
28+
switchMap((dRepKeyHash) =>
29+
historyTransactions$.pipe(
30+
map((txs) => {
31+
if (!dRepKeyHash) return false;
32+
const reverseTxs = [...txs].reverse();
33+
34+
for (const {
35+
body: { certificates }
36+
} of reverseTxs) {
37+
if (certificates) {
38+
for (const certificate of certificates) {
39+
if (!hasOwnDRepCredential({ certificate, dRepKeyHash })) continue;
40+
if (certificate.__typename === Cardano.CertificateType.UnregisterDelegateRepresentative) return false;
41+
if (certificate.__typename === Cardano.CertificateType.RegisterDelegateRepresentative) return true;
42+
}
43+
}
44+
}
45+
46+
return false;
47+
})
48+
)
49+
),
50+
distinctUntilChanged()
51+
)
52+
);

packages/wallet/src/services/WalletUtil.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -296,7 +296,7 @@ export const requiresForeignSignatures = async (tx: Cardano.Tx, wallet: Observab
296296
})
297297
.filter((acct): acct is KeyManagementUtil.StakeKeySignerData => acct.derivationPath !== null);
298298

299-
const dRepKey = await wallet.getPubDRepKey();
299+
const dRepKey = await wallet.governance.getPubDRepKey();
300300
const dRepKeyHash = dRepKey ? (await Crypto.Ed25519PublicKey.fromHex(dRepKey).hash()).hex() : undefined;
301301

302302
return (

packages/wallet/src/services/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,4 @@ export * from './HandlesTracker';
1818
export * from './ChangeAddress';
1919
export * from './AddressTracker';
2020
export * from './WalletAssetProvider';
21+
export * from './DRepRegistrationTracker';

packages/wallet/src/types.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,12 @@ export interface ObservableWallet {
8484
readonly addresses$: Observable<WalletAddress[]>;
8585
readonly publicStakeKeys$: Observable<PubStakeKeyAndStatus[]>;
8686
readonly handles$: Observable<HandleInfo[]>;
87+
readonly governance: {
88+
/** true this wallet is registered as drep */
89+
readonly isRegisteredAsDRep$: Observable<boolean>;
90+
/** Returns the wallet account's public DRep Key or undefined if the wallet doesn't control any DRep key */
91+
getPubDRepKey(): Promise<Ed25519PublicKeyHex | undefined>;
92+
};
8793
/** All owned and historical assets */
8894
readonly assetInfo$: Observable<Assets>;
8995
/**
@@ -96,8 +102,6 @@ export interface ObservableWallet {
96102

97103
getName(): Promise<string>;
98104

99-
/** Returns the wallet account's public DRep Key or undefined if the wallet doesn't control any DRep key */
100-
getPubDRepKey(): Promise<Ed25519PublicKeyHex | undefined>;
101105
/**
102106
* @deprecated Use `createTxBuilder()` instead.
103107
* @throws InputSelectionError

packages/wallet/test/PersonalWallet/load.test.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,9 @@ const assertWalletProperties = async (
200200
: expect(firstValueFrom(wallet.handles$)).rejects.toThrowError(InvalidConfigurationError));
201201
// inputAddressResolver
202202
expect(typeof wallet.util).toBe('object');
203+
expect(typeof wallet.governance).toBe('object');
204+
// isRegisteredAsDRep$
205+
expect(typeof (await firstValueFrom(wallet.governance.isRegisteredAsDRep$))).toBe('boolean');
203206
};
204207

205208
const assertWalletProperties2 = async (wallet: ObservableWallet) => {

packages/wallet/test/PersonalWallet/methods.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -447,7 +447,7 @@ describe('BaseWallet methods', () => {
447447
});
448448

449449
test('rejects if bech32 DRepID is not a type 6 address', async () => {
450-
const dRepKey = await wallet.getPubDRepKey();
450+
const dRepKey = await wallet.governance.getPubDRepKey();
451451
for (const type in Cardano.AddressType) {
452452
if (!Number.isNaN(Number(type)) && Number(type) !== Cardano.AddressType.EnterpriseKey) {
453453
const drepid = buildDRepIDFromDRepKey(dRepKey!, 0, type as unknown as Cardano.AddressType);
@@ -458,7 +458,7 @@ describe('BaseWallet methods', () => {
458458
});
459459

460460
it('getPubDRepKey', async () => {
461-
const response = await wallet.getPubDRepKey();
461+
const response = await wallet.governance.getPubDRepKey();
462462
expect(typeof response).toBe('string');
463463
});
464464

@@ -486,7 +486,7 @@ describe('BaseWallet methods', () => {
486486
);
487487
await waitForWalletStateSettle(wallet);
488488

489-
const response = await wallet.getPubDRepKey();
489+
const response = await wallet.governance.getPubDRepKey();
490490
expect(response).toBe('string');
491491
expect(bip32Account.derivePublicKey).toHaveBeenCalledTimes(3);
492492
});

packages/wallet/test/integration/cip30mapping.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ const createWalletAndApiWithStores = async (
7676
submitTx: createMockGenericCallback(),
7777
...(!!getCollateralCallback && { getCollateral: getCollateralCallback })
7878
};
79-
wallet.getPubDRepKey = jest.fn(wallet.getPubDRepKey);
79+
wallet.governance.getPubDRepKey = jest.fn(wallet.governance.getPubDRepKey);
8080

8181
const api = cip30.createWalletApi(of(wallet), confirmationCallback, { logger });
8282
if (settle) await waitForWalletStateSettle(wallet);
@@ -606,10 +606,10 @@ describe('cip30', () => {
606606
describe('api.getPubDRepKey', () => {
607607
test("returns the DRep key derived from the wallet's public key", async () => {
608608
const cip95PubDRepKey = await api.getPubDRepKey(context);
609-
expect(cip95PubDRepKey).toEqual(await wallet.getPubDRepKey());
609+
expect(cip95PubDRepKey).toEqual(await wallet.governance.getPubDRepKey());
610610
});
611611
test('throws an ApiError on unexpected error', async () => {
612-
(wallet.getPubDRepKey as jest.Mock).mockRejectedValueOnce(new Error('unexpected error'));
612+
(wallet.governance.getPubDRepKey as jest.Mock).mockRejectedValueOnce(new Error('unexpected error'));
613613
try {
614614
await api.getPubDRepKey(context);
615615
} catch (error) {

0 commit comments

Comments
 (0)