Skip to content

Commit

Permalink
Merge remote-tracking branch 'hoodieshq/feat/solana-safe-staking' int…
Browse files Browse the repository at this point in the history
…o feat/safer-solana
  • Loading branch information
hedi-edelbloute committed Dec 22, 2023
2 parents 973275e + a9b409f commit e1fb9bf
Show file tree
Hide file tree
Showing 18 changed files with 201 additions and 113 deletions.
6 changes: 6 additions & 0 deletions .changeset/tricky-needles-allow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"ledger-live-desktop": patch
"@ledgerhq/live-common": patch
---

Safer Solana staking
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { getMainAccount } from "@ledgerhq/live-common/account/index";
import { SOLANA_DELEGATION_RESERVE } from "@ledgerhq/live-common/families/solana/utils";
import React, { Fragment, PureComponent } from "react";
import { Trans } from "react-i18next";
import TrackPage from "~/renderer/analytics/TrackPage";
Expand All @@ -7,9 +8,11 @@ import Button from "~/renderer/components/Button";
import CurrencyDownStatusAlert from "~/renderer/components/CurrencyDownStatusAlert";
import ErrorBanner from "~/renderer/components/ErrorBanner";
import SpendableBanner from "~/renderer/components/SpendableBanner";
import Alert from "~/renderer/components/Alert";
import AccountFooter from "~/renderer/modals/Send/AccountFooter";
import AmountField from "~/renderer/modals/Send/fields/AmountField";
import { StepProps } from "../types";

