Skip to content

Commit

Permalink
feat: support all quorum rules
Browse files Browse the repository at this point in the history
  • Loading branch information
greatertomi committed Jul 15, 2024
1 parent 6a29b3c commit 166cc52
Show file tree
Hide file tree
Showing 10 changed files with 117 additions and 99 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,14 @@ interface UserInfoProps {
onOpenWalletAccounts?: (wallet: AnyBip32Wallet<Wallet.WalletMetadata, Wallet.AccountMetadata>) => void;
}

interface RenderWalletOptionsParams {
wallet: AnyWallet<Wallet.WalletMetadata, Wallet.AccountMetadata>;
lastActiveAccount?: Bip32WalletAccount<Wallet.AccountMetadata>;
}

const NO_WALLETS: AnyWallet<Wallet.WalletMetadata, Wallet.AccountMetadata>[] = [];
const isBip32Wallet = <T, U>(wallet: AnyWallet<T, U>): wallet is AnyBip32Wallet<T, U> =>
wallet.type === WalletType.InMemory || wallet.type === WalletType.Ledger || wallet.type === WalletType.Trezor;

export const UserInfo = ({ onOpenWalletAccounts, avatarVisible = true }: UserInfoProps): React.ReactElement => {
const { t } = useTranslation();
Expand Down Expand Up @@ -79,68 +86,25 @@ export const UserInfo = ({ onOpenWalletAccounts, avatarVisible = true }: UserInf
[cardanoWallet]
);

// TODO: merge with renderBip32Wallet in case wallet option is not different
const renderScriptWallet = useCallback(
(wallet: ScriptWallet<Wallet.WalletMetadata>) => {
const renderWalletOption = useCallback(
({ wallet, lastActiveAccount }: RenderWalletOptionsParams) => {
const walletAvatar = getAvatar(wallet.walletId);

return (
<ProfileDropdown.WalletOption
key={wallet.walletId}
title={wallet.metadata.name}
subtitle={t('sharedWallets.userInfo.label')}
subtitle={lastActiveAccount?.metadata.name || t('sharedWallets.userInfo.label')}
id={`wallet-option-${wallet.walletId}`}
onClick={async () => {
if (activeWalletId === wallet.walletId) {
return;
}
analytics.sendEventToPostHog(PostHogAction.MultiWalletSwitchWallet);

await activateWallet({
walletId: wallet.walletId
});
setIsDropdownMenuOpen(false);
toast.notify({
duration: TOAST_DEFAULT_DURATION,
text: t('multiWallet.activated.wallet', { walletName: wallet.metadata.name })
});
}}
type={getUiWalletType(wallet.type)}
profile={
walletAvatar
? {
fallbackText: fullWalletName,
imageSrc: walletAvatar
}
: undefined
}
/>
);
},
[activateWallet, activeWalletId, analytics, fullWalletName, getAvatar, setIsDropdownMenuOpen, t]
);

const renderBip32Wallet = useCallback(
(wallet: AnyBip32Wallet<Wallet.WalletMetadata, Wallet.AccountMetadata>) => {
const lastActiveAccount = getLastActiveAccount(wallet);
const walletAvatar = getAvatar(wallet.walletId);

return (
<ProfileDropdown.WalletOption
key={wallet.walletId}
title={wallet.metadata.name}
subtitle={lastActiveAccount.metadata.name}
id={`wallet-option-${wallet.walletId}`}
onOpenAccountsMenu={() => onOpenWalletAccounts(wallet)}
onClick={async () => {
if (activeWalletId === wallet.walletId) {
return;
}
analytics.sendEventToPostHog(PostHogAction.MultiWalletSwitchWallet);

await activateWallet({
walletId: wallet.walletId,
accountIndex: lastActiveAccount.accountIndex
...(lastActiveAccount && { accountIndex: lastActiveAccount.accountIndex })
});
setIsDropdownMenuOpen(false);
toast.notify({
Expand All @@ -157,22 +121,35 @@ export const UserInfo = ({ onOpenWalletAccounts, avatarVisible = true }: UserInf
}
: undefined
}
{...(isBip32Wallet(wallet) && {
onOpenAccountsMenu: () => onOpenWalletAccounts(wallet)
})}
/>
);
},
[
getLastActiveAccount,
getAvatar,
fullWalletName,
onOpenWalletAccounts,
activateWallet,
activeWalletId,
analytics,
activateWallet,
fullWalletName,
getAvatar,
onOpenWalletAccounts,
setIsDropdownMenuOpen,
t
]
);

const renderScriptWallet = useCallback(
(wallet: ScriptWallet<Wallet.WalletMetadata>) => renderWalletOption({ wallet }),
[renderWalletOption]
);

const renderBip32Wallet = useCallback(
(wallet: AnyBip32Wallet<Wallet.WalletMetadata, Wallet.AccountMetadata>) =>
renderWalletOption({ wallet, lastActiveAccount: getLastActiveAccount(wallet) }),
[getLastActiveAccount, renderWalletOption]
);

const renderWallet = useCallback(
(wallet: AnyWallet<Wallet.WalletMetadata, Wallet.AccountMetadata>, isLast: boolean) => (
<div key={wallet.walletId}>
Expand Down
61 changes: 23 additions & 38 deletions apps/browser-extension-wallet/src/hooks/useWalletManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,7 @@ import { useCustomSubmitApi } from '@hooks/useCustomSubmitApi';
import { setBackgroundStorage } from '@lib/scripts/background/storage';
import * as KeyManagement from '@cardano-sdk/key-management';
import { Buffer } from 'buffer';
import { Cardano } from '@cardano-sdk/core';
import * as Crypto from '@cardano-sdk/crypto';
import { buildSharedWalletScript, QuorumOptionValue, ScriptKind } from '@lace/core';

const { AVAILABLE_CHAINS, CHAIN } = config();
const DEFAULT_CHAIN_ID = Wallet.Cardano.ChainIds[CHAIN];
Expand All @@ -57,6 +56,7 @@ interface CreateSharedWalletParams {
chainId?: Wallet.Cardano.ChainId;
publicKeys: Wallet.Crypto.Bip32PublicKeyHex[];
ownSignerWalletId: WalletId;
quorumRules: QuorumOptionValue;
}

export interface CreateHardwareWallet {
Expand All @@ -71,7 +71,6 @@ type WalletManagerAddAccountProps = {
metadata: Wallet.AccountMetadata;
accountIndex: number;
passphrase?: Uint8Array;
purpose?: KeyManagement.KeyPurpose;
};

type ActivateWalletProps = Omit<WalletManagerActivateProps, 'chainId'>;
Expand Down Expand Up @@ -221,38 +220,6 @@ export const connectHardwareWallet = async (model: Wallet.HardwareWallets): Prom
const connectHardwareWalletRevamped = async (usbDevice: USBDevice): Promise<Wallet.HardwareWalletConnection> =>
Wallet.connectDeviceRevamped(usbDevice);

const deriveSharedWalletExtendedPublicKeyHash = async (
key: Crypto.Bip32PublicKeyHex,
derivationPath: KeyManagement.AccountKeyDerivationPath
): Promise<Crypto.Ed25519KeyHashHex> => {
const accountKey = Crypto.Bip32PublicKey.fromHex(key);
const paymentKey = await accountKey.derive([derivationPath.role, derivationPath.index]);
return Crypto.Ed25519KeyHashHex(await paymentKey.hash());
};

const buildSharedWalletScript = async (
expectedSigners: Array<Crypto.Bip32PublicKeyHex>,
derivationPath: KeyManagement.AccountKeyDerivationPath
) => {
const signers = [...expectedSigners].sort((key1, key2) => key1.localeCompare(key2));

const script: Cardano.NativeScript = {
__type: Cardano.ScriptType.Native,
kind: Cardano.NativeScriptKind.RequireAllOf,
scripts: []
};

for (const signer of signers) {
script.scripts.push({
__type: Cardano.ScriptType.Native,
keyHash: await deriveSharedWalletExtendedPublicKeyHash(signer, derivationPath),
kind: Cardano.NativeScriptKind.RequireSignature
});
}

return script;
};

export const useWalletManager = (): UseWalletManager => {
const {
walletLock,
Expand Down Expand Up @@ -839,7 +806,8 @@ export const useWalletManager = (): UseWalletManager => {
name,
chainId = getCurrentChainId(),
publicKeys,
ownSignerWalletId
ownSignerWalletId,
quorumRules
}: CreateSharedWalletParams): Promise<Wallet.CardanoWallet> => {
const paymentScriptKeyPath = {
index: 0,
Expand All @@ -851,8 +819,25 @@ export const useWalletManager = (): UseWalletManager => {
role: KeyManagement.KeyRole.Stake
};

const paymentScript = await buildSharedWalletScript(publicKeys, paymentScriptKeyPath);
const stakingScript = await buildSharedWalletScript(publicKeys, stakingScriptKeyPath);
const scriptKind: ScriptKind =
quorumRules.option === 'AllAddresses'
? { kind: Wallet.Cardano.NativeScriptKind.RequireAllOf }
: // eslint-disable-next-line unicorn/no-nested-ternary
quorumRules.option === 'RequireNOf'
? { kind: Wallet.Cardano.NativeScriptKind.RequireNOf, required: quorumRules.numberOfCosigner }
: { kind: Wallet.Cardano.NativeScriptKind.RequireAnyOf };

const paymentScript = await buildSharedWalletScript({
expectedSigners: publicKeys,
derivationPath: paymentScriptKeyPath,
kindInfo: scriptKind
});

const stakingScript = await buildSharedWalletScript({
expectedSigners: publicKeys,
derivationPath: stakingScriptKeyPath,
kindInfo: scriptKind
});

const createScriptWalletProps: AddWalletProps<Wallet.WalletMetadata, Wallet.AccountMetadata> = {
metadata: { name },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,8 @@ export const SharedWallet = (): JSX.Element => {
name: data.name,
chainId: DEFAULT_CHAIN_ID,
publicKeys,
ownSignerWalletId: activeWalletId
ownSignerWalletId: activeWalletId,
quorumRules: data.quorumRules
});
};

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import * as Crypto from '@cardano-sdk/crypto';
import * as KeyManagement from '@cardano-sdk/key-management';

export const deriveEd25519KeyHashFromBip32PublicKey = async (
key: Crypto.Bip32PublicKeyHex,
derivationPath: KeyManagement.AccountKeyDerivationPath
): Promise<Crypto.Ed25519KeyHashHex> => {
const accountKey = Crypto.Bip32PublicKey.fromHex(key);
const derivedKey = await accountKey.derive([derivationPath.role, derivationPath.index]);
return Crypto.Ed25519KeyHashHex(await derivedKey.hash());
};
1 change: 1 addition & 0 deletions packages/cardano/src/wallet/util/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ export * from './stake-pool-transformer';
export * from './calculate-deposit-reclaim';
export * from './drep';
export * from './voter';
export * from './derive-ed25519-key-hash-from-bip32-public-key';
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,5 @@ export { SharedWalletCreationFlow } from './creation-flow';
export type { CoSigner, QuorumOptionValue } from './creation-flow';
export { GenerateSharedKeysFlow } from './generate-keys-flow';
export type { LinkedWalletType } from './generate-keys-flow';
export { AddSharedWalletMainPageFlow } from './main-page-flow';
export { AddSharedWalletMainPageFlow, SharedWalletEntry } from './main-page-flow';
export { SharedWalletRestorationFlow } from './restore-flow';
Original file line number Diff line number Diff line change
Expand Up @@ -10,24 +10,24 @@ const meta: Meta<typeof SharedWalletEntry> = {
export default meta;
type Story = StoryObj<typeof SharedWalletEntry>;

const generateSharedKeysMock = async (): Promise<string> => {
action('keys generate')();
return Promise.resolve('mocked-key');
};

export const NoKeys: Story = {
args: {
createAndImportOptionsDisabled: true,
getSharedKeys: generateSharedKeysMock,
keysMode: 'generate',
onCreateSharedWalletClick: action('create click'),
onImportSharedWalletClick: action('import click'),
onKeysCopyClick: action('keys copy'),
onKeysGenerateClick: action('keys generate'),
},
};

export const KeysAvailable: Story = {
args: {
createAndImportOptionsDisabled: true,
createAndImportOptionsDisabled: false,
keysMode: 'copy',
onCreateSharedWalletClick: action('create click'),
onImportSharedWalletClick: action('import click'),
onKeysCopyClick: action('keys copy'),
onKeysGenerateClick: action('keys generate'),
},
};
1 change: 1 addition & 0 deletions packages/core/src/shared-wallets/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from './add-shared-wallet';
export * from './utils';
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { Wallet } from '@lace/cardano';

export type ScriptKind =
| { kind: Wallet.Cardano.NativeScriptKind.RequireAllOf }
| { kind: Wallet.Cardano.NativeScriptKind.RequireAnyOf }
| { kind: Wallet.Cardano.NativeScriptKind.RequireNOf; required: number };

type SharedWalletScriptParams = {
derivationPath: Wallet.KeyManagement.AccountKeyDerivationPath;
expectedSigners: Array<Wallet.Crypto.Bip32PublicKeyHex>;
kindInfo: ScriptKind;
};

type ScriptType =
| Wallet.Cardano.RequireAllOfScript
| Wallet.Cardano.RequireAtLeastScript
| Wallet.Cardano.RequireAnyOfScript;

export const buildSharedWalletScript = async ({
expectedSigners,
derivationPath,
kindInfo,
}: SharedWalletScriptParams): Promise<ScriptType> => {
const signers = [...expectedSigners].sort((key1, key2) => key1.localeCompare(key2));

const scripts: Wallet.Cardano.NativeScript[] = [];

for (const signer of signers) {
scripts.push({
__type: Wallet.Cardano.ScriptType.Native,
keyHash: await Wallet.util.deriveEd25519KeyHashFromBip32PublicKey(signer, derivationPath),
kind: Wallet.Cardano.NativeScriptKind.RequireSignature,
});
}

return {
__type: Wallet.Cardano.ScriptType.Native,
scripts,
...kindInfo,
};
};
1 change: 1 addition & 0 deletions packages/core/src/shared-wallets/utils/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './build-shared-wallet-script';

0 comments on commit 166cc52

Please sign in to comment.