Skip to content

Commit

Permalink
Merge pull request #2096 from Emurgo/yushi/catalyst-ledger
Browse files Browse the repository at this point in the history
Ledger wallet Catalyst voting registration support
  • Loading branch information
vsubhuman committed Jun 16, 2021
2 parents 44a3f68 + 8f3b59b commit d6efe27
Show file tree
Hide file tree
Showing 16 changed files with 316 additions and 83 deletions.
44 changes: 32 additions & 12 deletions packages/yoroi-extension/app/api/ada/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,7 @@ import { MultiToken } from '../common/lib/MultiToken';
import type { DefaultTokenEntry } from '../common/lib/MultiToken';
import { hasSendAllDefault, builtSendTokenList } from '../common/index';
import { getReceiveAddress } from '../../stores/stateless/addressStores';
import { generateRegistrationMetadata } from './lib/cardanoCrypto/catalyst';

// ADA specific Request / Response params

Expand Down Expand Up @@ -382,12 +383,25 @@ type CreateVotingRegTxRequestCommon = {|
export type CreateVotingRegTxRequest = {|
...CreateVotingRegTxRequestCommon,
normalWallet: {|
metadata: RustModule.WalletV4.GeneralTransactionMetadata,
metadata: RustModule.WalletV4.TransactionMetadata,
|}
|} | {|
...CreateVotingRegTxRequestCommon,
trezorTWallet: {|
votingPublicKey: string,
stakingKeyPath: Array<number>,
stakingKey: string,
rewardAddress: string,
nonce: number,
|}
|} | {|
...CreateVotingRegTxRequestCommon,
ledgerNanoWallet: {|
votingPublicKey: string,
stakingKeyPath: Array<number>,
stakingKey: string,
rewardAddress: string,
nonce: number,
|}
|};

Expand Down Expand Up @@ -1412,13 +1426,22 @@ export default class AdaApi {
throw new Error(`${nameof(this.createVotingRegTx)} no internal addresses left. Should never happen`);
}
let trxMetadata;
if (request.trezorTWallet) {
trxMetadata = undefined;
if (request.trezorTWallet || request.ledgerNanoWallet) {
// Pass a placeholder metadata so that the tx fee is correctly
// calculated.
const hwWallet = request.trezorTWallet || request.ledgerNanoWallet;
trxMetadata = generateRegistrationMetadata(
hwWallet.votingPublicKey,
hwWallet.stakingKey,
hwWallet.rewardAddress,
hwWallet.nonce,
(_hashedMetadata) => {
return '0'.repeat(64 * 2)
},
);
} else {
// Mnemonic wallet
trxMetadata = RustModule.WalletV4.TransactionMetadata.new(
request.normalWallet.metadata
);
trxMetadata = request.normalWallet.metadata;
}

const unsignedTx = shelleyNewAdaUnsignedTx(
Expand Down Expand Up @@ -1452,12 +1475,9 @@ export default class AdaApi {
wits: new Set(),
},
trezorTCatalystRegistrationTxSignData:
request.trezorTWallet ?
{
votingPublicKey: request.trezorTWallet.votingPublicKey,
nonce: request.absSlotNumber,
} :
undefined,
request.trezorTWallet ? request.trezorTWallet : undefined,
ledgerNanoCatalystRegistrationTxSignData:
request.ledgerNanoWallet ? request.ledgerNanoWallet: undefined,
});
} catch (error) {
Logger.error(`${nameof(AdaApi)}::${nameof(this.createVotingRegTx)} error: ` + stringifyError(error));
Expand Down
66 changes: 51 additions & 15 deletions packages/yoroi-extension/app/api/ada/lib/cardanoCrypto/catalyst.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,21 @@ export const CatalystLabels = Object.freeze({
DATA: 61284,
SIG: 61285,
});
export function generateRegistration(request: {|
stakePrivateKey: RustModule.WalletV4.PrivateKey,
catalystPrivateKey: RustModule.WalletV4.PrivateKey,
receiverAddress: Buffer,
slotNumber: number,
|}): RustModule.WalletV4.GeneralTransactionMetadata {

function prefix0x(hex: string): string {
if (hex.startsWith('0x')) {
return hex;
}
return '0x' + hex;
}

export function generateRegistrationMetadata(
votingPublicKey: string,
stakingPublicKey: string,
rewardAddress: string,
nonce: number,
signer: Uint8Array => string,
): RustModule.WalletV4.TransactionMetadata {

/**
* Catalyst follows a certain standard to prove the voting power
Expand All @@ -32,10 +41,10 @@ export function generateRegistration(request: {|

const registrationData = RustModule.WalletV4.encode_json_str_to_metadatum(
JSON.stringify({
'1': `0x${Buffer.from(request.catalystPrivateKey.to_public().as_bytes()).toString('hex')}`,
'2': `0x${Buffer.from(request.stakePrivateKey.to_public().as_bytes()).toString('hex')}`,
'3': `0x${Buffer.from(request.receiverAddress).toString('hex')}`,
'4': request.slotNumber,
'1': prefix0x(votingPublicKey),
'2': prefix0x(stakingPublicKey),
'3': prefix0x(rewardAddress),
'4': nonce,
}),
RustModule.WalletV4.MetadataJsonSchema.BasicConversions
);
Expand All @@ -48,19 +57,46 @@ export function generateRegistration(request: {|
const hashedMetadata = blake2b(256 / 8).update(
generalMetadata.to_bytes()
).digest('binary');
const catalystSignature = request.stakePrivateKey
.sign(hashedMetadata)
.to_hex();

generalMetadata.insert(
RustModule.WalletV4.BigNum.from_str(CatalystLabels.SIG.toString()),
RustModule.WalletV4.encode_json_str_to_metadatum(
JSON.stringify({
'1': `0x${catalystSignature}`,
'1': prefix0x(signer(hashedMetadata)),
}),
RustModule.WalletV4.MetadataJsonSchema.BasicConversions
)
);

return generalMetadata;
// This is how Ledger constructs the metadata. We must be consistent with it.
const metadataList = RustModule.WalletV4.MetadataList.new();
metadataList.add(
RustModule.WalletV4.TransactionMetadatum.from_bytes(
generalMetadata.to_bytes()
)
);
metadataList.add(
RustModule.WalletV4.TransactionMetadatum.new_list(
RustModule.WalletV4.MetadataList.new()
)
);

return RustModule.WalletV4.TransactionMetadata.from_bytes(
metadataList.to_bytes()
);
}

export function generateRegistration(request: {|
stakePrivateKey: RustModule.WalletV4.PrivateKey,
catalystPrivateKey: RustModule.WalletV4.PrivateKey,
receiverAddress: Buffer,
slotNumber: number,
|}): RustModule.WalletV4.TransactionMetadata {
return generateRegistrationMetadata(
Buffer.from(request.catalystPrivateKey.to_public().as_bytes()).toString('hex'),
Buffer.from(request.stakePrivateKey.to_public().as_bytes()).toString('hex'),
Buffer.from(request.receiverAddress).toString('hex'),
request.slotNumber,
(hashedMetadata) => request.stakePrivateKey.sign(hashedMetadata).to_hex(),
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,6 @@ beforeAll(async () => {
});

test('Generate Catalyst registration tx', async () => {
// const paymentKey = RustModule.WalletV4.PublicKey.from_bytes(
// Buffer.from('3273a5316e4de228863bd7cf8dac90d57149e1a595f3dd131073b84e35546676', 'hex')
// );
const stakePrivateKey = RustModule.WalletV4.PrivateKey.from_normal_bytes(
Buffer.from('f5beaeff7932a4164d270afde7716067582412e8977e67986cd9b456fc082e3a', 'hex')
);
Expand All @@ -26,12 +23,15 @@ test('Generate Catalyst registration tx', async () => {
);

const nonce = 1234;
const result = generateRegistration({
const metadata = generateRegistration({
stakePrivateKey,
catalystPrivateKey,
receiverAddress: Buffer.from(address.to_address().to_bytes()),
slotNumber: nonce,
});
const result = RustModule.WalletV4.GeneralTransactionMetadata.from_bytes(
RustModule.WalletV4.MetadataList.from_bytes(metadata.to_bytes()).get(0).to_bytes()
);

const data = result.get(RustModule.WalletV4.BigNum.from_str(CatalystLabels.DATA.toString()));
if (data == null) throw new Error('Should never happen');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,17 @@ type NetworkSettingSnapshot = {|
+KeyDeposit: BigNumber,
|};

type LedgerNanoCatalystRegistrationTxSignData = {|
votingPublicKey: string,
stakingKeyPath: Array<number>,
stakingKey: string,
rewardAddress: string,
nonce: number,
|};

type TrezorTCatalystRegistrationTxSignData =
LedgerNanoCatalystRegistrationTxSignData;

export class HaskellShelleyTxSignRequest
implements ISignRequest<RustModule.WalletV4.TransactionBuilder> {

Expand All @@ -45,10 +56,10 @@ implements ISignRequest<RustModule.WalletV4.TransactionBuilder> {
neededHashes: Set<string>, // StakeCredential
wits: Set<string>, // Vkeywitness
|};
trezorTCatalystRegistrationTxSignData: void | {|
votingPublicKey: string,
nonce: BigNumber,
|};
trezorTCatalystRegistrationTxSignData:
void | TrezorTCatalystRegistrationTxSignData;
ledgerNanoCatalystRegistrationTxSignData:
void | LedgerNanoCatalystRegistrationTxSignData;

constructor(data: {|
senderUtxos: Array<CardanoAddressedUtxo>,
Expand All @@ -60,10 +71,10 @@ implements ISignRequest<RustModule.WalletV4.TransactionBuilder> {
neededHashes: Set<string>, // StakeCredential
wits: Set<string>, // Vkeywitness
|},
trezorTCatalystRegistrationTxSignData?: void | {|
votingPublicKey: string,
nonce: BigNumber,
|};
trezorTCatalystRegistrationTxSignData?:
void | TrezorTCatalystRegistrationTxSignData;
ledgerNanoCatalystRegistrationTxSignData?:
void | LedgerNanoCatalystRegistrationTxSignData;
|}) {
this.senderUtxos = data.senderUtxos;
this.unsignedTx = data.unsignedTx;
Expand All @@ -73,6 +84,8 @@ implements ISignRequest<RustModule.WalletV4.TransactionBuilder> {
this.neededStakingKeyHashes = data.neededStakingKeyHashes;
this.trezorTCatalystRegistrationTxSignData =
data.trezorTCatalystRegistrationTxSignData;
this.ledgerNanoCatalystRegistrationTxSignData =
data.ledgerNanoCatalystRegistrationTxSignData;
}

txId(): string {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,13 @@ import type {
Address, Value, Addressing,
} from '../../lib/storage/models/PublicDeriver/interfaces';
import { HaskellShelleyTxSignRequest } from './HaskellShelleyTxSignRequest';
import { AddressType, CertificateType, TransactionSigningMode, TxOutputDestinationType, } from '@cardano-foundation/ledgerjs-hw-app-cardano';
import {
AddressType,
CertificateType,
TransactionSigningMode,
TxOutputDestinationType,
TxAuxiliaryDataType,
} from '@cardano-foundation/ledgerjs-hw-app-cardano';
import { RustModule } from '../../lib/cardanoCrypto/rustLoader';
import { toHexOrBase58 } from '../../lib/storage/bridge/utils';
import {
Expand Down Expand Up @@ -75,6 +81,28 @@ export async function createLedgerSignTxPayload(request: {|
}
const ttl = txBody.ttl();
let auxiliaryData = undefined;
if (request.signRequest.ledgerNanoCatalystRegistrationTxSignData) {
const { votingPublicKey, stakingKeyPath, nonce } =
request.signRequest.ledgerNanoCatalystRegistrationTxSignData;
auxiliaryData = {
type: TxAuxiliaryDataType.CATALYST_REGISTRATION,
params: {
votingPublicKeyHex: votingPublicKey.replace(/^0x/, ''),
stakingPath: stakingKeyPath,
rewardsDestination: {
type: AddressType.REWARD,
params: {
stakingPath: stakingKeyPath,
},
},
nonce,
}
};
}
return {
signingMode: TransactionSigningMode.ORDINARY_TRANSACTION,
tx: {
Expand All @@ -88,7 +116,7 @@ export async function createLedgerSignTxPayload(request: {|
},
withdrawals: ledgerWithdrawal.length === 0 ? null : ledgerWithdrawal,
certificates: ledgerCertificates.length === 0 ? null : ledgerCertificates,
auxiliaryData: undefined,
auxiliaryData,
validityIntervalStart: undefined,
}
};
Expand Down Expand Up @@ -358,7 +386,7 @@ export function toLedgerAddressParameters(request: {|
return {
type: AddressType.REWARD,
params: {
spendingPath: request.path, // reward addresses use spending path
stakingPath: request.path, // reward addresses use spending path
},
};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ test('Generate address parameters', async () => {
})).toEqual(({
type: AddressType.REWARD,
params: {
spendingPath: stakingKeyPath,
stakingPath: stakingKeyPath,
}
}: DeviceOwnedAddress));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ import Voting from '../../../components/wallet/voting/Voting';
import VotingRegistrationDialogContainer from '../dialogs/voting/VotingRegistrationDialogContainer';
import type { GeneratedData as VotingRegistrationDialogContainerData } from '../dialogs/voting/VotingRegistrationDialogContainer';
import { handleExternalLinkClick } from '../../../utils/routing';
import { WalletTypeOption, } from '../../../api/ada/lib/storage/models/ConceptualWallet/interfaces';
import {
isTrezorTWallet,
} from '../../../api/ada/lib/storage/models/ConceptualWallet/index';
import UnsupportedWallet from '../UnsupportedWallet';
import { PublicDeriver } from '../../../api/ada/lib/storage/models/PublicDeriver/index';
import LoadingSpinner from '../../../components/widgets/LoadingSpinner';
Expand Down Expand Up @@ -88,7 +90,8 @@ export default class VotingPage extends Component<Props> {
if(selected == null){
throw new Error(`${nameof(VotingPage)} no wallet selected`);
}
if (selected.getParent().getWalletType() === WalletTypeOption.HARDWARE_WALLET) {

if (isTrezorTWallet(selected.getParent())) {
return <UnsupportedWallet />;
}

Expand Down
9 changes: 9 additions & 0 deletions packages/yoroi-extension/app/domain/LedgerLocalizedError.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ export const ledgerErrors: * = defineMessages({
id: 'wallet.hw.ledger.common.error.105',
defaultMessage: '!!!Network error. Please check your internet connection.',
},
cip15NotSupportedError106: {
id: 'wallet.hw.ledger.catalyst.unsupported.106',
defaultMessage: '!!!Please upgrade your Ledger firmware version to at least 2.0.0 and Caradano app version to 2.3.2 or above.',
},
});

export function convertToLocalizableError(error: Error): LocalizableError {
Expand Down Expand Up @@ -90,6 +94,11 @@ export function convertToLocalizableError(error: Error): LocalizableError {
// Showing - Network error. Please check your internet connection.
localizableError = new LocalizableError(ledgerErrors.networkError105);
break;
case 'catalyst registration not supported':
localizableError = new LocalizableError(
ledgerErrors.cip15NotSupportedError106
);
break;
default:
/** we are not able to figure out why Error is thrown
* make it, Something unexpected happened */
Expand Down
1 change: 1 addition & 0 deletions packages/yoroi-extension/app/i18n/locales/en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -537,6 +537,7 @@
"wallet.hw.common.error.101": "Necessary permissions were not granted by the user. Please retry.",
"wallet.hw.incorrectDevice": "Incorrect device detected. Expected device {expectedDeviceId}, but got device {responseDeviceId}. Please plug in the correct device",
"wallet.hw.incorrectVersion": "Incorrect device version detected. We support version {supportedVersions} but you have version {responseVersion}.",
"wallet.hw.ledger.catalyst.unsupported.106": "Please upgrade your Ledger firmware version to at least 2.0.0 and Caradano app version to 2.3.2 or above.",
"wallet.hw.ledger.common.error.101": "Operation cancelled on Ledger device.",
"wallet.hw.ledger.common.error.102": "Operation cancelled by user.",
"wallet.hw.ledger.common.error.103": "Ledger device is locked, please unlock it and retry.",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ export default class HWVerifyAddressStore extends Store<StoresMap, ActionsMap> {
try {
this.ledgerConnect = new LedgerConnect({
locale: this.stores.profile.currentLocale,
connectorUrl: 'https://emurgo.github.io/yoroi-extension-ledger-connect-vnext/#/v3',
connectorUrl: 'https://emurgo.github.io/yoroi-extension-ledger-connect-vnext/catalyst/#/v3.1',
});
await prepareLedgerConnect(this.ledgerConnect);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ export default class LedgerConnectStore
try {
const ledgerConnect = new LedgerConnect({
locale: this.stores.profile.currentLocale,
connectorUrl: 'https://emurgo.github.io/yoroi-extension-ledger-connect-vnext/#/v3',
connectorUrl: 'https://emurgo.github.io/yoroi-extension-ledger-connect-vnext/catalyst/#/v3.1',
});
this.ledgerConnect = ledgerConnect;
await prepareLedgerConnect(ledgerConnect);
Expand Down

0 comments on commit d6efe27

Please sign in to comment.