Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ledger wallet Catalyst voting registration support #2096

Merged
merged 10 commits into from
Jun 16, 2021
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
Loading