Skip to content

Commit

Permalink
feat: add another ledger solana validator (#8563)
Browse files Browse the repository at this point in the history
* feat: add ledger solana validator

* add changeset

* remove unnecessary code

* small fic

* fix tests
  • Loading branch information
Canestin authored Dec 2, 2024
1 parent 0d7115c commit 93f8f78
Show file tree
Hide file tree
Showing 13 changed files with 127 additions and 66 deletions.
7 changes: 7 additions & 0 deletions .changeset/clever-impalas-explain.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@ledgerhq/coin-solana": minor
"ledger-live-desktop": minor
"@ledgerhq/live-common": minor
---

add another ledger solana validator
1 change: 1 addition & 0 deletions apps/ledger-live-desktop/src/config/urls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,7 @@ export const urls = {
solana: {
staking: "https://support.ledger.com/article/4731749170461-zd",
recipient_info: "https://support.ledger.com",
ledgerByChorusOneTC: "https://chorus.one/tos",
ledgerByFigmentTC:
"https://cdn.figment.io/legal/Current%20Ledger_Online%20Staking%20Delgation%20Services%20Agreement.pdf",
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import Box from "~/renderer/components/Box";
import Button from "~/renderer/components/Button";
import ErrorBanner from "~/renderer/components/ErrorBanner";
import ErrorDisplay from "../../shared/components/ErrorDisplay";
import LedgerByFigmentTC from "../../shared/components/LedgerByFigmentTCLink";
import LedgerValidatorTCLink from "../../shared/components/LedgerValidatorTCLink";
import ValidatorsField from "../../shared/fields/ValidatorsField";
import { StepProps } from "../types";
export default function StepValidator({
Expand Down Expand Up @@ -72,7 +72,8 @@ export function StepValidatorFooter({
if (!transaction) return null;
return (
<>
<LedgerByFigmentTC transaction={transaction} />
<LedgerValidatorTCLink transaction={transaction} />

<Box horizontal>
<Button mr={1} secondary onClick={onClose}>
<Trans i18nKey="common.cancel" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import TrackPage from "~/renderer/analytics/TrackPage";
import Box from "~/renderer/components/Box";
import Button from "~/renderer/components/Button";
import ErrorBanner from "~/renderer/components/ErrorBanner";
import LedgerByFigmentTC from "../../shared/components/LedgerByFigmentTCLink";
import LedgerValidatorTCLink from "../../shared/components/LedgerValidatorTCLink";
import ValidatorsField from "../../shared/fields/ValidatorsField";
import { StepProps } from "../types";

Expand Down Expand Up @@ -78,7 +78,8 @@ export function StepValidatorFooter({
if (!transaction) return null;
return (
<>
<LedgerByFigmentTC transaction={transaction} />
<LedgerValidatorTCLink transaction={transaction} />

<Box horizontal>
<Button mr={1} secondary onClick={onClose}>
<Trans i18nKey="common.cancel" />
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import React from "react";
import LinkWithExternalIcon from "~/renderer/components/LinkWithExternalIcon";
import { useTranslation } from "react-i18next";
import { urls } from "~/config/urls";
import { openURL } from "~/renderer/linking";
import {
LEDGER_VALIDATORS_VOTE_ACCOUNTS,
LEDGER_VALIDATOR_BY_FIGMENT,
LEDGER_VALIDATOR_BY_CHORUS_ONE,
} from "@ledgerhq/live-common/families/solana/staking";

import { Transaction } from "@ledgerhq/live-common/families/solana/types";
type Props = {
transaction: Transaction;
};
export default function LedgerValidatorTCLink({ transaction }: Props) {
const { t } = useTranslation();
if (!shouldShowTC(transaction)) return null;

const data = getTCInfo(transaction);
if (!data) return null;

const openLedgerValidatorTC = () => openURL(data.url);
return <LinkWithExternalIcon label={t(data.label)} onClick={openLedgerValidatorTC} />;
}
const shouldShowTC = ({ model }: Transaction) => {
switch (model.kind) {
case "stake.createAccount":
return LEDGER_VALIDATORS_VOTE_ACCOUNTS.includes(model.uiState.delegate.voteAccAddress);
case "stake.delegate":
return LEDGER_VALIDATORS_VOTE_ACCOUNTS.includes(model.uiState.voteAccAddr);
default:
break;
}
return false;
};

const getTCInfo = ({ model }: Transaction) => {
const TC_INFO: Record<string, { label: string; url: string }> = {
[LEDGER_VALIDATOR_BY_CHORUS_ONE.voteAccount]: {
label: "solana.delegation.ledgerByChorusOneTC",
url: urls.solana.ledgerByChorusOneTC,
},
[LEDGER_VALIDATOR_BY_FIGMENT.voteAccount]: {
label: "solana.delegation.ledgerByFigmentTC",
url: urls.solana.ledgerByFigmentTC,
},
};

switch (model.kind) {
case "stake.createAccount":
return TC_INFO[model.uiState.delegate.voteAccAddress];
case "stake.delegate":
return TC_INFO[model.uiState.voteAccAddr];
default:
break;
}
return null;
};
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { useValidators } from "@ledgerhq/live-common/families/solana/react";
import { ValidatorsAppValidator } from "@ledgerhq/live-common/families/solana/staking";
import { SolanaAccount } from "@ledgerhq/live-common/families/solana/types";

import React, { useMemo, useState, useCallback } from "react";
import React, { useState, useCallback } from "react";
import { Trans } from "react-i18next";
import styled from "styled-components";
import Box from "~/renderer/components/Box";
Expand All @@ -25,16 +25,12 @@ const ValidatorField = ({ account, onChangeValidator, chosenVoteAccAddr }: Props
const [showAll, setShowAll] = useState(false);
const unit = useAccountUnit(account);
const validators = useValidators(account.currency, search);
const chosenValidator = useMemo(() => {
if (chosenVoteAccAddr !== null) {
return validators.find(v => v.voteAccount === chosenVoteAccAddr);
}
}, [validators, chosenVoteAccAddr]);

const onSearch = useCallback(
(evt: React.ChangeEvent<HTMLInputElement>) => setSearch(evt.target.value),
[setSearch],
);

const renderItem = (validator: ValidatorsAppValidator) => {
return (
<ValidatorRow
Expand All @@ -53,9 +49,9 @@ const ValidatorField = ({ account, onChangeValidator, chosenVoteAccAddr }: Props
<ValidatorsFieldContainer>
<Box p={1} data-testid="validator-list">
<ScrollLoadingList
data={showAll ? validators : [chosenValidator ?? validators[0]]}
data={showAll ? validators : validators.slice(0, 2)}
style={{
flex: showAll ? "1 0 240px" : "1 0 63px",
flex: showAll ? "1 0 240px" : "1 0 126px",
marginBottom: 0,
paddingLeft: 0,
}}
Expand Down
3 changes: 3 additions & 0 deletions apps/ledger-live-desktop/static/i18n/en/app.json
Original file line number Diff line number Diff line change
Expand Up @@ -3045,6 +3045,7 @@
"validatorGroup": {
"title": "Validator Group",
"ledgerByFigmentTC": "Ledger by Figment T&Cs",
"ledgerByChorusOneTC": "Ledger by Chorus One T&Cs",
"totalVotes": "Total Votes",
"commission": "Commission"
}
Expand Down Expand Up @@ -3642,6 +3643,7 @@
"withdrawableTitle": "Withdrawable",
"statusUpdateNotice": "The status of the delegation will be updated when the transaction is confirmed.",
"ledgerByFigmentTC": "Ledger by Figment T&Cs",
"ledgerByChorusOneTC": "Ledger by Chorus One T&Cs",
"emptyState": {
"description": "You can earn SOL rewards by delegating your assets.",
"info": "How Delegation works",
Expand Down Expand Up @@ -3900,6 +3902,7 @@
},
"validator": {
"title": "Validators",
"ledgerByChorusOneTC": "Ledger by Chorus One T&Cs",
"ledgerByFigmentTC": "Ledger by Figment T&Cs"
},
"connectDevice": {
Expand Down
4 changes: 2 additions & 2 deletions libs/coin-modules/coin-solana/src/tx-fees.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { ChainAPI } from "./api";
import { buildTransactionWithAPI } from "./buildTransaction";
import createTransaction from "./createTransaction";
import { Transaction, TransactionModel } from "./types";
import { LEDGER_VALIDATOR, assertUnreachable } from "./utils";
import { LEDGER_VALIDATOR_DEFAULT, assertUnreachable } from "./utils";
import { VersionedTransaction as OnChainTransaction } from "@solana/web3.js";
import { log } from "@ledgerhq/logs";
import { getStakeAccountAddressWithSeed } from "./api/chain/web3";
Expand Down Expand Up @@ -88,7 +88,7 @@ const createDummyStakeCreateAccountTx = async (address: string): Promise<Transac
kind: "stake.createAccount",
amount: 0,
delegate: {
voteAccAddress: LEDGER_VALIDATOR.voteAccount,
voteAccAddress: LEDGER_VALIDATOR_DEFAULT.voteAccount,
},
fromAccAddress: address,
seed: "",
Expand Down
40 changes: 30 additions & 10 deletions libs/coin-modules/coin-solana/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,39 @@ import { getEnv } from "@ledgerhq/live-env";
import { ValidatorsAppValidator } from "./validator-app";
import BigNumber from "bignumber.js";

// Hardcoding the Ledger validator info as backup,
// Hardcoding the Ledger validators info as backup,
// because backend is flaky and sometimes doesn't return it anymore
export const LEDGER_VALIDATOR: ValidatorsAppValidator = {
export const LEDGER_VALIDATOR_BY_FIGMENT: ValidatorsAppValidator = {
voteAccount: "26pV97Ce83ZQ6Kz9XT4td8tdoUFPTng8Fb8gPyc53dJx",
name: "Ledger by Figment",
avatarUrl:
"https://s3.amazonaws.com/keybase_processed_uploads/3c47b62f3d28ecfd821536f69be82905_360_360.jpg",
wwwUrl: "https://www.ledger.com/staking",
activeStake: 4784119000000000,
activeStake: 9079057178046828,
commission: 7,
totalScore: 6,
};

export const LEDGER_VALIDATOR_BY_CHORUS_ONE: ValidatorsAppValidator = {
voteAccount: "CpfvLiiPALdzZTP3fUrALg2TXwEDSAknRh1sn5JCt9Sr",
name: "Ledger by Chorus One",
avatarUrl:
"https://s3.amazonaws.com/keybase_processed_uploads/3c47b62f3d28ecfd821536f69be82905_360_360.jpg",
wwwUrl: "https://www.ledger.com/staking",
activeStake: 0,
commission: 7,
totalScore: 6,
};

export const LEDGER_VALIDATOR_LIST: ValidatorsAppValidator[] = [
LEDGER_VALIDATOR_BY_CHORUS_ONE,
LEDGER_VALIDATOR_BY_FIGMENT,
];

export const LEDGER_VALIDATOR_DEFAULT = LEDGER_VALIDATOR_BY_FIGMENT;

export const LEDGER_VALIDATORS_VOTE_ACCOUNTS = LEDGER_VALIDATOR_LIST.map(v => v.voteAccount);

export const SOLANA_DELEGATION_RESERVE = 0.01;

export const assertUnreachable = (_: never): never => {
Expand Down Expand Up @@ -75,7 +95,7 @@ export function clusterByCurrencyId(currencyId: string): Cluster {

export function defaultVoteAccAddrByCurrencyId(currencyId: string): string | undefined {
const voteAccAddrs: Record<string, string | undefined> = {
solana: LEDGER_VALIDATOR.voteAccount,
solana: undefined,
solana_devnet: undefined,
solana_testnet: undefined,
};
Expand Down Expand Up @@ -147,17 +167,17 @@ export type Functions<T> = keyof {
[K in keyof T as T[K] extends Function ? K : never]: T[K];
};

// move Ledger validator to the first position
// move Ledger validators to the first positions
export function ledgerFirstValidators(
validators: ValidatorsAppValidator[],
): ValidatorsAppValidator[] {
const [ledgerValidator, restValidators] = partition(
v => v.voteAccount === LEDGER_VALIDATOR.voteAccount,
const [ledgerValidators, restValidators] = partition(
v => LEDGER_VALIDATORS_VOTE_ACCOUNTS.includes(v.voteAccount),
validators,
);
return ledgerValidator.length
? ledgerValidator.concat(restValidators)
: [LEDGER_VALIDATOR].concat(restValidators);
return ledgerValidators.length === 2
? ledgerValidators.concat(restValidators)
: LEDGER_VALIDATOR_LIST.concat(restValidators);
}

export function profitableValidators(validators: ValidatorsAppValidator[]) {
Expand Down
11 changes: 6 additions & 5 deletions libs/ledger-live-common/src/families/solana/banner.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { getCurrentSolanaPreloadData } from "@ledgerhq/coin-solana/preload-data";
import { stakeActions } from "@ledgerhq/coin-solana/logic";
import { LEDGER_VALIDATOR } from "@ledgerhq/coin-solana/utils";
import { LEDGER_VALIDATORS_VOTE_ACCOUNTS } from "@ledgerhq/coin-solana/utils";
import type { SolanaAccount } from "@ledgerhq/coin-solana/types";
import { ValidatorsAppValidator } from "@ledgerhq/coin-solana/validator-app/index";
import { isAccountEmpty } from "../../account";
Expand All @@ -23,8 +23,9 @@ export function getAccountBannerState(account: SolanaAccount): AccountBannerStat
const { validators } = getCurrentSolanaPreloadData(account.currency) ?? {
validators: [],
};
const ledgerValidator = validators.find(
validator => validator.voteAccount === LEDGER_VALIDATOR.voteAccount,

const ledgerValidator = validators.find(validator =>
LEDGER_VALIDATORS_VOTE_ACCOUNTS.includes(validator.voteAccount),
);

// If Ledger doesn't provide validator, we don't display banner
Expand All @@ -49,7 +50,7 @@ export function getAccountBannerState(account: SolanaAccount): AccountBannerStat
const actions = stakeActions(delegation);
const isValidRedelegation =
validator &&
validatorAdress !== ledgerValidator.voteAccount &&
!LEDGER_VALIDATORS_VOTE_ACCOUNTS.includes(validatorAdress) &&
worstValidator.commission <= validator.commission &&
actions.includes("deactivate");
if (isValidRedelegation) {
Expand All @@ -58,7 +59,7 @@ export function getAccountBannerState(account: SolanaAccount): AccountBannerStat
}
}
if (worstValidator) {
if (worstValidator?.voteAccount === ledgerValidator?.voteAccount) {
if (LEDGER_VALIDATORS_VOTE_ACCOUNTS.includes(worstValidator?.voteAccount)) {
if (!isAccountEmpty(account)) {
display = true;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { setEnv } from "@ledgerhq/live-env";
import type { Account, CurrencyBridge } from "@ledgerhq/types-live";
import type { Transaction } from "@ledgerhq/coin-solana/types";
import { getCurrentSolanaPreloadData } from "@ledgerhq/coin-solana/preload-data";
import { LEDGER_VALIDATOR } from "@ledgerhq/coin-solana/utils";
import { LEDGER_VALIDATOR_DEFAULT } from "@ledgerhq/coin-solana/utils";
import { getAccountBridge, getCurrencyBridge } from "../../bridge";
import { getCryptoCurrencyById } from "../../currencies";
import { makeBridgeCacheSystem } from "../../bridge/cache";
Expand Down Expand Up @@ -44,7 +44,9 @@ describe("solana/react", () => {
const { result } = renderHook(() => hooks.useValidators(account.currency, "Ledger"));

expect(
result.current.some(validator => validator.voteAccount === LEDGER_VALIDATOR.voteAccount),
result.current.some(
validator => validator.voteAccount === LEDGER_VALIDATOR_DEFAULT.voteAccount,
),
).toBe(true);
});
});
Expand Down
6 changes: 5 additions & 1 deletion libs/ledger-live-common/src/families/solana/staking.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
export {
defaultVoteAccAddrByCurrencyId,
SOLANA_DELEGATION_RESERVE,
LEDGER_VALIDATOR,
LEDGER_VALIDATOR_DEFAULT,
LEDGER_VALIDATORS_VOTE_ACCOUNTS,
LEDGER_VALIDATOR_BY_FIGMENT,
LEDGER_VALIDATOR_BY_CHORUS_ONE,
LEDGER_VALIDATOR_LIST,
assertUnreachable,
sweetch,
tupleOfUnion,
Expand Down

0 comments on commit 93f8f78

Please sign in to comment.