const StepAmount = ({
t,
account,
Expand Down Expand Up @@ -52,6 +55,14 @@ const StepAmount = ({
t={t}
withUseMaxLabel={true}
/>
<Alert type="warning" small>
<Trans
i18nKey="solana.delegation.flow.steps.amount.reserveWarning"
values={{
amount: SOLANA_DELEGATION_RESERVE,
}}
/>
</Alert>
</Fragment>
)}
</Box>
Expand Down
3 changes: 2 additions & 1 deletion apps/ledger-live-desktop/static/i18n/en/app.json
Original file line number Diff line number Diff line change
Expand Up @@ -3537,7 +3537,8 @@
"title": "Validator"
},
"amount": {
"title": "Amount"
"title": "Amount",
"reserveWarning": "Please ensure you reserve at least {{amount}} SOL in your wallet to cover future network fees to deactivate and withdraw your stake"
},
"confirmation": {
"success": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { getAccountUnit } from "@ledgerhq/live-common/account/index";
import { getAccountCurrency } from "@ledgerhq/live-common/account/helpers";
import { getAccountBridge } from "@ledgerhq/live-common/bridge/index";
import useBridgeTransaction from "@ledgerhq/live-common/bridge/useBridgeTransaction";
import { SOLANA_DELEGATION_RESERVE } from "@ledgerhq/live-common/families/solana/utils";
import { useTheme } from "@react-navigation/native";
import { BigNumber } from "bignumber.js";
import invariant from "invariant";
Expand Down Expand Up @@ -181,6 +182,17 @@ export default function DelegationSelectAmount({ navigation, route }: Props) {
/>
</View>
</View>
<View>
<Text color="grey">
<InfoIcon size={12} color="grey" />{" "}
<Trans
i18nKey="solana.delegation.reserveWarning"
values={{
amount: SOLANA_DELEGATION_RESERVE,
}}
/>
</Text>
</View>
<View style={styles.continueWrapper}>
<Button
event="SendAmountCoinContinue"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import DelegatingContainer from "../../tezos/DelegatingContainer";
import ValidatorImage from "../shared/ValidatorImage";
import { StackNavigatorProps } from "~/components/RootNavigator/types/helpers";
import { DelegationAction, SolanaDelegationFlowParamList } from "./types";
import TranslatedError from "../../../components/TranslatedError";

type Props = StackNavigatorProps<SolanaDelegationFlowParamList, ScreenName.DelegationSummary>;

Expand Down Expand Up @@ -158,6 +159,7 @@ export default function DelegationSummary({ navigation, route }: Props) {
]);

const hasErrors = Object.keys(status.errors).length > 0;
const error = Object.values(status.errors)[0];

return (
<SafeAreaView style={[styles.root, { backgroundColor: colors.background }]}>
Expand Down Expand Up @@ -222,6 +224,7 @@ export default function DelegationSummary({ navigation, route }: Props) {
</View>
</View>
<View style={styles.footer}>
<TranslatedError error={error} />
<Button
event="SummaryContinue"
type="primary"
Expand Down
3 changes: 2 additions & 1 deletion apps/ledger-live-mobile/src/locales/en/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -5739,7 +5739,8 @@
"delegatedTo": "delegated to",
"started": {
"description": "You may earn rewards by delegating your SOL assets to a validator."
}
},
"reserveWarning": "Please ensure you reserve at least {{amount}} SOL in your wallet to cover future network fees to deactivate and withdraw your stake"
}
},
"near": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,9 @@ exports[`solana currency bridge scanAccounts solana seed 1 1`] = `
"seedIdentifier": "AQbkEagmPgmsdAfS4X8V8UyJnXXjVPMvjeD15etqQ3Jh",
"solanaResources": {
"stakes": "[]",
"unstakeReserve": "0",
},
"spendableBalance": "83389840",
"spendableBalance": "82498960",
"starred": false,
"swapHistory": [],
"syncHash": undefined,
Expand Down Expand Up @@ -52,6 +53,7 @@ exports[`solana currency bridge scanAccounts solana seed 1 1`] = `
"seedIdentifier": "AQbkEagmPgmsdAfS4X8V8UyJnXXjVPMvjeD15etqQ3Jh",
"solanaResources": {
"stakes": "[]",
"unstakeReserve": "0",
},
"spendableBalance": "0",
"starred": false,
Expand Down
1 change: 1 addition & 0 deletions libs/ledger-live-common/src/families/solana/banner.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ const account: SolanaAccount = {
withdrawable: 0,
},
],
unstakeReserve: BigNumber(0),
},
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -726,13 +726,14 @@ function stakingTests(): TransactionTestSpec[] {
},
expectedStatus: {
amount: testOnChainData.fundedSenderBalance
.minus(fees(1))
.minus(fees(1)) // transaction fee
.minus(fees(1).multipliedBy(2)) // undelegate + withdraw fees reserve
.minus(testOnChainData.fees.stakeAccountRentExempt)
.minus(testOnChainData.fees.systemAccountRentExempt),
estimatedFees: fees(1).plus(testOnChainData.fees.stakeAccountRentExempt),
totalSpent: testOnChainData.fundedSenderBalance.minus(
testOnChainData.fees.systemAccountRentExempt,
),
totalSpent: testOnChainData.fundedSenderBalance
.minus(testOnChainData.fees.systemAccountRentExempt)
.minus(fees(1).multipliedBy(2)), // undelegate + withdraw fees reserve,
errors: {},
},
},
Expand Down Expand Up @@ -1050,6 +1051,7 @@ async function runStakeTest(stakeTestSpec: StakeTestSpec) {
},
} as SolanaStake,
],
unstakeReserve: BigNumber(0),
},
};

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import type { Account } from "@ledgerhq/types-live";
import type { Command, Transaction } from "./types";
import {
buildTransferInstructions,
Expand All @@ -20,15 +19,15 @@ import {
import { ChainAPI } from "./api";

export const buildTransactionWithAPI = async (
account: Account,
address: string,
transaction: Transaction,
api: ChainAPI,
): Promise<readonly [OnChainTransaction, (signature: Buffer) => OnChainTransaction]> => {
const instructions = buildInstructions(transaction);

const recentBlockhash = await api.getLatestBlockhash();

const feePayer = new PublicKey(account.freshAddress);
const feePayer = new PublicKey(address);

const tm = new TransactionMessage({
payerKey: feePayer,
Expand All @@ -41,7 +40,7 @@ export const buildTransactionWithAPI = async (
return [
tx,
(signature: Buffer) => {
tx.addSignature(new PublicKey(account.freshAddress), signature);
tx.addSignature(new PublicKey(address), signature);
return tx;
},
];
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,54 @@
import type { AccountBridge } from "@ledgerhq/types-live";
import type { Account, AccountBridge } from "@ledgerhq/types-live";
import type { Transaction } from "./types";
import BigNumber from "bignumber.js";
import { ChainAPI } from "./api";
import {
getAccountMinimumBalanceForRentExemption,
getStakeAccountMinimumBalanceForRentExemption,
} from "./api/chain/web3";
import { getStakeAccountMinimumBalanceForRentExemption } from "./api/chain/web3";
import { getMainAccount } from "../../account";
import { estimateTxFee } from "./tx-fees";

export const estimateFeeAndSpendable = async (
api: ChainAPI,
account: Account,
transaction?: Transaction | undefined | null,
): Promise<{ fee: number; spendable: BigNumber }> => {
const txKind = transaction?.model.kind ?? "transfer";
const txFee = await estimateTxFee(api, account.freshAddress, txKind);

const spendableBalance = BigNumber.max(account.spendableBalance.minus(txFee), 0);

switch (txKind) {
case "token.createATA": {
const assocAccRentExempt = await api.getAssocTokenAccMinNativeBalance();

return {
fee: txFee + assocAccRentExempt,
spendable: BigNumber.max(spendableBalance.minus(assocAccRentExempt), 0),
};
}
case "stake.createAccount": {
const stakeAccRentExempt = await getStakeAccountMinimumBalanceForRentExemption(api);
const unstakeReserve =
(await estimateTxFee(api, account.freshAddress, "stake.undelegate")) +
(await estimateTxFee(api, account.freshAddress, "stake.withdraw"));

return {
fee: txFee + stakeAccRentExempt,
spendable: BigNumber.max(
spendableBalance.minus(stakeAccRentExempt).minus(unstakeReserve),
0,
),
};
}

default: {
return {
fee: txFee,
spendable: spendableBalance,
};
}
}
};

const estimateMaxSpendableWithAPI = async (
{
account,
Expand All @@ -20,24 +60,8 @@ const estimateMaxSpendableWithAPI = async (
const mainAccount = getMainAccount(account, parentAccount);

switch (account.type) {
case "Account": {
const txKind = transaction?.model.kind ?? "transfer";
const txFee = await estimateTxFee(api, mainAccount, txKind);

switch (txKind) {
case "stake.createAccount": {
const stakeAccRentExempt = await getStakeAccountMinimumBalanceForRentExemption(api);
return BigNumber.max(account.spendableBalance.minus(txFee).minus(stakeAccRentExempt), 0);
}
default: {
const rentExemptMin = await getAccountMinimumBalanceForRentExemption(
api,
account.freshAddress,
);
return BigNumber.max(account.spendableBalance.minus(txFee).minus(rentExemptMin), 0);
}
}
}
case "Account":
return (await estimateFeeAndSpendable(api, mainAccount, transaction)).spendable;
case "TokenAccount":
return account.spendableBalance;
}
Expand Down
Loading

6 comments on commit e1fb9bf

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Bot] Testing with 'Nitrogen' ($0.00) ⏲ 10min 5s

What is the bot and how does it work? Everything is documented here!

1 critical spec errors

Spec Solana failed!

Error: scan accounts timeout for currency Solana
Details of the 0 mutations

Spec Solana (failed)


Details of the 8 uncovered mutations

Spec Solana (8)

  • Transfer ~50%:
  • Transfer Max:
  • Delegate:
  • Deactivate Activating Delegation:
  • Deactivate Active Delegation:
  • Reactivate Deactivating Delegation:
  • Activate Inactive Delegation:
  • Withdraw Delegation:
Portfolio ($0.00) – Details of the 1 currencies
Spec (accounts) State Remaining Runs (est) funds?
Solana (0) 0 ops , 🤷‍♂️ ``

Performance ⏲ 10min 5s

Time spent for each spec: (total across mutations)

Spec (accounts) preload scan re-sync tx status sign op broadcast test destination test
TOTAL 193ms N/A N/A N/A N/A N/A N/A N/A
Solana (0) 193ms N/A N/A N/A N/A N/A N/A N/A

What is the bot and how does it work? Everything is documented here!

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Bot] Testing with 'Oxygen' 💰 1 miss funds ($0.00) ⏲ 31.8s

💰 1 specs may miss funds: Solana

What is the bot and how does it work? Everything is documented here!

⚠️ 1 spec hints
  • Spec Solana:
    • There are not enough accounts (1) to cover all mutations (8).
      Please increase the account target to at least 9 accounts
Details of the 0 mutations

Spec Solana (failed)

Spec Solana found 1 Solana accounts (preload: 142ms). Will use Solana 1.2.0 on nanoS 2.1.0
Solana 1 cross: 0 SOL (0ops) (5aSiryY55P7fcasAzSWGf2bGDr1SKokiZntRNpnTDnVC on 44'/501'/0') solanaSub#0 js:2:solana:5aSiryY55P7fcasAzSWGf2bGDr1SKokiZntRNpnTDnVC:solanaSub

This SEED does not have Solana. Please send funds to 5aSiryY55P7fcasAzSWGf2bGDr1SKokiZntRNpnTDnVC

Details of the 8 uncovered mutations

Spec Solana (8)

  • Transfer ~50%:
  • Transfer Max:
  • Delegate:
  • Deactivate Activating Delegation:
  • Deactivate Active Delegation:
  • Reactivate Deactivating Delegation:
  • Activate Inactive Delegation:
  • Withdraw Delegation:
Portfolio ($0.00) – Details of the 1 currencies
Spec (accounts) State Remaining Runs (est) funds?
Solana (1) 0 ops , 0 SOL ($0.00) 5aSiryY55P7fcasAzSWGf2bGDr1SKokiZntRNpnTDnVC
Solana 1 cross: 0 SOL (0ops) (5aSiryY55P7fcasAzSWGf2bGDr1SKokiZntRNpnTDnVC on 44'/501'/0') solanaSub#0 js:2:solana:5aSiryY55P7fcasAzSWGf2bGDr1SKokiZntRNpnTDnVC:solanaSub
Performance ⏲ 31.8s

Time spent for each spec: (total across mutations)

Spec (accounts) preload scan re-sync tx status sign op broadcast test destination test
TOTAL 142ms 19s N/A N/A N/A N/A N/A N/A
Solana (0) 142ms 19s N/A N/A N/A N/A N/A N/A

What is the bot and how does it work? Everything is documented here!

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Bot] Testing with 'Nitrogen' ($0.00) ⏲ 10min 4s

What is the bot and how does it work? Everything is documented here!

1 critical spec errors

Spec Solana failed!

Error: scan accounts timeout for currency Solana
Details of the 0 mutations

Spec Solana (failed)


Details of the 8 uncovered mutations

Spec Solana (8)

  • Transfer ~50%:
  • Transfer Max:
  • Delegate:
  • Deactivate Activating Delegation:
  • Deactivate Active Delegation:
  • Reactivate Deactivating Delegation:
  • Activate Inactive Delegation:
  • Withdraw Delegation:
Portfolio ($0.00) – Details of the 1 currencies
Spec (accounts) State Remaining Runs (est) funds?
Solana (0) 0 ops , 🤷‍♂️ ``

Performance ⏲ 10min 4s

Time spent for each spec: (total across mutations)

Spec (accounts) preload scan re-sync tx status sign op broadcast test destination test
TOTAL 132ms N/A N/A N/A N/A N/A N/A N/A
Solana (0) 132ms N/A N/A N/A N/A N/A N/A N/A

What is the bot and how does it work? Everything is documented here!

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Bot] Testing with 'Nitrogen' ($0.00) ⏲ 10min 4s

What is the bot and how does it work? Everything is documented here!

1 critical spec errors

Spec Solana failed!

Error: scan accounts timeout for currency Solana
Details of the 0 mutations

Spec Solana (failed)


Details of the 8 uncovered mutations

Spec Solana (8)

  • Transfer ~50%:
  • Transfer Max:
  • Delegate:
  • Deactivate Activating Delegation:
  • Deactivate Active Delegation:
  • Reactivate Deactivating Delegation:
  • Activate Inactive Delegation:
  • Withdraw Delegation:
Portfolio ($0.00) – Details of the 1 currencies
Spec (accounts) State Remaining Runs (est) funds?
Solana (0) 0 ops , 🤷‍♂️ ``

Performance ⏲ 10min 4s

Time spent for each spec: (total across mutations)

Spec (accounts) preload scan re-sync tx status sign op broadcast test destination test
TOTAL 142ms N/A N/A N/A N/A N/A N/A N/A
Solana (0) 142ms N/A N/A N/A N/A N/A N/A N/A

What is the bot and how does it work? Everything is documented here!

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Bot] Testing with 'Nitrogen' ($0.00) ⏲ 10min 3s

What is the bot and how does it work? Everything is documented here!

1 critical spec errors

Spec Solana failed!

Error: scan accounts timeout for currency Solana
Details of the 0 mutations

Spec Solana (failed)


Details of the 8 uncovered mutations

Spec Solana (8)

  • Transfer ~50%:
  • Transfer Max:
  • Delegate:
  • Deactivate Activating Delegation:
  • Deactivate Active Delegation:
  • Reactivate Deactivating Delegation:
  • Activate Inactive Delegation:
  • Withdraw Delegation:
Portfolio ($0.00) – Details of the 1 currencies
Spec (accounts) State Remaining Runs (est) funds?
Solana (0) 0 ops , 🤷‍♂️ ``

Performance ⏲ 10min 3s

Time spent for each spec: (total across mutations)

Spec (accounts) preload scan re-sync tx status sign op broadcast test destination test
TOTAL 156ms N/A N/A N/A N/A N/A N/A N/A
Solana (0) 156ms N/A N/A N/A N/A N/A N/A N/A

What is the bot and how does it work? Everything is documented here!

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Bot] Testing with 'Nitrogen' ($0.00) ⏲ 10min 4s

What is the bot and how does it work? Everything is documented here!

1 critical spec errors

Spec Solana failed!

Error: scan accounts timeout for currency Solana
Details of the 0 mutations

Spec Solana (failed)


Details of the 8 uncovered mutations

Spec Solana (8)

  • Transfer ~50%:
  • Transfer Max:
  • Delegate:
  • Deactivate Activating Delegation:
  • Deactivate Active Delegation:
  • Reactivate Deactivating Delegation:
  • Activate Inactive Delegation:
  • Withdraw Delegation:
Portfolio ($0.00) – Details of the 1 currencies
Spec (accounts) State Remaining Runs (est) funds?
Solana (0) 0 ops , 🤷‍♂️ ``

Performance ⏲ 10min 4s

Time spent for each spec: (total across mutations)

Spec (accounts) preload scan re-sync tx status sign op broadcast test destination test
TOTAL 134ms N/A N/A N/A N/A N/A N/A N/A
Solana (0) 134ms N/A N/A N/A N/A N/A N/A N/A

What is the bot and how does it work? Everything is documented here!

Please sign in to comment.