Skip to content

Commit

Permalink
Merge pull request #1192 from input-output-hk/feat/lw-9897-implement-…
Browse files Browse the repository at this point in the history
…drep-registration-tracker

feat(wallet): implement drep registration tracker
  • Loading branch information
vetalcore committed Apr 9, 2024
2 parents d53a96e + 9cf346f commit b0c08fd
Show file tree
Hide file tree
Showing 13 changed files with 278 additions and 23 deletions.
35 changes: 24 additions & 11 deletions packages/wallet/src/Wallets/BaseWallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import {
createAddressTracker,
createAssetsTracker,
createBalanceTracker,
createDRepRegistrationTracker,
createDelegationTracker,
createHandlesTracker,
createProviderStatusTracker,
Expand Down Expand Up @@ -89,7 +90,7 @@ import {
import { Bip32Account, GroupedAddress, WitnessedTx, Witnesser, cip8, util } from '@cardano-sdk/key-management';
import { ChangeAddressResolver, InputSelector, roundRobinRandomImprove } from '@cardano-sdk/input-selection';
import { Cip30DataSignature } from '@cardano-sdk/dapp-connector';
import { Ed25519PublicKeyHex } from '@cardano-sdk/crypto';
import { Ed25519PublicKey, Ed25519PublicKeyHex } from '@cardano-sdk/crypto';
import {
GenericTxBuilder,
InitializeTxProps,
Expand Down Expand Up @@ -210,6 +211,8 @@ const processOutgoingTx = (input: Cardano.Tx | TxCBOR | OutgoingTx | WitnessedTx
id: input.id
};
};
const getDRepKeyHash = async (dRepKey: Ed25519PublicKeyHex | undefined) =>
dRepKey ? (await Ed25519PublicKey.fromHex(dRepKey).hash()).hex() : undefined;

export class BaseWallet implements ObservableWallet {
#inputSelector: InputSelector;
Expand Down Expand Up @@ -254,6 +257,10 @@ export class BaseWallet implements ObservableWallet {
readonly handleProvider: HandleProvider;
readonly changeAddressResolver: ChangeAddressResolver;
readonly publicStakeKeys$: TrackerSubject<PubStakeKeyAndStatus[]>;
readonly governance: {
readonly isRegisteredAsDRep$: Observable<boolean>;
getPubDRepKey(): Promise<Ed25519PublicKeyHex | undefined>;
};
handles$: Observable<HandleInfo[]>;

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

this.getPubDRepKey().catch(() => void 0);
const getPubDRepKey = async (): Promise<Ed25519PublicKeyHex | undefined> => {
if (isBip32PublicCredentialsManager(this.#publicCredentialsManager)) {
return (await this.#publicCredentialsManager.bip32Account.derivePublicKey(util.DREP_KEY_DERIVATION_PATH)).hex();
}

return undefined;
};

this.governance = {
getPubDRepKey,
isRegisteredAsDRep$: createDRepRegistrationTracker({
historyTransactions$: this.transactions.history$,
pubDRepKeyHash$: from(getPubDRepKey().then(getDRepKeyHash))
})
};

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

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

async getPubDRepKey(): Promise<Ed25519PublicKeyHex | undefined> {
if (isBip32PublicCredentialsManager(this.#publicCredentialsManager)) {
return (await this.#publicCredentialsManager.bip32Account.derivePublicKey(util.DREP_KEY_DERIVATION_PATH)).hex();
}

return undefined;
}

async discoverAddresses(): Promise<GroupedAddress[]> {
if (isBip32PublicCredentialsManager(this.#publicCredentialsManager)) {
const addresses = await this.#publicCredentialsManager.addressDiscovery.discover(
Expand Down
2 changes: 1 addition & 1 deletion packages/wallet/src/cip30.ts
Original file line number Diff line number Diff line change
Expand Up @@ -564,7 +564,7 @@ const extendedCip95WalletApi = (
logger.debug('getting public DRep key');
try {
const wallet = await firstValueFrom(wallet$);
const dReKey = await wallet.getPubDRepKey();
const dReKey = await wallet.governance.getPubDRepKey();

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

Expand Down
52 changes: 52 additions & 0 deletions packages/wallet/src/services/DRepRegistrationTracker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/* eslint-disable sonarjs/cognitive-complexity */
import * as Crypto from '@cardano-sdk/crypto';
import { Cardano } from '@cardano-sdk/core';
import { Observable, distinctUntilChanged, map, switchMap } from 'rxjs';
import { TrackerSubject } from '@cardano-sdk/util-rxjs';

interface CreateDRepRegistrationTrackerProps {
historyTransactions$: Observable<Cardano.HydratedTx[]>;
pubDRepKeyHash$: Observable<Crypto.Ed25519KeyHashHex | undefined>;
}

interface IsOwnDRepCredentialProps {
certificate: Cardano.Certificate;
dRepKeyHash: Crypto.Ed25519KeyHashHex;
}

const hasOwnDRepCredential = ({ certificate, dRepKeyHash }: IsOwnDRepCredentialProps) =>
'dRepCredential' in certificate &&
certificate.dRepCredential.type === Cardano.CredentialType.KeyHash &&
certificate.dRepCredential.hash === Crypto.Hash28ByteBase16.fromEd25519KeyHashHex(dRepKeyHash);

export const createDRepRegistrationTracker = ({
historyTransactions$,
pubDRepKeyHash$
}: CreateDRepRegistrationTrackerProps): TrackerSubject<boolean> =>
new TrackerSubject(
pubDRepKeyHash$.pipe(
switchMap((dRepKeyHash) =>
historyTransactions$.pipe(
map((txs) => {
if (!dRepKeyHash) return false;
const reverseTxs = [...txs].reverse();

for (const {
body: { certificates }
} of reverseTxs) {
if (certificates) {
for (const certificate of certificates) {
if (!hasOwnDRepCredential({ certificate, dRepKeyHash })) continue;
if (certificate.__typename === Cardano.CertificateType.UnregisterDelegateRepresentative) return false;
if (certificate.__typename === Cardano.CertificateType.RegisterDelegateRepresentative) return true;
}
}
}

return false;
})
)
),
distinctUntilChanged()
)
);
2 changes: 1 addition & 1 deletion packages/wallet/src/services/WalletUtil.ts
Original file line number Diff line number Diff line change
Expand Up @@ -296,7 +296,7 @@ export const requiresForeignSignatures = async (tx: Cardano.Tx, wallet: Observab
})
.filter((acct): acct is KeyManagementUtil.StakeKeySignerData => acct.derivationPath !== null);

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

return (
Expand Down
1 change: 1 addition & 0 deletions packages/wallet/src/services/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,4 @@ export * from './HandlesTracker';
export * from './ChangeAddress';
export * from './AddressTracker';
export * from './WalletAssetProvider';
export * from './DRepRegistrationTracker';
8 changes: 6 additions & 2 deletions packages/wallet/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,12 @@ export interface ObservableWallet {
readonly addresses$: Observable<WalletAddress[]>;
readonly publicStakeKeys$: Observable<PubStakeKeyAndStatus[]>;
readonly handles$: Observable<HandleInfo[]>;
readonly governance: {
/** true this wallet is registered as drep */
readonly isRegisteredAsDRep$: Observable<boolean>;
/** Returns the wallet account's public DRep Key or undefined if the wallet doesn't control any DRep key */
getPubDRepKey(): Promise<Ed25519PublicKeyHex | undefined>;
};
/** All owned and historical assets */
readonly assetInfo$: Observable<Assets>;
/**
Expand All @@ -96,8 +102,6 @@ export interface ObservableWallet {

getName(): Promise<string>;

/** Returns the wallet account's public DRep Key or undefined if the wallet doesn't control any DRep key */
getPubDRepKey(): Promise<Ed25519PublicKeyHex | undefined>;
/**
* @deprecated Use `createTxBuilder()` instead.
* @throws InputSelectionError
Expand Down
3 changes: 3 additions & 0 deletions packages/wallet/test/PersonalWallet/load.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,9 @@ const assertWalletProperties = async (
: expect(firstValueFrom(wallet.handles$)).rejects.toThrowError(InvalidConfigurationError));
// inputAddressResolver
expect(typeof wallet.util).toBe('object');
expect(typeof wallet.governance).toBe('object');
// isRegisteredAsDRep$
expect(typeof (await firstValueFrom(wallet.governance.isRegisteredAsDRep$))).toBe('boolean');
};

const assertWalletProperties2 = async (wallet: ObservableWallet) => {
Expand Down
6 changes: 3 additions & 3 deletions packages/wallet/test/PersonalWallet/methods.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -447,7 +447,7 @@ describe('BaseWallet methods', () => {
});

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

it('getPubDRepKey', async () => {
const response = await wallet.getPubDRepKey();
const response = await wallet.governance.getPubDRepKey();
expect(typeof response).toBe('string');
});

Expand Down Expand Up @@ -486,7 +486,7 @@ describe('BaseWallet methods', () => {
);
await waitForWalletStateSettle(wallet);

const response = await wallet.getPubDRepKey();
const response = await wallet.governance.getPubDRepKey();
expect(response).toBe('string');
expect(bip32Account.derivePublicKey).toHaveBeenCalledTimes(3);
});
Expand Down
6 changes: 3 additions & 3 deletions packages/wallet/test/integration/cip30mapping.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ const createWalletAndApiWithStores = async (
submitTx: createMockGenericCallback(),
...(!!getCollateralCallback && { getCollateral: getCollateralCallback })
};
wallet.getPubDRepKey = jest.fn(wallet.getPubDRepKey);
wallet.governance.getPubDRepKey = jest.fn(wallet.governance.getPubDRepKey);

const api = cip30.createWalletApi(of(wallet), confirmationCallback, { logger });
if (settle) await waitForWalletStateSettle(wallet);
Expand Down Expand Up @@ -606,10 +606,10 @@ describe('cip30', () => {
describe('api.getPubDRepKey', () => {
test("returns the DRep key derived from the wallet's public key", async () => {
const cip95PubDRepKey = await api.getPubDRepKey(context);
expect(cip95PubDRepKey).toEqual(await wallet.getPubDRepKey());
expect(cip95PubDRepKey).toEqual(await wallet.governance.getPubDRepKey());
});
test('throws an ApiError on unexpected error', async () => {
(wallet.getPubDRepKey as jest.Mock).mockRejectedValueOnce(new Error('unexpected error'));
(wallet.governance.getPubDRepKey as jest.Mock).mockRejectedValueOnce(new Error('unexpected error'));
try {
await api.getPubDRepKey(context);
} catch (error) {
Expand Down

0 comments on commit b0c08fd

Please sign in to comment.