diff --git a/.changeset/many-hats-protect.md b/.changeset/many-hats-protect.md new file mode 100644 index 00000000000..72340b6cc74 --- /dev/null +++ b/.changeset/many-hats-protect.md @@ -0,0 +1,9 @@ +--- +"@ledgerhq/coin-cardano": minor +"@ledgerhq/errors": minor +"ledger-live-desktop": minor +"live-mobile": minor +"@ledgerhq/live-common": minor +--- + +Modularize Cardano diff --git a/apps/ledger-live-desktop/src/renderer/families/cardano/DelegationFlowModal/Body.tsx b/apps/ledger-live-desktop/src/renderer/families/cardano/DelegationFlowModal/Body.tsx index db4c0989cbb..b2fd1b31e9d 100644 --- a/apps/ledger-live-desktop/src/renderer/families/cardano/DelegationFlowModal/Body.tsx +++ b/apps/ledger-live-desktop/src/renderer/families/cardano/DelegationFlowModal/Body.tsx @@ -24,7 +24,7 @@ import GenericStepConnectDevice from "~/renderer/modals/Send/steps/GenericStepCo import StepConfirmation, { StepConfirmationFooter } from "./steps/StepConfirmation"; import { CardanoAccount } from "@ledgerhq/live-common/families/cardano/types"; import { TFunction } from "i18next"; -import { StakePool } from "@ledgerhq/live-common/families/cardano/api/api-types"; +import { StakePool } from "@ledgerhq/live-common/families/cardano/staking"; type OwnProps = { stepId: StepId; diff --git a/apps/ledger-live-desktop/src/renderer/families/cardano/DelegationFlowModal/LedgerPoolIcon.tsx b/apps/ledger-live-desktop/src/renderer/families/cardano/DelegationFlowModal/LedgerPoolIcon.tsx index 9cf32ee735c..a0d34900863 100644 --- a/apps/ledger-live-desktop/src/renderer/families/cardano/DelegationFlowModal/LedgerPoolIcon.tsx +++ b/apps/ledger-live-desktop/src/renderer/families/cardano/DelegationFlowModal/LedgerPoolIcon.tsx @@ -1,10 +1,9 @@ -import { StakePool } from "@ledgerhq/live-common/families/cardano/api/api-types"; +import { LEDGER_POOL_IDS, StakePool } from "@ledgerhq/live-common/families/cardano/staking"; import React from "react"; import { IconContainer } from "~/renderer/components/Delegation/ValidatorRow"; import LedgerLiveLogo from "~/renderer/components/LedgerLiveLogo"; import Logo from "~/renderer/icons/Logo"; import FirstLetterIcon from "~/renderer/components/FirstLetterIcon"; -import { LEDGER_POOL_IDS } from "@ledgerhq/live-common/families/cardano/utils"; const CardanoLedgerPoolIcon = ({ validator }: { validator: StakePool }) => { return ( diff --git a/apps/ledger-live-desktop/src/renderer/families/cardano/DelegationFlowModal/ScrollLoadingList.tsx b/apps/ledger-live-desktop/src/renderer/families/cardano/DelegationFlowModal/ScrollLoadingList.tsx index 13f0d41b724..6370109a290 100644 --- a/apps/ledger-live-desktop/src/renderer/families/cardano/DelegationFlowModal/ScrollLoadingList.tsx +++ b/apps/ledger-live-desktop/src/renderer/families/cardano/DelegationFlowModal/ScrollLoadingList.tsx @@ -1,6 +1,6 @@ import React, { useCallback, useState, useRef, memo, useEffect } from "react"; import debounce from "lodash/debounce"; -import { StakePool } from "@ledgerhq/live-common/families/cardano/api/api-types"; +import { StakePool } from "@ledgerhq/live-common/families/cardano/staking"; import styled from "styled-components"; import Box from "~/renderer/components/Box"; import BigSpinner from "~/renderer/components/BigSpinner"; diff --git a/apps/ledger-live-desktop/src/renderer/families/cardano/DelegationFlowModal/fields/ValidatorField.tsx b/apps/ledger-live-desktop/src/renderer/families/cardano/DelegationFlowModal/fields/ValidatorField.tsx index 924a9e0d4a9..a747464d49c 100644 --- a/apps/ledger-live-desktop/src/renderer/families/cardano/DelegationFlowModal/fields/ValidatorField.tsx +++ b/apps/ledger-live-desktop/src/renderer/families/cardano/DelegationFlowModal/fields/ValidatorField.tsx @@ -9,11 +9,13 @@ import IconAngleDown from "~/renderer/icons/AngleDown"; import ValidatorRow from "./ValidatorRow"; import { Account } from "@ledgerhq/types-live"; import { TransactionStatus } from "@ledgerhq/live-common/generated/types"; -import { StakePool } from "@ledgerhq/live-common/families/cardano/api/api-types"; +import { + LEDGER_POOL_IDS, + StakePool, + fetchPoolDetails, +} from "@ledgerhq/live-common/families/cardano/staking"; import { useCardanoFamilyPools } from "@ledgerhq/live-common/families/cardano/react"; -import { fetchPoolDetails } from "@ledgerhq/live-common/families/cardano/api/getPools"; import ValidatorSearchInput from "~/renderer/components/Delegation/ValidatorSearchInput"; -import { LEDGER_POOL_IDS } from "@ledgerhq/live-common/families/cardano/utils"; import { CardanoDelegation } from "@ledgerhq/live-common/families/cardano/types"; import BigSpinner from "~/renderer/components/BigSpinner"; import { useAccountUnit } from "~/renderer/hooks/useAccountUnit"; diff --git a/apps/ledger-live-desktop/src/renderer/families/cardano/DelegationFlowModal/fields/ValidatorRow.tsx b/apps/ledger-live-desktop/src/renderer/families/cardano/DelegationFlowModal/fields/ValidatorRow.tsx index fc59867b7e3..bdd1f2f8d29 100644 --- a/apps/ledger-live-desktop/src/renderer/families/cardano/DelegationFlowModal/fields/ValidatorRow.tsx +++ b/apps/ledger-live-desktop/src/renderer/families/cardano/DelegationFlowModal/fields/ValidatorRow.tsx @@ -11,7 +11,7 @@ import Text from "~/renderer/components/Text"; import Check from "~/renderer/icons/Check"; import { openURL } from "~/renderer/linking"; import LedgerPoolIcon from "../LedgerPoolIcon"; -import { StakePool } from "@ledgerhq/live-common/families/cardano/api/api-types"; +import { StakePool } from "@ledgerhq/live-common/families/cardano/staking"; type Props = { currency: CryptoCurrency; diff --git a/apps/ledger-live-desktop/src/renderer/families/cardano/DelegationFlowModal/steps/StepDelegation.tsx b/apps/ledger-live-desktop/src/renderer/families/cardano/DelegationFlowModal/steps/StepDelegation.tsx index 614ba6b3c29..9729c15951e 100644 --- a/apps/ledger-live-desktop/src/renderer/families/cardano/DelegationFlowModal/steps/StepDelegation.tsx +++ b/apps/ledger-live-desktop/src/renderer/families/cardano/DelegationFlowModal/steps/StepDelegation.tsx @@ -8,7 +8,7 @@ import Box from "~/renderer/components/Box"; import Button from "~/renderer/components/Button"; import { AccountBridge } from "@ledgerhq/types-live"; import { Transaction as CardanoTransaction } from "@ledgerhq/live-common/families/cardano/types"; -import { StakePool } from "@ledgerhq/live-common/families/cardano/api/api-types"; +import { StakePool } from "@ledgerhq/live-common/families/cardano/staking"; import ValidatorField from "../fields/ValidatorField"; import ErrorBanner from "~/renderer/components/ErrorBanner"; import AccountFooter from "~/renderer/modals/Send/AccountFooter"; diff --git a/apps/ledger-live-desktop/src/renderer/families/cardano/DelegationFlowModal/types.ts b/apps/ledger-live-desktop/src/renderer/families/cardano/DelegationFlowModal/types.ts index 4aaae75ed38..a876859563e 100644 --- a/apps/ledger-live-desktop/src/renderer/families/cardano/DelegationFlowModal/types.ts +++ b/apps/ledger-live-desktop/src/renderer/families/cardano/DelegationFlowModal/types.ts @@ -7,7 +7,7 @@ import { Transaction, TransactionStatus, } from "@ledgerhq/live-common/families/cardano/types"; -import { StakePool } from "@ledgerhq/live-common/families/cardano/api/api-types"; +import { StakePool } from "@ledgerhq/live-common/families/cardano/staking"; export type StepId = "validator" | "summary" | "connectDevice" | "confirmation"; diff --git a/apps/ledger-live-mobile/src/families/cardano/DelegationFlow/02-Summary.tsx b/apps/ledger-live-mobile/src/families/cardano/DelegationFlow/02-Summary.tsx index bb3fa248eba..6211f832a94 100644 --- a/apps/ledger-live-mobile/src/families/cardano/DelegationFlow/02-Summary.tsx +++ b/apps/ledger-live-mobile/src/families/cardano/DelegationFlow/02-Summary.tsx @@ -10,19 +10,19 @@ import { getAccountCurrency } from "@ledgerhq/live-common/account/index"; import { getAccountBridge } from "@ledgerhq/live-common/bridge/index"; import useBridgeTransaction from "@ledgerhq/live-common/bridge/useBridgeTransaction"; import { formatCurrencyUnit, getCurrencyColor } from "@ledgerhq/live-common/currencies/index"; -import type { - APIGetPoolsDetail, - StakePool, -} from "@ledgerhq/live-common/families/cardano/api/api-types"; +import { + LEDGER_POOL_IDS, + fetchPoolDetails, + type APIGetPoolsDetail, + type StakePool, +} from "@ledgerhq/live-common/families/cardano/staking"; import type { CardanoAccount, CardanoDelegation, + TransactionStatus, } from "@ledgerhq/live-common/families/cardano/types"; -import { LEDGER_POOL_IDS } from "@ledgerhq/live-common/families/cardano/utils"; -import { fetchPoolDetails } from "@ledgerhq/live-common/families/cardano/api/getPools"; import { Box, Text } from "@ledgerhq/native-ui"; import { AccountLike } from "@ledgerhq/types-live"; -import { TransactionStatus } from "@ledgerhq/live-common/families/cardano/types"; import Button from "~/components/Button"; import Skeleton from "~/components/Skeleton"; import Circle from "~/components/Circle"; diff --git a/apps/ledger-live-mobile/src/families/cardano/DelegationFlow/SelectPool.tsx b/apps/ledger-live-mobile/src/families/cardano/DelegationFlow/SelectPool.tsx index 063a999cda0..929aef91a93 100644 --- a/apps/ledger-live-mobile/src/families/cardano/DelegationFlow/SelectPool.tsx +++ b/apps/ledger-live-mobile/src/families/cardano/DelegationFlow/SelectPool.tsx @@ -3,7 +3,7 @@ import invariant from "invariant"; import React, { useCallback } from "react"; import { FlatList, StyleSheet, View, SafeAreaView } from "react-native"; import { useSelector } from "react-redux"; -import type { StakePool } from "@ledgerhq/live-common/families/cardano/api/api-types"; +import type { StakePool } from "@ledgerhq/live-common/families/cardano/staking"; import { useCardanoFamilyPools } from "@ledgerhq/live-common/families/cardano/react"; import { TrackScreen } from "~/analytics"; import { ScreenName } from "~/const"; diff --git a/apps/ledger-live-mobile/src/families/cardano/DelegationFlow/types.ts b/apps/ledger-live-mobile/src/families/cardano/DelegationFlow/types.ts index f62e6a92dbc..75213c9eb8c 100644 --- a/apps/ledger-live-mobile/src/families/cardano/DelegationFlow/types.ts +++ b/apps/ledger-live-mobile/src/families/cardano/DelegationFlow/types.ts @@ -1,7 +1,7 @@ import type { Transaction, TransactionStatus } from "@ledgerhq/live-common/families/cardano/types"; import type { Operation } from "@ledgerhq/types-live"; import type { Device } from "@ledgerhq/live-common/hw/actions/types"; -import type { StakePool } from "@ledgerhq/live-common/families/cardano/api/api-types"; +import type { StakePool } from "@ledgerhq/live-common/families/cardano/staking"; import { ScreenName } from "~/const"; export type CardanoDelegationFlowParamList = { diff --git a/apps/ledger-live-mobile/src/families/cardano/Delegations/Row.tsx b/apps/ledger-live-mobile/src/families/cardano/Delegations/Row.tsx index 4d2477f6350..55c715b0445 100644 --- a/apps/ledger-live-mobile/src/families/cardano/Delegations/Row.tsx +++ b/apps/ledger-live-mobile/src/families/cardano/Delegations/Row.tsx @@ -4,7 +4,7 @@ import { useTranslation } from "react-i18next"; import { useTheme } from "@react-navigation/native"; import { Text } from "@ledgerhq/native-ui"; import type { CardanoDelegation } from "@ledgerhq/live-common/families/cardano/types"; -import { LEDGER_POOL_IDS } from "@ledgerhq/live-common/families/cardano/utils"; +import { LEDGER_POOL_IDS } from "@ledgerhq/live-common/families/cardano/staking"; import { Currency, Unit } from "@ledgerhq/types-cryptoassets"; import CounterValue from "~/components/CounterValue"; import ArrowRight from "~/icons/ArrowRight"; diff --git a/apps/ledger-live-mobile/src/families/cardano/Delegations/index.tsx b/apps/ledger-live-mobile/src/families/cardano/Delegations/index.tsx index 81de384fca9..e2f5fe8f730 100644 --- a/apps/ledger-live-mobile/src/families/cardano/Delegations/index.tsx +++ b/apps/ledger-live-mobile/src/families/cardano/Delegations/index.tsx @@ -7,7 +7,7 @@ import type { CardanoAccount, CardanoDelegation, } from "@ledgerhq/live-common/families/cardano/types"; -import { LEDGER_POOL_IDS } from "@ledgerhq/live-common/families/cardano/utils"; +import { LEDGER_POOL_IDS } from "@ledgerhq/live-common/families/cardano/staking"; import { getDefaultExplorerView, getStakePoolExplorer } from "@ledgerhq/live-common/explorers"; import { StackNavigationProp } from "@react-navigation/stack"; diff --git a/apps/ledger-live-mobile/src/families/cardano/shared/PoolRow.tsx b/apps/ledger-live-mobile/src/families/cardano/shared/PoolRow.tsx index 3c564a787f9..f8c4c658cd4 100644 --- a/apps/ledger-live-mobile/src/families/cardano/shared/PoolRow.tsx +++ b/apps/ledger-live-mobile/src/families/cardano/shared/PoolRow.tsx @@ -1,7 +1,7 @@ import React, { useCallback } from "react"; import { Trans } from "react-i18next"; import { StyleSheet, View } from "react-native"; -import type { StakePool } from "@ledgerhq/live-common/families/cardano/api/api-types"; +import type { StakePool } from "@ledgerhq/live-common/families/cardano/staking"; import { Text } from "@ledgerhq/native-ui"; import Touchable from "~/components/Touchable"; import PoolImage from "./PoolImage"; diff --git a/libs/coin-modules/coin-cardano/.eslintrc.js b/libs/coin-modules/coin-cardano/.eslintrc.js new file mode 100644 index 00000000000..8f6848a860b --- /dev/null +++ b/libs/coin-modules/coin-cardano/.eslintrc.js @@ -0,0 +1,20 @@ +module.exports = { + env: { + browser: true, + es6: true, + }, + overrides: [ + { + files: ["src/**/*.test.{ts,tsx}"], + env: { + "jest/globals": true, + }, + plugins: ["jest"], + }, + ], + rules: { + "no-console": ["error", { allow: ["warn", "error"] }], + "@typescript-eslint/no-empty-function": "off", + "@typescript-eslint/no-explicit-any": "warn", + }, +}; diff --git a/libs/coin-modules/coin-cardano/.unimportedrc.json b/libs/coin-modules/coin-cardano/.unimportedrc.json new file mode 100644 index 00000000000..ece910efacb --- /dev/null +++ b/libs/coin-modules/coin-cardano/.unimportedrc.json @@ -0,0 +1,33 @@ +{ + "entry": [ + "src/account.ts", + "src/api/getPools.ts", + "src/bridge/index.ts", + "src/cli-transaction.ts", + "src/datasets/rawAccount.1.ts", + "src/datasets/scanAccounts.ts", + "src/deviceTransactionConfig.ts", + "src/errors.ts", + "src/specs.ts", + "src/speculos-deviceActions.ts", + "src/transaction.ts", + "src/utils.ts" + ], + "ignoreUnimported": [ + "src/api/getDelegationInfo.ts", + "src/api/getNetworkInfo.ts", + "src/api/getTransactions.ts", + "src/api/submitTransaction.ts", + "src/hw-getAddress.ts", + "src/js-broadcast.ts", + "src/js-buildTransaction.ts", + "src/js-estimateMaxSpendable.ts", + "src/js-getTransactionStatus.ts", + "src/js-signOperation.ts", + "src/js-synchronisation.ts", + "src/js-transaction.ts", + "src/postSyncPatch.ts", + "src/serialization.ts", + "src/signer.ts" + ] +} \ No newline at end of file diff --git a/libs/coin-modules/coin-cardano/jest.config.js b/libs/coin-modules/coin-cardano/jest.config.js new file mode 100644 index 00000000000..256cf95233c --- /dev/null +++ b/libs/coin-modules/coin-cardano/jest.config.js @@ -0,0 +1,8 @@ +/** @type {import('ts-jest/dist/types').JestConfigWithTsJest} */ +module.exports = { + collectCoverageFrom: ["src/**/*.ts"], + coverageDirectory: "coverage", + preset: "ts-jest", + testEnvironment: "node", + testPathIgnorePatterns: ["lib/", "lib-es/"], +}; diff --git a/libs/coin-modules/coin-cardano/package.json b/libs/coin-modules/coin-cardano/package.json new file mode 100644 index 00000000000..6b0fd5ae758 --- /dev/null +++ b/libs/coin-modules/coin-cardano/package.json @@ -0,0 +1,91 @@ +{ + "name": "@ledgerhq/coin-cardano", + "version": "0.0.1", + "description": "Ledger Cardano Coin integration", + "keywords": [ + "Ledger", + "LedgerWallet", + "ada", + "Cardano", + "Hardware Wallet" + ], + "repository": { + "type": "git", + "url": "https://github.com/LedgerHQ/ledger-live.git" + }, + "bugs": { + "url": "https://github.com/LedgerHQ/ledger-live/issues" + }, + "homepage": "https://github.com/LedgerHQ/ledger-live/tree/develop/libs/coin-modules/coin-cardano", + "publishConfig": { + "access": "public" + }, + "typesVersions": { + "*": { + "lib/*": [ + "lib/*" + ], + "lib-es/*": [ + "lib-es/*" + ], + "bridge": [ + "lib/bridge/index" + ], + "*": [ + "lib/*" + ] + } + }, + "exports": { + "./lib/*": "./lib/*.js", + "./lib-es/*": "./lib-es/*.js", + "./bridge": { + "require": "./lib/bridge/index.js", + "default": "./lib-es/bridge/index.js" + }, + "./*": { + "require": "./lib/*.js", + "default": "./lib-es/*.js" + }, + "./package.json": "./package.json" + }, + "license": "Apache-2.0", + "dependencies": { + "@ledgerhq/coin-framework": "workspace:^", + "@ledgerhq/cryptoassets": "workspace:^", + "@ledgerhq/devices": "workspace:^", + "@ledgerhq/errors": "workspace:^", + "@ledgerhq/live-env": "workspace:^", + "@ledgerhq/live-network": "workspace:^", + "@ledgerhq/logs": "workspace:^", + "@ledgerhq/types-cryptoassets": "workspace:^", + "@ledgerhq/types-live": "workspace:^", + "@stricahq/bip32ed25519": "^1.0.3", + "@stricahq/typhonjs": "^1.2.6", + "bech32": "^1.1.3", + "bignumber.js": "^9.1.2", + "expect": "^27.4.6", + "invariant": "^2.2.2", + "lodash": "^4.17.21", + "rxjs": "^7.8.1" + }, + "devDependencies": { + "@types/invariant": "^2.2.2", + "@types/jest": "^29.5.10", + "@types/lodash": "^4.14.191", + "jest": "^29.7.0", + "ts-jest": "^29.1.1" + }, + "scripts": { + "clean": "rimraf lib lib-es", + "build": "tsc && tsc -m ES6 --outDir lib-es", + "coverage": "jest --coverage --testPathIgnorePatterns='/bridge.integration.test.ts|node_modules|lib-es|lib/' --passWithNoTests && mv coverage/coverage-final.json coverage/coverage-cardano.json", + "prewatch": "pnpm build", + "watch": "tsc --watch", + "doc": "documentation readme src/** --section=API --pe ts --re ts --re d.ts", + "lint": "eslint ./src --no-error-on-unmatched-pattern --ext .ts,.tsx --cache", + "lint:fix": "pnpm lint --fix", + "test": "jest", + "unimported": "unimported" + } +} diff --git a/libs/ledger-live-common/src/families/cardano/account.ts b/libs/coin-modules/coin-cardano/src/account.ts similarity index 87% rename from libs/ledger-live-common/src/families/cardano/account.ts rename to libs/coin-modules/coin-cardano/src/account.ts index 3d47541e1c5..aca2b922dfa 100644 --- a/libs/ledger-live-common/src/families/cardano/account.ts +++ b/libs/coin-modules/coin-cardano/src/account.ts @@ -1,6 +1,6 @@ import invariant from "invariant"; -import { getAccountCurrency } from "../../account"; -import { formatCurrencyUnit } from "../../currencies"; +import { getAccountCurrency } from "@ledgerhq/coin-framework/account/index"; +import { formatCurrencyUnit } from "@ledgerhq/coin-framework/currencies/index"; import { CardanoAccount } from "./types"; export function formatAccountSpecifics(account: CardanoAccount): string { diff --git a/libs/ledger-live-common/src/families/cardano/address.unit.test.ts b/libs/coin-modules/coin-cardano/src/address.unit.test.ts similarity index 100% rename from libs/ledger-live-common/src/families/cardano/address.unit.test.ts rename to libs/coin-modules/coin-cardano/src/address.unit.test.ts diff --git a/libs/ledger-live-common/src/families/cardano/api/api-types.ts b/libs/coin-modules/coin-cardano/src/api/api-types.ts similarity index 100% rename from libs/ledger-live-common/src/families/cardano/api/api-types.ts rename to libs/coin-modules/coin-cardano/src/api/api-types.ts diff --git a/libs/ledger-live-common/src/families/cardano/api/getDelegationInfo.ts b/libs/coin-modules/coin-cardano/src/api/getDelegationInfo.ts similarity index 100% rename from libs/ledger-live-common/src/families/cardano/api/getDelegationInfo.ts rename to libs/coin-modules/coin-cardano/src/api/getDelegationInfo.ts diff --git a/libs/ledger-live-common/src/families/cardano/api/getNetworkInfo.ts b/libs/coin-modules/coin-cardano/src/api/getNetworkInfo.ts similarity index 100% rename from libs/ledger-live-common/src/families/cardano/api/getNetworkInfo.ts rename to libs/coin-modules/coin-cardano/src/api/getNetworkInfo.ts diff --git a/libs/ledger-live-common/src/families/cardano/api/getPools.ts b/libs/coin-modules/coin-cardano/src/api/getPools.ts similarity index 100% rename from libs/ledger-live-common/src/families/cardano/api/getPools.ts rename to libs/coin-modules/coin-cardano/src/api/getPools.ts diff --git a/libs/ledger-live-common/src/families/cardano/api/getTransactions.ts b/libs/coin-modules/coin-cardano/src/api/getTransactions.ts similarity index 100% rename from libs/ledger-live-common/src/families/cardano/api/getTransactions.ts rename to libs/coin-modules/coin-cardano/src/api/getTransactions.ts diff --git a/libs/ledger-live-common/src/families/cardano/api/submitTransaction.ts b/libs/coin-modules/coin-cardano/src/api/submitTransaction.ts similarity index 100% rename from libs/ledger-live-common/src/families/cardano/api/submitTransaction.ts rename to libs/coin-modules/coin-cardano/src/api/submitTransaction.ts diff --git a/libs/coin-modules/coin-cardano/src/bridge.integration.test.ts b/libs/coin-modules/coin-cardano/src/bridge.integration.test.ts new file mode 100644 index 00000000000..83bd4bf91e2 --- /dev/null +++ b/libs/coin-modules/coin-cardano/src/bridge.integration.test.ts @@ -0,0 +1,113 @@ +import BigNumber from "bignumber.js"; +import { cardanoRawAccount1 } from "./datasets/rawAccount.1"; +import { cardanoScanAccounts } from "./datasets/scanAccounts"; +import { CardanoInvalidPoolId, CardanoMinAmountError } from "./errors"; +import { fromTransactionRaw } from "./transaction"; +import type { Transaction } from "./types"; +import type { DatasetTest } from "@ledgerhq/types-live"; + +export const dataset: DatasetTest = { + implementations: ["js"], + currencies: { + cardano_testnet: { + scanAccounts: cardanoScanAccounts, + accounts: [ + { + raw: cardanoRawAccount1, + transactions: [ + { + name: "amount less then minimum", + transaction: fromTransactionRaw({ + family: "cardano", + recipient: + "addr_test1qpl90kc2jl5kr9tev0s7vays9yhwcdnq8nlylyk4dqsdq3g466elxnxwrzwq72pvp5akenj30t5s9et7frfvrxxx8xcsxrzs87", + amount: "0.1", + mode: "send", + poolId: undefined, + }), + expectedStatus: { + amount: new BigNumber("0.1"), + errors: { + amount: new CardanoMinAmountError(), + }, + }, + }, + /* // FIXME broken test + { + name: "token amount more than balance", + transaction: fromTransactionRaw({ + family: "cardano", + recipient: + "addr_test1qpl90kc2jl5kr9tev0s7vays9yhwcdnq8nlylyk4dqsdq3g466elxnxwrzwq72pvp5akenj30t5s9et7frfvrxxx8xcsxrzs87", + amount: "101", + subAccountId: + "js:2:cardano_testnet:806499588e0c4a58f4119f7e6e096bf42c3f774a528d2acec9e82ceebf87d1ceb3d4f3622dd2c77c65cc89c123f79337db22cf8a69f122e36dab1bf5083bf82d:cardano+cardano_testnet%2Fnative%2F47be64fcc8a7fe5321b976282ce4e43e4d29015f6613cfabcea28eab54657374", + mode: "send", + poolId: undefined, + }), + expectedStatus: { + amount: new BigNumber("101"), + errors: { + amount: new NotEnoughBalance(), + }, + }, + }, + { + name: "send max token", + transaction: fromTransactionRaw({ + family: "cardano", + recipient: + "addr_test1qpl90kc2jl5kr9tev0s7vays9yhwcdnq8nlylyk4dqsdq3g466elxnxwrzwq72pvp5akenj30t5s9et7frfvrxxx8xcsxrzs87", + amount: "0", + subAccountId: + "js:2:cardano_testnet:806499588e0c4a58f4119f7e6e096bf42c3f774a528d2acec9e82ceebf87d1ceb3d4f3622dd2c77c65cc89c123f79337db22cf8a69f122e36dab1bf5083bf82d:cardano+cardano_testnet%2Fnative%2F47be64fcc8a7fe5321b976282ce4e43e4d29015f6613cfabcea28eab54657374", + mode: "send", + useAllAmount: true, + poolId: undefined, + }), + expectedStatus: { + amount: new BigNumber("100"), + totalSpent: new BigNumber("100"), + errors: {}, + warnings: {}, + }, + }, + */ + { + name: "delegate to invalid poolId", + transaction: fromTransactionRaw({ + family: "cardano", + recipient: "", + amount: "0", + mode: "delegate", + poolId: "efae72c07a26e4542ba55ef59d35ad45ffaaac312865e3a758ede", + }), + expectedStatus: { + errors: { + poolId: new CardanoInvalidPoolId(), + }, + }, + }, + { + name: "delegate valid poolId", + transaction: fromTransactionRaw({ + family: "cardano", + recipient: "", + amount: "0", + mode: "delegate", + poolId: "efae72c07a26e4542ba55ef59d35ad45ffaaac312865e3a758ede997", + }), + expectedStatus: {}, + }, + ], + }, + ], + }, + }, +}; + +describe("Cardano bridge", () => { + test.todo( + "This is an empty test to make jest command pass. Remove it once there is a real test.", + ); +}); diff --git a/libs/coin-modules/coin-cardano/src/bridge/index.ts b/libs/coin-modules/coin-cardano/src/bridge/index.ts new file mode 100644 index 00000000000..0393869a458 --- /dev/null +++ b/libs/coin-modules/coin-cardano/src/bridge/index.ts @@ -0,0 +1,68 @@ +import { + defaultUpdateTransaction, + makeAccountBridgeReceive, + makeScanAccounts, + makeSync, +} from "@ledgerhq/coin-framework/bridge/jsHelpers"; +import getAddressWrapper from "@ledgerhq/coin-framework/bridge/getAddressWrapper"; +import { SignerContext } from "@ledgerhq/coin-framework/signer"; +import type { Transaction } from "../types"; +import { makeGetAccountShape } from "../js-synchronisation"; +import estimateMaxSpendable from "../js-estimateMaxSpendable"; +import { createTransaction, prepareTransaction } from "../js-transaction"; +import getTransactionStatus from "../js-getTransactionStatus"; +import buildSignOperation from "../js-signOperation"; +import broadcast from "../js-broadcast"; +import type { AccountBridge, CurrencyBridge } from "@ledgerhq/types-live"; +import { assignToAccountRaw, assignFromAccountRaw } from "../serialization"; +import { CardanoSigner } from "../signer"; +import resolver from "../hw-getAddress"; +import postSyncPatch from "../postSyncPatch"; + +export function buildCurrencyBridge(signerContext: SignerContext): CurrencyBridge { + const getAddress = resolver(signerContext); + const scanAccounts = makeScanAccounts({ + getAccountShape: makeGetAccountShape(signerContext), + getAddressFn: getAddressWrapper(getAddress), + }); + + return { + scanAccounts, + preload: async () => ({}), + hydrate: () => {}, + }; +} + +export function buildAccountBridge( + signerContext: SignerContext, +): AccountBridge { + const sync = makeSync({ + getAccountShape: makeGetAccountShape(signerContext), + postSync: postSyncPatch, + }); + + const getAddress = resolver(signerContext); + + const receive = makeAccountBridgeReceive(getAddressWrapper(getAddress)); + + return { + estimateMaxSpendable, + createTransaction, + updateTransaction: defaultUpdateTransaction, + getTransactionStatus, + prepareTransaction, + sync, + receive, + signOperation: buildSignOperation(signerContext), + broadcast, + assignToAccountRaw, + assignFromAccountRaw, + }; +} + +export function createBridges(signerContext: SignerContext) { + return { + currencyBridge: buildCurrencyBridge(signerContext), + accountBridge: buildAccountBridge(signerContext), + }; +} diff --git a/libs/ledger-live-common/src/families/cardano/buildSubAccounts.ts b/libs/coin-modules/coin-cardano/src/buildSubAccounts.ts similarity index 97% rename from libs/ledger-live-common/src/families/cardano/buildSubAccounts.ts rename to libs/coin-modules/coin-cardano/src/buildSubAccounts.ts index 74e8935574c..0e15d56e707 100644 --- a/libs/ledger-live-common/src/families/cardano/buildSubAccounts.ts +++ b/libs/coin-modules/coin-cardano/src/buildSubAccounts.ts @@ -1,5 +1,5 @@ import BigNumber from "bignumber.js"; -import { encodeOperationId } from "../../operation"; +import { encodeOperationId } from "@ledgerhq/coin-framework/operation"; import { APITransaction } from "./api/api-types"; import { getAccountChange, getMemoFromTx, isHexString } from "./logic"; import { CardanoOperation, CardanoOperationExtra, PaymentCredential, Token } from "./types"; @@ -12,7 +12,7 @@ import { } from "@ledgerhq/coin-framework/account/index"; import groupBy from "lodash/groupBy"; import keyBy from "lodash/keyBy"; -import { mergeOps } from "../../bridge/jsHelpers"; +import { mergeOps } from "@ledgerhq/coin-framework/bridge/jsHelpers"; import type { CryptoCurrency } from "@ledgerhq/types-cryptoassets"; import type { Account, TokenAccount } from "@ledgerhq/types-live"; diff --git a/libs/ledger-live-common/src/families/cardano/cli-transaction.ts b/libs/coin-modules/coin-cardano/src/cli-transaction.ts similarity index 85% rename from libs/ledger-live-common/src/families/cardano/cli-transaction.ts rename to libs/coin-modules/coin-cardano/src/cli-transaction.ts index d272891f15d..5c774922eaa 100644 --- a/libs/ledger-live-common/src/families/cardano/cli-transaction.ts +++ b/libs/coin-modules/coin-cardano/src/cli-transaction.ts @@ -1,8 +1,7 @@ -import { Account, AccountLike } from "@ledgerhq/types-live"; +import { Account, AccountLike, TokenAccount } from "@ledgerhq/types-live"; import invariant from "invariant"; import flatMap from "lodash/flatMap"; -import { getAccountCurrency } from "../../account"; -import { TokenAccount } from "@ledgerhq/coin-solana/api/chain/account/token"; +import { getAccountCurrency } from "@ledgerhq/coin-framework/account/index"; import { CardanoAccount, Transaction } from "./types"; const options = [ @@ -31,7 +30,7 @@ function inferAccounts(account: Account, opts: Record): Account[] | return accounts; } - return opts.token.map(token => { + return opts.token.map((token: any) => { const subAccounts = account.subAccounts || []; const subAccount = subAccounts.find(a => { @@ -74,8 +73,10 @@ function inferTransactions( }); } -export default { - options, - inferAccounts, - inferTransactions, -}; +export default function makeCliTools() { + return { + options, + inferAccounts, + inferTransactions, + }; +} diff --git a/libs/ledger-live-common/src/families/cardano/constants.ts b/libs/coin-modules/coin-cardano/src/constants.ts similarity index 100% rename from libs/ledger-live-common/src/families/cardano/constants.ts rename to libs/coin-modules/coin-cardano/src/constants.ts diff --git a/libs/ledger-live-common/src/families/cardano/datasets/rawAccount.1.ts b/libs/coin-modules/coin-cardano/src/datasets/rawAccount.1.ts similarity index 100% rename from libs/ledger-live-common/src/families/cardano/datasets/rawAccount.1.ts rename to libs/coin-modules/coin-cardano/src/datasets/rawAccount.1.ts diff --git a/libs/ledger-live-common/src/families/cardano/datasets/scanAccounts.ts b/libs/coin-modules/coin-cardano/src/datasets/scanAccounts.ts similarity index 100% rename from libs/ledger-live-common/src/families/cardano/datasets/scanAccounts.ts rename to libs/coin-modules/coin-cardano/src/datasets/scanAccounts.ts diff --git a/libs/ledger-live-common/src/families/cardano/deviceTransactionConfig.ts b/libs/coin-modules/coin-cardano/src/deviceTransactionConfig.ts similarity index 92% rename from libs/ledger-live-common/src/families/cardano/deviceTransactionConfig.ts rename to libs/coin-modules/coin-cardano/src/deviceTransactionConfig.ts index f12a40470cc..917e1abcede 100644 --- a/libs/ledger-live-common/src/families/cardano/deviceTransactionConfig.ts +++ b/libs/coin-modules/coin-cardano/src/deviceTransactionConfig.ts @@ -1,7 +1,7 @@ import BigNumber from "bignumber.js"; -import { getAccountCurrency, getMainAccount } from "../../account"; -import { formatCurrencyUnit } from "../../currencies"; -import { DeviceTransactionField } from "../../transaction"; +import { getAccountCurrency, getMainAccount } from "@ledgerhq/coin-framework/account/index"; +import { formatCurrencyUnit } from "@ledgerhq/coin-framework/currencies/index"; +import type { CommonDeviceTransactionField as DeviceTransactionField } from "@ledgerhq/coin-framework/transaction/common"; import { Account, AccountLike } from "@ledgerhq/types-live"; import { decodeTokenAssetId, decodeTokenCurrencyId } from "./buildSubAccounts"; import { CardanoAccount, Transaction, TransactionStatus } from "./types"; diff --git a/libs/ledger-live-common/src/families/cardano/errors.ts b/libs/coin-modules/coin-cardano/src/errors.ts similarity index 100% rename from libs/ledger-live-common/src/families/cardano/errors.ts rename to libs/coin-modules/coin-cardano/src/errors.ts diff --git a/libs/coin-modules/coin-cardano/src/hw-getAddress.ts b/libs/coin-modules/coin-cardano/src/hw-getAddress.ts new file mode 100644 index 00000000000..dca0f44b9e2 --- /dev/null +++ b/libs/coin-modules/coin-cardano/src/hw-getAddress.ts @@ -0,0 +1,37 @@ +import { utils as TyphonUtils } from "@stricahq/typhonjs"; +import { address as TyphonAddress } from "@stricahq/typhonjs"; +import { GetAddressFn } from "@ledgerhq/coin-framework/bridge/getAddressWrapper"; +import { SignerContext } from "@ledgerhq/coin-framework/signer"; +import { GetAddressOptions } from "@ledgerhq/coin-framework/derivation"; +import { getBipPathFromString, getBipPathString } from "./logic"; +import { StakeChain } from "./types"; +import { STAKING_ADDRESS_INDEX } from "./constants"; +import { getNetworkParameters } from "./networks"; +import { CardanoSigner } from "./signer"; + +const resolver = (signerContext: SignerContext): GetAddressFn => { + return async (deviceId: string, { path, verify, currency }: GetAddressOptions) => { + const spendingPath = getBipPathFromString(path); + const stakingPathString = getBipPathString({ + account: spendingPath.account, + chain: StakeChain.stake, + index: STAKING_ADDRESS_INDEX, + }); + const networkParams = getNetworkParameters(currency.id); + + const r = await signerContext(deviceId, signer => + signer.getAddress({ path, stakingPathString, networkParams, verify }), + ); + + const address = TyphonUtils.getAddressFromHex(r.addressHex) as TyphonAddress.BaseAddress; + + return { + address: address.getBech32(), + // Here, we use publicKey hash, as cardano app doesn't export the public key + publicKey: address.paymentCredential.hash, + path, + }; + }; +}; + +export default resolver; diff --git a/libs/ledger-live-common/src/families/cardano/js-broadcast.ts b/libs/coin-modules/coin-cardano/src/js-broadcast.ts similarity index 88% rename from libs/ledger-live-common/src/families/cardano/js-broadcast.ts rename to libs/coin-modules/coin-cardano/src/js-broadcast.ts index f41e778f044..5aec17be97b 100644 --- a/libs/ledger-live-common/src/families/cardano/js-broadcast.ts +++ b/libs/coin-modules/coin-cardano/src/js-broadcast.ts @@ -1,5 +1,5 @@ import type { BroadcastFnSignature } from "@ledgerhq/types-live"; -import { patchOperationWithHash } from "../../operation"; +import { patchOperationWithHash } from "@ledgerhq/coin-framework/operation"; import { submitTransaction } from "./api/submitTransaction"; /** diff --git a/libs/ledger-live-common/src/families/cardano/js-buildTransaction.ts b/libs/coin-modules/coin-cardano/src/js-buildTransaction.ts similarity index 100% rename from libs/ledger-live-common/src/families/cardano/js-buildTransaction.ts rename to libs/coin-modules/coin-cardano/src/js-buildTransaction.ts diff --git a/libs/ledger-live-common/src/families/cardano/js-estimateMaxSpendable.ts b/libs/coin-modules/coin-cardano/src/js-estimateMaxSpendable.ts similarity index 96% rename from libs/ledger-live-common/src/families/cardano/js-estimateMaxSpendable.ts rename to libs/coin-modules/coin-cardano/src/js-estimateMaxSpendable.ts index 6fc5384cfe0..d180fb4ee45 100644 --- a/libs/ledger-live-common/src/families/cardano/js-estimateMaxSpendable.ts +++ b/libs/coin-modules/coin-cardano/src/js-estimateMaxSpendable.ts @@ -1,7 +1,7 @@ import { BigNumber } from "bignumber.js"; import { log } from "@ledgerhq/logs"; import type { AccountLike, Account } from "@ledgerhq/types-live"; -import { getMainAccount } from "../../account"; +import { getMainAccount } from "@ledgerhq/coin-framework/account/index"; import type { CardanoAccount, Transaction } from "./types"; import { createTransaction } from "./js-transaction"; import { diff --git a/libs/ledger-live-common/src/families/cardano/js-getTransactionStatus.ts b/libs/coin-modules/coin-cardano/src/js-getTransactionStatus.ts similarity index 98% rename from libs/ledger-live-common/src/families/cardano/js-getTransactionStatus.ts rename to libs/coin-modules/coin-cardano/src/js-getTransactionStatus.ts index 01d47d4ff28..5191e8523bf 100644 --- a/libs/ledger-live-common/src/families/cardano/js-getTransactionStatus.ts +++ b/libs/coin-modules/coin-cardano/src/js-getTransactionStatus.ts @@ -1,5 +1,6 @@ import { BigNumber } from "bignumber.js"; import { + AccountAwaitingSendPendingOperations, RecipientRequired, FeeNotLoaded, InvalidAddress, @@ -21,7 +22,6 @@ import { CardanoMinAmountError, CardanoNotEnoughFunds, } from "./errors"; -import { AccountAwaitingSendPendingOperations } from "../../errors"; import { getNetworkParameters } from "./networks"; import { decodeTokenAssetId, decodeTokenCurrencyId } from "./buildSubAccounts"; import estimateMaxSpendable from "./js-estimateMaxSpendable"; diff --git a/libs/ledger-live-common/src/families/cardano/js-getTransactionStatus.unit.test.ts b/libs/coin-modules/coin-cardano/src/js-getTransactionStatus.unit.test.ts similarity index 100% rename from libs/ledger-live-common/src/families/cardano/js-getTransactionStatus.unit.test.ts rename to libs/coin-modules/coin-cardano/src/js-getTransactionStatus.unit.test.ts diff --git a/libs/ledger-live-common/src/families/cardano/js-signOperation.ts b/libs/coin-modules/coin-cardano/src/js-signOperation.ts similarity index 59% rename from libs/ledger-live-common/src/families/cardano/js-signOperation.ts rename to libs/coin-modules/coin-cardano/src/js-signOperation.ts index bf2a4146da0..c08dcf1b1d0 100644 --- a/libs/ledger-live-common/src/families/cardano/js-signOperation.ts +++ b/libs/coin-modules/coin-cardano/src/js-signOperation.ts @@ -1,7 +1,19 @@ import { BigNumber } from "bignumber.js"; import { Observable } from "rxjs"; +import { Bip32PublicKey } from "@stricahq/bip32ed25519"; +import { Transaction as TyphonTransaction, types as TyphonTypes } from "@stricahq/typhonjs"; +import ShelleyTypeAddress from "@stricahq/typhonjs/dist/address/ShelleyTypeAddress"; +import { HashType } from "@stricahq/typhonjs/dist/types"; +import { OperationType, SignOperationEvent, SignOperationFnSignature } from "@ledgerhq/types-live"; import { FeeNotLoaded } from "@ledgerhq/errors"; - +import { formatCurrencyUnit } from "@ledgerhq/coin-framework/currencies/index"; +import { encodeOperationId } from "@ledgerhq/coin-framework/operation"; +import { SignerContext } from "@ledgerhq/coin-framework/signer"; +import { MEMO_LABEL } from "./constants"; +import { buildTransaction } from "./js-buildTransaction"; +import { getAccountStakeCredential, getExtendedPublicKeyFromHex, getOperationType } from "./logic"; +import { getNetworkParameters } from "./networks"; +import { CardanoSigner, Witness } from "./signer"; import type { CardanoAccount, CardanoOperation, @@ -9,37 +21,7 @@ import type { CardanoResources, Transaction, } from "./types"; - -import { withDevice } from "../../hw/deviceAccess"; -import { encodeOperationId } from "../../operation"; - -import { buildTransaction } from "./js-buildTransaction"; - -import Ada, { - CertificateType, - Networks, - SignTransactionRequest, - TransactionSigningMode, - TxAuxiliaryDataType, - Witness, -} from "@cardano-foundation/ledgerjs-hw-app-cardano"; -import { types as TyphonTypes, Transaction as TyphonTransaction } from "@stricahq/typhonjs"; -import { Bip32PublicKey } from "@stricahq/bip32ed25519"; -import { getAccountStakeCredential, getExtendedPublicKeyFromHex, getOperationType } from "./logic"; -import ShelleyTypeAddress from "@stricahq/typhonjs/dist/address/ShelleyTypeAddress"; -import { getNetworkParameters } from "./networks"; -import { MEMO_LABEL } from "./constants"; -import { - prepareStakeDelegationCertificate, - prepareLedgerInput, - prepareLedgerOutput, - prepareStakeRegistrationCertificate, - prepareStakeDeRegistrationCertificate, - prepareWithdrawal, -} from "./tx-helpers"; -import { OperationType, SignOperationFnSignature } from "@ledgerhq/types-live"; -import { formatCurrencyUnit } from "../../currencies"; -import { HashType } from "@stricahq/typhonjs/dist/types"; +import typhonSerializer from "./typhonSerializer"; const buildOptimisticOperation = ( account: CardanoAccount, @@ -206,6 +188,56 @@ const buildOptimisticOperation = ( return op; }; +/** + * Sign Transaction with Ledger hardware + */ +const buildSignOperation = + (signerContext: SignerContext): SignOperationFnSignature => + ({ account, deviceId, transaction }): Observable => + new Observable(o => { + async function main() { + o.next({ type: "device-signature-requested" }); + + if (!transaction.fees) { + throw new FeeNotLoaded(); + } + + const unsignedTransaction = await buildTransaction(account as CardanoAccount, transaction); + const signerTransaction = typhonSerializer(unsignedTransaction, account.index); + + const networkParams = getNetworkParameters(account.currency.id); + const signedData = await signerContext(deviceId, signer => + signer.sign({ + transaction: signerTransaction, + networkParams, + }), + ); + + const accountPubKey = getExtendedPublicKeyFromHex(account.xpub as string); + const signed = signTx(unsignedTransaction, accountPubKey, signedData.witnesses); + + o.next({ type: "device-signature-granted" }); + + const operation = buildOptimisticOperation( + account as CardanoAccount, + unsignedTransaction, + transaction, + ); + + o.next({ + type: "signed", + signedOperation: { + operation, + signature: signed.payload, + }, + }); + } + main().then( + () => o.complete(), + e => o.error(e), + ); + }); + /** * Adds signatures to unsigned transaction */ @@ -227,112 +259,4 @@ const signTx = ( return unsignedTransaction.buildTransaction(); }; -/** - * Sign Transaction with Ledger hardware - */ -const signOperation: SignOperationFnSignature = ({ account, deviceId, transaction }) => - withDevice(deviceId)( - transport => - new Observable(o => { - async function main() { - o.next({ type: "device-signature-requested" }); - - if (!transaction.fees) { - throw new FeeNotLoaded(); - } - - const unsignedTransaction = await buildTransaction( - account as CardanoAccount, - transaction, - ); - - const accountPubKey = getExtendedPublicKeyFromHex(account.xpub as string); - - const rawInputs = unsignedTransaction.getInputs(); - const ledgerAppInputs = rawInputs.map(i => prepareLedgerInput(i, account.index)); - - const rawOutptus = unsignedTransaction.getOutputs(); - const ledgerAppOutputs = rawOutptus.map(o => prepareLedgerOutput(o, account.index)); - - const rawCertificates = unsignedTransaction.getCertificates(); - const ledgerCertificates = rawCertificates.map(rcert => { - if (rcert.certType === (CertificateType.STAKE_REGISTRATION as number)) { - return prepareStakeRegistrationCertificate( - rcert as TyphonTypes.StakeRegistrationCertificate, - ); - } else if (rcert.certType === (CertificateType.STAKE_DELEGATION as number)) { - return prepareStakeDelegationCertificate( - rcert as TyphonTypes.StakeDelegationCertificate, - ); - } else if (rcert.certType === (CertificateType.STAKE_DEREGISTRATION as number)) { - return prepareStakeDeRegistrationCertificate( - rcert as TyphonTypes.StakeDeRegistrationCertificate, - ); - } else { - throw new Error("Invalid Certificate type"); - } - }); - - const rawWithdrawals = unsignedTransaction.getWithdrawals(); - const ledgerWithdrawals = rawWithdrawals.map(prepareWithdrawal); - - const auxiliaryDataHashHex = unsignedTransaction.getAuxiliaryDataHashHex(); - - const networkParams = getNetworkParameters(account.currency.id); - const network = - networkParams.networkId === Networks.Mainnet.networkId - ? Networks.Mainnet - : Networks.Testnet; - - const trxOptions: SignTransactionRequest = { - signingMode: TransactionSigningMode.ORDINARY_TRANSACTION, - tx: { - network, - inputs: ledgerAppInputs, - outputs: ledgerAppOutputs, - certificates: ledgerCertificates, - withdrawals: ledgerWithdrawals, - fee: unsignedTransaction.getFee().toString(), - ttl: unsignedTransaction.getTTL()?.toString(), - validityIntervalStart: null, - auxiliaryData: auxiliaryDataHashHex - ? { - type: TxAuxiliaryDataType.ARBITRARY_HASH, - params: { - hashHex: auxiliaryDataHashHex, - }, - } - : null, - }, - additionalWitnessPaths: [], - }; - - // Sign by device - const appAda = new Ada(transport); - const r = await appAda.signTransaction(trxOptions); - const signed = signTx(unsignedTransaction, accountPubKey, r.witnesses); - - o.next({ type: "device-signature-granted" }); - - const operation = buildOptimisticOperation( - account as CardanoAccount, - unsignedTransaction, - transaction, - ); - - o.next({ - type: "signed", - signedOperation: { - operation, - signature: signed.payload, - }, - }); - } - main().then( - () => o.complete(), - e => o.error(e), - ); - }), - ); - -export default signOperation; +export default buildSignOperation; diff --git a/libs/ledger-live-common/src/families/cardano/js-synchronisation.ts b/libs/coin-modules/coin-cardano/src/js-synchronisation.ts similarity index 87% rename from libs/ledger-live-common/src/families/cardano/js-synchronisation.ts rename to libs/coin-modules/coin-cardano/src/js-synchronisation.ts index e43cf563511..8f153f7ad65 100644 --- a/libs/ledger-live-common/src/families/cardano/js-synchronisation.ts +++ b/libs/coin-modules/coin-cardano/src/js-synchronisation.ts @@ -1,17 +1,19 @@ +import BigNumber from "bignumber.js"; +import uniqBy from "lodash/uniqBy"; +import { utils as TyphonUtils } from "@stricahq/typhonjs"; +import { calculateMinUtxoAmount } from "@stricahq/typhonjs/dist/utils/utils"; +import type { Operation, OperationType, TokenAccount } from "@ledgerhq/types-live"; +import { listTokensForCryptoCurrency } from "@ledgerhq/cryptoassets"; import { AccountShapeInfo, GetAccountShape, - makeScanAccounts, mergeOps, -} from "../../bridge/jsHelpers"; -import { makeSync } from "../../bridge/jsHelpers"; +} from "@ledgerhq/coin-framework/bridge/jsHelpers"; import { encodeAccountId } from "@ledgerhq/coin-framework/account/index"; import { inferSubOperations } from "@ledgerhq/coin-framework/serialization/index"; - -import BigNumber from "bignumber.js"; -import Ada, { ExtendedPublicKey } from "@cardano-foundation/ledgerjs-hw-app-cardano"; -import { str_to_path } from "@cardano-foundation/ledgerjs-hw-app-cardano/dist/utils/address"; -import { utils as TyphonUtils } from "@stricahq/typhonjs"; +import { encodeOperationId } from "@ledgerhq/coin-framework/operation"; +import { SignerContext } from "@ledgerhq/coin-framework/signer"; +import { formatCurrencyUnit } from "@ledgerhq/coin-framework/currencies/index"; import { APITransaction, HashType } from "./api/api-types"; import { CardanoAccount, @@ -20,7 +22,6 @@ import { CardanoOutput, PaymentCredential, ProtocolParams, - Transaction, StakeCredential, } from "./types"; import { @@ -33,23 +34,12 @@ import { isHexString, mergeTokens, } from "./logic"; -import { encodeOperationId } from "../../operation"; import { getNetworkParameters } from "./networks"; import { getNetworkInfo } from "./api/getNetworkInfo"; -import uniqBy from "lodash/uniqBy"; -import postSyncPatch from "./postSyncPatch"; import { getTransactions } from "./api/getTransactions"; -import type { - AccountBridge, - CurrencyBridge, - Operation, - OperationType, - TokenAccount, -} from "@ledgerhq/types-live"; import { buildSubAccounts } from "./buildSubAccounts"; -import { calculateMinUtxoAmount } from "@stricahq/typhonjs/dist/utils/utils"; -import { formatCurrencyUnit, listTokensForCryptoCurrency } from "../../currencies"; import { getDelegationInfo } from "./api/getDelegationInfo"; +import { CardanoSigner } from "./signer"; function mapTxToAccountOperation( tx: APITransaction, @@ -201,13 +191,8 @@ function prepareUtxos( return utxos; } -export type SignerContext = ( - deviceId: string, - fn: (signer: Ada) => Promise, -) => Promise; - export const makeGetAccountShape = - (signerContext: SignerContext): GetAccountShape => + (signerContext: SignerContext): GetAccountShape => async (info, { blacklistedTokenIds }) => { const { currency, @@ -222,19 +207,19 @@ export const makeGetAccountShape = const accountPath = `${rootPath}/${accountIndex}'`; const paramXpub = initialAccount?.xpub; - let extendedPubKeyRes; + let xpub; if (!paramXpub) { if (deviceId === undefined || deviceId === null) { // deviceId not provided throw new Error("deviceId required to generate the xpub"); } - extendedPubKeyRes = await signerContext(deviceId, signer => - signer.getExtendedPublicKey({ - path: str_to_path(accountPath), - }), + const extendedPubKeyRes = await signerContext(deviceId, signer => + signer.getPublicKey(accountPath), ); + xpub = `${extendedPubKeyRes.publicKeyHex}${extendedPubKeyRes.chainCodeHex}`; + } else { + xpub = paramXpub; } - const xpub = paramXpub || `${extendedPubKeyRes.publicKeyHex}${extendedPubKeyRes.chainCodeHex}`; const accountId = encodeAccountId({ type: "js", version: "2", @@ -366,12 +351,3 @@ export const makeGetAccountShape = }, }; }; - -export const scanAccounts = (signerContext: SignerContext): CurrencyBridge["scanAccounts"] => - makeScanAccounts({ getAccountShape: makeGetAccountShape(signerContext) }); - -export const sync = (signerContext: SignerContext): AccountBridge["sync"] => - makeSync({ - getAccountShape: makeGetAccountShape(signerContext), - postSync: postSyncPatch, - }); diff --git a/libs/ledger-live-common/src/families/cardano/js-synchronisation.unit.test.ts b/libs/coin-modules/coin-cardano/src/js-synchronisation.unit.test.ts similarity index 81% rename from libs/ledger-live-common/src/families/cardano/js-synchronisation.unit.test.ts rename to libs/coin-modules/coin-cardano/src/js-synchronisation.unit.test.ts index a2adb5c8a75..1b24f140107 100644 --- a/libs/ledger-live-common/src/families/cardano/js-synchronisation.unit.test.ts +++ b/libs/coin-modules/coin-cardano/src/js-synchronisation.unit.test.ts @@ -1,30 +1,37 @@ import BigNumber from "bignumber.js"; -import { AccountShapeInfo, GetAccountShape } from "../../bridge/jsHelpers"; +import { AccountShapeInfo, GetAccountShape } from "@ledgerhq/coin-framework/bridge/jsHelpers"; import { APINetworkInfo } from "./api/api-types"; import { getDelegationInfo } from "./api/getDelegationInfo"; import { getNetworkInfo } from "./api/getNetworkInfo"; import { getTransactions } from "./api/getTransactions"; import { buildSubAccounts } from "./buildSubAccounts"; -import { makeGetAccountShape, SignerContext } from "./js-synchronisation"; -import { BipPath, CardanoAccount, CardanoDelegation } from "./types"; +import { makeGetAccountShape } from "./js-synchronisation"; +import { BipPath, CardanoAccount, CardanoDelegation, PaymentCredential } from "./types"; +import type { SignerContext } from "@ledgerhq/coin-framework/signer"; +import { CardanoSigner } from "./signer"; + jest.mock("./buildSubAccounts"); jest.mock("./api/getTransactions"); jest.mock("./api/getNetworkInfo"); jest.mock("./api/getDelegationInfo"); describe("makeGetAccountShape", () => { - let signerContext: SignerContext; + let signerContext: SignerContext; let shape: GetAccountShape; let accountShapeInfo: AccountShapeInfo; - let getTransactionsMock; + let getTransactionsMock: jest.MaybeMockedDeep; beforeEach(() => { const pubKeyMock = { - extendedPubKeyHex: "extendedPubKeyHex", chainCodeHex: "chainCodeHex", publicKeyHex: "publicKeyHex", }; - signerContext = () => Promise.resolve(pubKeyMock); + const fakeSigner: CardanoSigner = { + getAddress: jest.fn(), + sign: jest.fn(), + getPublicKey: jest.fn().mockResolvedValue(pubKeyMock), + }; + signerContext = (_: string, fn: (signer: CardanoSigner) => Promise) => fn(fakeSigner); shape = makeGetAccountShape(signerContext); accountShapeInfo = { currency: { id: "cardano" } as any, @@ -93,7 +100,12 @@ describe("makeGetAccountShape", () => { externalCredentials: [ { path: { index: 0 } as BipPath, networkId: "id", isUsed: false, key: "" }, ], - internalCredentials: ["cred"], + internalCredentials: [ + { + isUsed: true, + key: "cred", + } as PaymentCredential, + ], blockHeight: 0, }), ); diff --git a/libs/ledger-live-common/src/families/cardano/js-transaction.ts b/libs/coin-modules/coin-cardano/src/js-transaction.ts similarity index 100% rename from libs/ledger-live-common/src/families/cardano/js-transaction.ts rename to libs/coin-modules/coin-cardano/src/js-transaction.ts diff --git a/libs/coin-modules/coin-cardano/src/logic.ts b/libs/coin-modules/coin-cardano/src/logic.ts new file mode 100644 index 00000000000..1047943985c --- /dev/null +++ b/libs/coin-modules/coin-cardano/src/logic.ts @@ -0,0 +1,330 @@ +import { + CARDANO_COIN_TYPE, + CARDANO_PURPOSE, + MEMO_LABEL, + STAKING_ADDRESS_INDEX, + TTL_GAP, +} from "./constants"; + +import { + utils as TyphonUtils, + types as TyphonTypes, + address as TyphonAddress, +} from "@stricahq/typhonjs"; + +import { + CardanoAccount, + BipPath, + PaymentChain, + PaymentCredential, + StakeChain, + StakeCredential, + Token, +} from "./types"; +import { Bip32PublicKey } from "@stricahq/bip32ed25519"; +import BigNumber from "bignumber.js"; +import { getNetworkParameters } from "./networks"; +import groupBy from "lodash/groupBy"; +import { APITransaction } from "./api/api-types"; +import { getCryptoCurrencyById } from "@ledgerhq/cryptoassets"; +import ShelleyTypeAddress from "@stricahq/typhonjs/dist/address/ShelleyTypeAddress"; +import type { OperationType } from "@ledgerhq/types-live"; +import type { CryptoCurrency } from "@ledgerhq/types-cryptoassets"; +import bech32 from "bech32"; + +/** + * returns BipPath object with account, chain and index field for cardano + * + * @param {string} path + */ +export function getBipPathFromString(path: string): BipPath { + const regEx = new RegExp(`^${CARDANO_PURPOSE}'/${CARDANO_COIN_TYPE}'/(\\d*)'/([012])/(\\d*)`); + const result = path.match(regEx); + if (result == null) { + throw new Error("Invalid derivation path"); + } + return getBipPath({ + account: parseInt(result[1]), + chain: parseInt(result[2]), + index: parseInt(result[3]), + }); +} + +/** + * + * @returns true if the account can stake, false otherwise + */ +export function canStake(account: CardanoAccount): boolean { + return account.balance.gt(0); +} + +/** + * + * @returns true if account is staked, false otherwise + */ +export function isAlreadyStaking(account: CardanoAccount): boolean { + return !!account.cardanoResources?.delegation?.poolId; +} + +/** + * returns complete bipPath with purpose, coin, account, chain and index for cardano + */ +export function getBipPath({ + account, + chain, + index, +}: { + account: number; + chain: PaymentChain | StakeChain; + index: number; +}): BipPath { + return { + purpose: CARDANO_PURPOSE, + coin: CARDANO_COIN_TYPE, + account, + chain, + index, + }; +} + +/** + * returns bipPathString from account, chain and index for cardano + */ +export function getBipPathString({ + account, + chain, + index, +}: { + account: number; + chain: number; + index: number; +}): string { + return `${CARDANO_PURPOSE}'/${CARDANO_COIN_TYPE}'/${account}'/${chain}/${index}`; +} + +export function getExtendedPublicKeyFromHex(keyHex: string): Bip32PublicKey { + return Bip32PublicKey.fromBytes(Buffer.from(keyHex, "hex")); +} + +export function getCredentialKey( + accountKey: Bip32PublicKey, + path: BipPath, +): { key: string; path: BipPath } { + const keyBytes = accountKey.derive(path.chain).derive(path.index).toPublicKey().hash(); + const pubKeyHex = keyBytes.toString("hex"); + return { + key: pubKeyHex, + path, + }; +} + +/** + * returns cardano base address by paymentKey and stakeKey + */ +export function getBaseAddress({ + networkId, + paymentCred, + stakeCred, +}: { + networkId: number; + paymentCred: PaymentCredential; + stakeCred: StakeCredential; +}): TyphonAddress.BaseAddress { + const paymentCredential: TyphonTypes.HashCredential = { + hash: paymentCred.key, + type: TyphonTypes.HashType.ADDRESS, + bipPath: paymentCred.path, + }; + + const stakeCredential: TyphonTypes.HashCredential = { + hash: stakeCred.key, + type: TyphonTypes.HashType.ADDRESS, + bipPath: stakeCred.path, + }; + return new TyphonAddress.BaseAddress(networkId, paymentCredential, stakeCredential); +} + +/** + * Returns true if address is a valid + * + * @param {string} address + */ +export const isValidAddress = (address: string, networkId: number): boolean => { + if (!address) return false; + + try { + const cardanoAddress = TyphonUtils.getAddressFromBech32(address); + if (cardanoAddress instanceof ShelleyTypeAddress) { + const addressNetworkId = Number(cardanoAddress.getHex().toLowerCase().charAt(1)); + if (addressNetworkId !== networkId) { + return false; + } + } + } catch (error) { + return false; + } + return true; +}; + +export const getAbsoluteSlot = function (networkName: string, time: Date): number { + const networkParams = getNetworkParameters(networkName); + const byronChainEndSlots = networkParams.shelleyStartEpoch * networkParams.byronSlotsPerEpoch; + const byronChainEndTime = byronChainEndSlots * networkParams.byronSlotDuration; + + const shelleyChainTime = time.getTime() - networkParams.chainStartTime - byronChainEndTime; + const shelleyChainSlots = Math.floor(shelleyChainTime / networkParams.shelleySlotDuration); + return byronChainEndSlots + shelleyChainSlots; +}; + +/** + * Returns the time to live for transaction + * + * @returns {number} + */ +export function getTTL(networkName: string): number { + return getAbsoluteSlot(networkName, new Date()) + TTL_GAP; +} + +export function getEpoch(networkName: string, time: Date): number { + const networkParams = getNetworkParameters(networkName); + const chainTime = time.getTime() - networkParams.chainStartTime; + const epoch = Math.floor( + chainTime / (networkParams.shelleySlotsPerEpoch * networkParams.shelleySlotDuration), + ); + return epoch; +} + +export function mergeTokens(tokens: Array): Array { + return Object.values(groupBy(tokens, t => `${t.policyId}${t.assetName}`)).map(similarTokens => ({ + policyId: similarTokens[0].policyId, + assetName: similarTokens[0].assetName, + amount: similarTokens.reduce((total, token) => total.plus(token.amount), new BigNumber(0)), + })); +} + +/** + * @param { Array } b + * @param { Array } a + * @returns a - b + */ +export function getTokenDiff( + a: Array, + b: Array, +): Array { + return mergeTokens(a.concat(b.map(t => ({ ...t, amount: t.amount.negated() })))).filter( + t => !t.amount.eq(0), + ); +} + +export function getAccountStakeCredential(xpub: string, index: number): StakeCredential { + const accountXPubKey = getExtendedPublicKeyFromHex(xpub); + const keyPath = getCredentialKey( + accountXPubKey, + getBipPath({ + account: index, + chain: StakeChain.stake, + index: STAKING_ADDRESS_INDEX, + }), + ); + return { + key: keyPath.key, + path: keyPath.path, + }; +} + +export function getOperationType({ + valueChange, + fees, +}: { + valueChange: BigNumber; + fees: BigNumber; +}): OperationType { + return valueChange.isNegative() + ? valueChange.absoluteValue().eq(fees) + ? "FEES" + : "OUT" + : valueChange.isPositive() + ? "IN" + : "NONE"; +} + +export function isTestnet(currency: CryptoCurrency): boolean { + return getCryptoCurrencyById(currency.id).isTestnetFor ? true : false; +} + +export function getAccountChange( + t: APITransaction, + accountCredentialsMap: Record, +): { ada: BigNumber; tokens: Array } { + let accountInputAda = new BigNumber(0); + const accountInputTokens: Array = []; + t.inputs.forEach(i => { + if (accountCredentialsMap[i.paymentKey]) { + accountInputAda = accountInputAda.plus(i.value); + accountInputTokens.push( + ...i.tokens.map(t => ({ + assetName: t.assetName, + policyId: t.policyId, + amount: new BigNumber(t.value), + })), + ); + } + }); + + let accountOutputAda = new BigNumber(0); + const accountOutputTokens: Array = []; + t.outputs.forEach(o => { + if (accountCredentialsMap[o.paymentKey]) { + accountOutputAda = accountOutputAda.plus(o.value); + accountOutputTokens.push( + ...o.tokens.map(t => ({ + assetName: t.assetName, + policyId: t.policyId, + amount: new BigNumber(t.value), + })), + ); + } + }); + + return { + ada: accountOutputAda.minus(accountInputAda), + tokens: getTokenDiff(accountOutputTokens, accountInputTokens), + }; +} + +export function getMemoFromTx(tx: APITransaction): string | undefined { + let memo; + const metadataValue = tx.metadata?.data.find(m => m.label === MEMO_LABEL.toString()); + if (metadataValue) { + try { + const parsedValue = JSON.parse(metadataValue.value); + if (parsedValue.msg && Array.isArray(parsedValue.msg) && parsedValue.msg.length) { + memo = parsedValue.msg.join(", "); + } + // eslint-disable-next-line no-empty + } catch (e) {} + } + return memo; +} + +export function isHexString(value: string): boolean { + const regExp = /^[0-9a-fA-F]+$/; + return regExp.test(value); +} + +export function decodeTokenName(assetName: string): string { + if (assetName.length > 0) { + const bytes = [...Buffer.from(assetName, "hex")]; + if (bytes.filter(byte => byte <= 32 || byte >= 127).length === 0) { + return String.fromCharCode(...bytes); + } + } + return assetName; +} + +export function getBech32PoolId(poolId: string, networkName: string): string { + const networkParams = getNetworkParameters(networkName); + const words = bech32.toWords(Buffer.from(poolId, "hex")); + const encoded = bech32.encode(networkParams.poolIdPrefix, words, 1000); + return encoded; +} diff --git a/libs/ledger-live-common/src/families/cardano/logic.unit.test.ts b/libs/coin-modules/coin-cardano/src/logic.unit.test.ts similarity index 88% rename from libs/ledger-live-common/src/families/cardano/logic.unit.test.ts rename to libs/coin-modules/coin-cardano/src/logic.unit.test.ts index ed3ba1b935e..16282417636 100644 --- a/libs/ledger-live-common/src/families/cardano/logic.unit.test.ts +++ b/libs/coin-modules/coin-cardano/src/logic.unit.test.ts @@ -3,11 +3,6 @@ import { canStake, isAlreadyStaking } from "./logic"; import { CardanoAccount } from "./types"; describe("canStake", () => { - it("should return false when acc not present", () => { - const noResourcesAcc = {} as CardanoAccount; - expect(canStake(noResourcesAcc)).toEqual(false); - }); - it("should return false when acc has no funds", () => { const accWithNoFunds = { balance: new BigNumber(0), diff --git a/libs/ledger-live-common/src/families/cardano/networks.ts b/libs/coin-modules/coin-cardano/src/networks.ts similarity index 100% rename from libs/ledger-live-common/src/families/cardano/networks.ts rename to libs/coin-modules/coin-cardano/src/networks.ts diff --git a/libs/ledger-live-common/src/families/cardano/postSyncPatch.ts b/libs/coin-modules/coin-cardano/src/postSyncPatch.ts similarity index 100% rename from libs/ledger-live-common/src/families/cardano/postSyncPatch.ts rename to libs/coin-modules/coin-cardano/src/postSyncPatch.ts diff --git a/libs/ledger-live-common/src/families/cardano/serialization.ts b/libs/coin-modules/coin-cardano/src/serialization.ts similarity index 100% rename from libs/ledger-live-common/src/families/cardano/serialization.ts rename to libs/coin-modules/coin-cardano/src/serialization.ts diff --git a/libs/coin-modules/coin-cardano/src/signer.ts b/libs/coin-modules/coin-cardano/src/signer.ts new file mode 100644 index 00000000000..6174c333470 --- /dev/null +++ b/libs/coin-modules/coin-cardano/src/signer.ts @@ -0,0 +1,98 @@ +import { CardanoLikeNetworkParameters } from "./types"; + +export type CardanoAddress = { + addressHex: string; +}; +// Coming from @cardano-foundation/ledgerjs-hw-app-cardano code (type SignedTransactionData) +type BIP32Path = Array; +export type Witness = { + path: BIP32Path; + witnessSignatureHex: string; +}; +export type CardanoSignature = { + txHashHex: string; + witnesses: Array; +}; +// Coming from @cardano-foundation/ledgerjs-hw-app-cardano code (type ExtendedPublicKey) +export type CardanoExtendedPublicKey = { + publicKeyHex: string; + chainCodeHex: string; +}; +export type GetAddressRequest = { + path: string; + stakingPathString: string; + networkParams: CardanoLikeNetworkParameters; + verify?: boolean; +}; + +export type SignerTxInput = { + txHashHex: string; + outputIndex: number; + path: string | null; +}; +export type SignerTxOutput = { + amount: string; + destination: + | { + isDeviceOwnedAddress: false; + params: { + addressHex: string; + }; + } + | { + isDeviceOwnedAddress: true; + params: { + spendingPath: string; + stakingPath: string; + }; + }; + tokenBundle: Array<{ + policyIdHex: string; + tokens: Array<{ + assetNameHex: string; + amount: string; + }>; + }>; +}; +export type SignerTxCertificate = + | { + type: "REGISTRATION" | "DEREGISTRATION"; + params: { + stakeCredential: { + keyPath: string; + }; + }; + } + | { + type: "DELEGATION"; + params: { + stakeCredential: { + keyPath: string; + }; + poolKeyHashHex: string; + }; + }; +export type SignerTxWithdrawal = { + stakeCredential: { + keyPath: string; + }; + amount: string; +}; +export type SignerTransaction = { + inputs: Array; + outputs: Array; + certificates: Array; + withdrawals: Array; + fee: string; + ttl?: string; + auxiliaryData: string | null; +}; +export type CardanoSignRequest = { + transaction: SignerTransaction; + networkParams: CardanoLikeNetworkParameters; +}; +export interface CardanoSigner { + getAddress(addressRequest: GetAddressRequest): Promise; + getPublicKey(accountPath: string): Promise; + sign(signRequest: CardanoSignRequest): Promise; +} diff --git a/libs/ledger-live-common/src/families/cardano/specs.ts b/libs/coin-modules/coin-cardano/src/specs.ts similarity index 97% rename from libs/ledger-live-common/src/families/cardano/specs.ts rename to libs/coin-modules/coin-cardano/src/specs.ts index 6dd79e9eaaa..293bc1441f5 100644 --- a/libs/ledger-live-common/src/families/cardano/specs.ts +++ b/libs/coin-modules/coin-cardano/src/specs.ts @@ -1,14 +1,14 @@ import expect from "expect"; -import type { AppSpec } from "../../bot/types"; +import type { AppSpec } from "@ledgerhq/coin-framework/bot/types"; import type { CardanoAccount, CardanoOperationExtra, CardanoResources, Transaction } from "./types"; -import { botTest, genericTestDestination, pickSiblings } from "../../bot/specs"; +import { botTest, genericTestDestination, pickSiblings } from "@ledgerhq/coin-framework/bot/specs"; import { getCryptoCurrencyById } from "@ledgerhq/cryptoassets"; import { DeviceModelId } from "@ledgerhq/devices"; import BigNumber from "bignumber.js"; import invariant from "invariant"; import { utils as TyphonUtils } from "@stricahq/typhonjs"; import { mergeTokens } from "./logic"; -import { formatCurrencyUnit, parseCurrencyUnit } from "../../currencies"; +import { formatCurrencyUnit, parseCurrencyUnit } from "@ledgerhq/coin-framework/currencies/index"; import { SubAccount } from "@ledgerhq/types-live"; import { acceptTransaction } from "./speculos-deviceActions"; diff --git a/libs/ledger-live-common/src/families/cardano/speculos-deviceActions.ts b/libs/coin-modules/coin-cardano/src/speculos-deviceActions.ts similarity index 93% rename from libs/ledger-live-common/src/families/cardano/speculos-deviceActions.ts rename to libs/coin-modules/coin-cardano/src/speculos-deviceActions.ts index 0754edb0835..4c4ae35b4b0 100644 --- a/libs/ledger-live-common/src/families/cardano/speculos-deviceActions.ts +++ b/libs/coin-modules/coin-cardano/src/speculos-deviceActions.ts @@ -1,7 +1,11 @@ -import type { DeviceAction } from "../../bot/types"; +import type { DeviceAction } from "@ledgerhq/coin-framework/bot/types"; import type { Transaction } from "./types"; import { getAccountStakeCredential, getBipPathString } from "./logic"; -import { deviceActionFlow, formatDeviceAmount, SpeculosButton } from "../../bot/specs"; +import { + deviceActionFlow, + formatDeviceAmount, + SpeculosButton, +} from "@ledgerhq/coin-framework/bot/specs"; export const acceptTransaction: DeviceAction = deviceActionFlow({ steps: [ diff --git a/libs/ledger-live-common/src/families/cardano/transaction.ts b/libs/coin-modules/coin-cardano/src/transaction.ts similarity index 91% rename from libs/ledger-live-common/src/families/cardano/transaction.ts rename to libs/coin-modules/coin-cardano/src/transaction.ts index c6c1d3931c8..319c3430377 100644 --- a/libs/ledger-live-common/src/families/cardano/transaction.ts +++ b/libs/coin-modules/coin-cardano/src/transaction.ts @@ -7,8 +7,8 @@ import { toTransactionCommonRaw, toTransactionStatusRawCommon as toTransactionStatusRaw, } from "@ledgerhq/coin-framework/serialization"; -import { getAccountCurrency } from "../../account"; -import { formatCurrencyUnit } from "../../currencies"; +import { getAccountCurrency } from "@ledgerhq/coin-framework/account/index"; +import { formatCurrencyUnit } from "@ledgerhq/coin-framework/currencies/index"; import type { Account } from "@ledgerhq/types-live"; export const formatTransaction = ( diff --git a/libs/coin-modules/coin-cardano/src/types.ts b/libs/coin-modules/coin-cardano/src/types.ts new file mode 100644 index 00000000000..87651df4cd6 --- /dev/null +++ b/libs/coin-modules/coin-cardano/src/types.ts @@ -0,0 +1,227 @@ +import type { BigNumber } from "bignumber.js"; +import { types as TyphonTypes } from "@stricahq/typhonjs"; +import type { + Account, + AccountRaw, + Operation, + OperationRaw, + TransactionCommon, + TransactionCommonRaw, + TransactionStatusCommon, + TransactionStatusCommonRaw, +} from "@ledgerhq/types-live"; + +export enum PaymentChain { + external = 0, + internal = 1, +} + +export enum StakeChain { + stake = 2, +} + +export enum NetworkId { + testnet = 0, + mainnet = 1, +} + +export type BipPath = { + purpose: number; + coin: number; + account: number; + chain: PaymentChain | StakeChain; + index: number; +}; + +export type BipPathRaw = { + purpose: number; + coin: number; + account: number; + chain: PaymentChain | StakeChain; + index: number; +}; + +export type Token = { + assetName: string; + policyId: string; + amount: BigNumber; +}; + +export type TokenRaw = { + assetName: string; + policyId: string; + amount: string; +}; + +export type PaymentKeyPath = { + key: string; + path: BipPath; +}; + +export type PaymentCredential = { + isUsed: boolean; + key: string; + path: BipPath; +}; + +export type PaymentCredentialRaw = { + isUsed: boolean; + key: string; + path: BipPathRaw; +}; + +export type StakeCredential = { + key: string; + path: BipPath; +}; + +export type CardanoOutput = { + hash: string; + index: number; + address: string; + amount: BigNumber; + tokens: Array; + paymentCredential: { + key: string; + path: BipPath; + }; +}; + +export type CardanoOutputRaw = { + hash: string; + index: number; + address: string; + amount: string; + tokens: Array; + paymentCredential: { + key: string; + path: BipPath; + }; +}; + +export type ProtocolParams = { + minFeeA: string; + minFeeB: string; + stakeKeyDeposit: string; + lovelacePerUtxoWord: string; + collateralPercent: string; + priceSteps: string; + priceMem: string; + languageView: TyphonTypes.LanguageView; +}; + +export type ProtocolParamsRaw = { + minFeeA: string; + minFeeB: string; + stakeKeyDeposit: string; + lovelacePerUtxoWord: string; + collateralPercent: string; + priceSteps: string; + priceMem: string; + // TyphonTypes.LanguageView is already a raw type + languageView: TyphonTypes.LanguageView; +}; + +export type CardanoDelegation = { + status: boolean; + poolId: string; + ticker: string; + name: string; + rewards: BigNumber; +}; + +export type CardanoDelegationRaw = { + status: boolean; + poolId: string; + ticker: string; + name: string; + rewards: string; +}; + +/** + * Cardano account resources + */ +export type CardanoResources = { + externalCredentials: Array; + internalCredentials: Array; + delegation: CardanoDelegation | undefined; + utxos: Array; + protocolParams: ProtocolParams; +}; + +/** + * Cardano account resources from raw JSON + */ +export type CardanoResourcesRaw = { + externalCredentials: Array; + internalCredentials: Array; + delegation: CardanoDelegationRaw | undefined; + utxos: Array; + protocolParams: ProtocolParamsRaw; +}; + +export type CardanoOperationMode = "send" | "delegate" | "undelegate"; + +/** + * Cardano transaction + */ +export type Transaction = TransactionCommon & { + mode: CardanoOperationMode; + family: "cardano"; + fees?: BigNumber; + memo?: string; + poolId: string | undefined; + // add here all transaction-specific fields if you implement other modes than "send" +}; + +/** + * Cardano transaction from a raw JSON + */ +export type TransactionRaw = TransactionCommonRaw & { + family: "cardano"; + mode: CardanoOperationMode; + fees?: string; + memo?: string; + poolId: string | undefined; + // also the transaction fields as raw JSON data +}; + +export type CardanoLikeNetworkParameters = { + identifier: string; + networkId: number; + chainStartTime: number; + byronSlotDuration: number; + byronSlotsPerEpoch: number; + shelleyStartEpoch: number; + shelleySlotDuration: number; + shelleySlotsPerEpoch: number; + addressPrefix: string; + poolIdPrefix: string; +}; + +/** + * Cardano currency data that will be preloaded. + */ +export type CardanoPreloadData = { + protocolParams: ProtocolParams; +}; + +export type CardanoAccount = Account & { cardanoResources: CardanoResources }; + +export type CardanoAccountRaw = AccountRaw & { + cardanoResources: CardanoResourcesRaw; +}; + +export type TransactionStatus = TransactionStatusCommon; + +export type TransactionStatusRaw = TransactionStatusCommonRaw; + +export type CardanoOperation = Operation; +export type CardanoOperationRaw = OperationRaw; + +export type CardanoOperationExtra = { + memo?: string; + deposit?: string; + refund?: string; + rewards?: string; +}; diff --git a/libs/coin-modules/coin-cardano/src/typhonSerializer.ts b/libs/coin-modules/coin-cardano/src/typhonSerializer.ts new file mode 100644 index 00000000000..e6e58cc05f4 --- /dev/null +++ b/libs/coin-modules/coin-cardano/src/typhonSerializer.ts @@ -0,0 +1,259 @@ +import { + types as TyphonTypes, + address as TyphonAddress, + Transaction as TyphonTransaction, +} from "@stricahq/typhonjs"; +import groupBy from "lodash/groupBy"; +import { getBipPathString } from "./logic"; +import { CertificateType } from "@stricahq/typhonjs/dist/types"; +import { SignerTxCertificate, SignerTxInput, SignerTxOutput, SignerTxWithdrawal } from "./signer"; + +/** + * Convert StricaTypes Transaction into a simpler types. + * We are keeping the minimal necessary for a signer. + */ +export default function (transaction: TyphonTransaction, accountIndex: number) { + const ledgerAppInputs = transaction.getInputs().map(prepareLedgerInput(accountIndex)); + const ledgerAppOutputs = transaction.getOutputs().map(prepareLedgerOutput(accountIndex)); + const ledgerCertificates = transaction.getCertificates().map(prepareCertificate); + const ledgerWithdrawals = transaction.getWithdrawals().map(prepareWithdrawal); + const auxiliaryDataHashHex = transaction.getAuxiliaryDataHashHex(); + + return { + inputs: ledgerAppInputs, + outputs: ledgerAppOutputs, + certificates: ledgerCertificates, + withdrawals: ledgerWithdrawals, + fee: transaction.getFee().toString(), + ttl: transaction.getTTL()?.toString(), + validityIntervalStart: null, + auxiliaryData: auxiliaryDataHashHex ?? null, + }; +} + +/** + * returns the formatted transactionInput for ledger cardano app + * + * @param {TyphonTypes.Input} input + * @param {number} accountIndex + * @returns {TxInput} + */ +const prepareLedgerInput = + (accountIndex: number) => + (input: TyphonTypes.Input): SignerTxInput => { + const paymentKeyPath = + input.address.paymentCredential.type === TyphonTypes.HashType.ADDRESS + ? input.address.paymentCredential.bipPath + : undefined; + return { + txHashHex: input.txId, + outputIndex: input.index, + path: paymentKeyPath + ? getBipPathString({ + account: accountIndex, + chain: paymentKeyPath.chain, + index: paymentKeyPath.index, + }) + : null, + }; + }; + +/** + * returns the formatted transactionOutput for ledger cardano app + * + * @param output + * @param accountIndex + * @returns {TxOutput} + */ +const prepareLedgerOutput = + (accountIndex: number) => + (output: TyphonTypes.Output): SignerTxOutput => { + const isByronAddress = output.address instanceof TyphonAddress.ByronAddress; + let isDeviceOwnedAddress = false; + + if (!isByronAddress) { + const address = output.address as TyphonTypes.ShelleyAddress; + isDeviceOwnedAddress = + address.paymentCredential && + address.paymentCredential.type === TyphonTypes.HashType.ADDRESS && + address.paymentCredential.bipPath !== undefined; + } + + let destination; + if (isDeviceOwnedAddress) { + const address = output.address as TyphonAddress.BaseAddress; + + const paymentKeyPath = (address.paymentCredential as TyphonTypes.HashCredential) + .bipPath as TyphonTypes.BipPath; + const stakingKeyPath = (address.stakeCredential as TyphonTypes.HashCredential) + .bipPath as TyphonTypes.BipPath; + + const paymentKeyPathString = getBipPathString({ + account: accountIndex, + chain: paymentKeyPath.chain, + index: paymentKeyPath.index, + }); + const stakingKeyPathString = getBipPathString({ + account: accountIndex, + chain: stakingKeyPath.chain, + index: stakingKeyPath.index, + }); + + destination = { + isDeviceOwnedAddress, + params: { + spendingPath: paymentKeyPathString, + stakingPath: stakingKeyPathString, + }, + }; + } else { + const address = output.address; + destination = { + isDeviceOwnedAddress, + params: { + addressHex: address.getHex(), + }, + }; + } + + const tokenBundle = Object.values(groupBy(output.tokens, ({ policyId }) => policyId)).map( + tokens => ({ + policyIdHex: tokens[0].policyId, + tokens: tokens.map(token => ({ + assetNameHex: token.assetName, + amount: token.amount.toString(), + })), + }), + ); + + return { + amount: output.amount.toString(), + destination, + tokenBundle, + }; + }; + +function prepareCertificate(cert: TyphonTypes.Certificate): SignerTxCertificate { + if (cert.certType === CertificateType.STAKE_REGISTRATION) { + return prepareStakeRegistrationCertificate(cert as TyphonTypes.StakeRegistrationCertificate); + } else if (cert.certType === CertificateType.STAKE_DELEGATION) { + return prepareStakeDelegationCertificate(cert as TyphonTypes.StakeDelegationCertificate); + } else if (cert.certType === CertificateType.STAKE_DE_REGISTRATION) { + return prepareStakeDeRegistrationCertificate( + cert as TyphonTypes.StakeDeRegistrationCertificate, + ); + } else { + throw new Error("Invalid Certificate type"); + } +} + +function prepareStakeRegistrationCertificate( + certificate: TyphonTypes.StakeRegistrationCertificate, +): { + type: "REGISTRATION"; + params: { + stakeCredential: { + keyPath: string; + }; + }; +} { + if ( + certificate.stakeCredential.type === TyphonTypes.HashType.ADDRESS && + certificate.stakeCredential.bipPath + ) { + return { + type: "REGISTRATION", + params: { + stakeCredential: { + keyPath: getBipPathString({ + account: certificate.stakeCredential.bipPath.account, + chain: certificate.stakeCredential.bipPath.chain, + index: certificate.stakeCredential.bipPath.index, + }), + }, + }, + }; + } else { + throw new Error("Invalid stakeKey type"); + } +} + +function prepareStakeDelegationCertificate(certificate: TyphonTypes.StakeDelegationCertificate): { + type: "DELEGATION"; + params: { + stakeCredential: { + keyPath: string; + }; + poolKeyHashHex: string; + }; +} { + if ( + certificate.stakeCredential.type === TyphonTypes.HashType.ADDRESS && + certificate.stakeCredential.bipPath + ) { + return { + type: "DELEGATION", + params: { + stakeCredential: { + keyPath: getBipPathString({ + account: certificate.stakeCredential.bipPath.account, + chain: certificate.stakeCredential.bipPath.chain, + index: certificate.stakeCredential.bipPath.index, + }), + }, + poolKeyHashHex: certificate.poolHash, + }, + }; + } else { + throw new Error("Invalid stakeKey type"); + } +} + +function prepareStakeDeRegistrationCertificate(certificate: TyphonTypes.Certificate): { + type: "DEREGISTRATION"; + params: { + stakeCredential: { + keyPath: string; + }; + }; +} { + if ( + certificate.stakeCredential.type === TyphonTypes.HashType.ADDRESS && + certificate.stakeCredential.bipPath + ) { + return { + type: "DEREGISTRATION", + params: { + stakeCredential: { + keyPath: getBipPathString({ + account: certificate.stakeCredential.bipPath.account, + chain: certificate.stakeCredential.bipPath.chain, + index: certificate.stakeCredential.bipPath.index, + }), + }, + }, + }; + } else { + throw new Error("Invalid stakeKey type"); + } +} + +function prepareWithdrawal(withdrawal: TyphonTypes.Withdrawal): SignerTxWithdrawal { + if ( + withdrawal.rewardAccount.stakeCredential.type === TyphonTypes.HashType.ADDRESS && + withdrawal.rewardAccount.stakeCredential.bipPath + ) { + return { + stakeCredential: { + keyPath: getBipPathString({ + account: withdrawal.rewardAccount.stakeCredential.bipPath.account, + chain: withdrawal.rewardAccount.stakeCredential.bipPath.chain, + index: withdrawal.rewardAccount.stakeCredential.bipPath.index, + }), + }, + amount: withdrawal.amount.toString(), + }; + } else { + throw new Error("Invalid stakeKey type"); + } +} diff --git a/libs/ledger-live-common/src/families/cardano/utils.ts b/libs/coin-modules/coin-cardano/src/utils.ts similarity index 100% rename from libs/ledger-live-common/src/families/cardano/utils.ts rename to libs/coin-modules/coin-cardano/src/utils.ts diff --git a/libs/coin-modules/coin-cardano/tsconfig.json b/libs/coin-modules/coin-cardano/tsconfig.json new file mode 100644 index 00000000000..cdb8be8ecb9 --- /dev/null +++ b/libs/coin-modules/coin-cardano/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "../../../tsconfig.base", + "compilerOptions": { + "declaration": true, + "declarationMap": true, + "module": "commonjs", + "downlevelIteration": true, + "lib": ["es2020", "dom"], + "outDir": "lib" + }, + "include": ["src/**/*"] +} diff --git a/libs/ledger-live-common/.unimportedrc.json b/libs/ledger-live-common/.unimportedrc.json index 9cecb83df02..34e002bb959 100644 --- a/libs/ledger-live-common/.unimportedrc.json +++ b/libs/ledger-live-common/.unimportedrc.json @@ -2536,7 +2536,9 @@ "src/families/cardano/datasets/scanAccounts.ts", "src/families/cardano/js-getTransactionStatus.unit.test.ts", "src/families/cardano/js-synchronisation.unit.test.ts", + "src/families/cardano/logic.ts", "src/families/cardano/logic.unit.test.ts", + "src/families/cardano/staking.ts", "src/families/casper/__snapshots__/bridge.integration.test.ts.snap", "src/families/casper/bridge.integration.test.ts", "src/families/casper/utils.unit.test.ts", diff --git a/libs/ledger-live-common/package.json b/libs/ledger-live-common/package.json index 04c2b3f93fc..14e6e459a4a 100644 --- a/libs/ledger-live-common/package.json +++ b/libs/ledger-live-common/package.json @@ -135,6 +135,7 @@ "@keplr-wallet/proto-types": "^0.12.76", "@ledgerhq/coin-algorand": "workspace:^", "@ledgerhq/coin-bitcoin": "workspace:^", + "@ledgerhq/coin-cardano": "workspace:^", "@ledgerhq/coin-evm": "workspace:^", "@ledgerhq/coin-framework": "workspace:^", "@ledgerhq/coin-near": "workspace:^", @@ -181,7 +182,6 @@ "@stacks/network": "6.10.0", "@stacks/transactions": "6.11.0", "@stellar/stellar-sdk": "^11.3.0", - "@stricahq/bip32ed25519": "^1.0.3", "@stricahq/typhonjs": "^1.2.6", "@taquito/ledger-signer": "^13.0.1", "@types/bchaddrjs": "^0.4.0", diff --git a/libs/ledger-live-common/scripts/sync-families-dispatch.mjs b/libs/ledger-live-common/scripts/sync-families-dispatch.mjs index 31c0febc6c8..41bd8af4961 100644 --- a/libs/ledger-live-common/scripts/sync-families-dispatch.mjs +++ b/libs/ledger-live-common/scripts/sync-families-dispatch.mjs @@ -24,6 +24,7 @@ const targets = [ const familiesWPackage = [ "algorand", "bitcoin", + "cardano", "evm", "near", "polkadot", @@ -188,10 +189,12 @@ async function genTypesFile(families) { return ""; })(); - imprts += `import { Transaction as ${family}Transaction } from "${importPath}${family}/types${typesAsFolder}"; -import { TransactionRaw as ${family}TransactionRaw } from "${importPath}${family}/types${typesAsFolder}"; -import { TransactionStatus as ${family}TransactionStatus } from "${importPath}${family}/types${typesAsFolder}"; -import { TransactionStatusRaw as ${family}TransactionStatusRaw } from "${importPath}${family}/types${typesAsFolder}"; + imprts += `import type { + Transaction as ${family}Transaction, + TransactionRaw as ${family}TransactionRaw, + TransactionStatus as ${family}TransactionStatus, + TransactionStatusRaw as ${family}TransactionStatusRaw, +} from "${importPath}${family}/types${typesAsFolder}"; `; exprtsT += ` | ${family}Transaction`; diff --git a/libs/ledger-live-common/src/errors.ts b/libs/ledger-live-common/src/errors.ts index cc820311d08..cdbec65adb6 100644 --- a/libs/ledger-live-common/src/errors.ts +++ b/libs/ledger-live-common/src/errors.ts @@ -30,9 +30,6 @@ export const DeviceOnboarded = createCustomErrorClass("DeviceOnboarded"); export const DeviceNotOnboarded = createCustomErrorClass("DeviceNotOnboarded"); export const DeviceAlreadySetup = createCustomErrorClass("DeviceAlreadySetup"); -export const AccountAwaitingSendPendingOperations = createCustomErrorClass( - "AccountAwaitingSendPendingOperations", -); export const SourceHasMultiSign = createCustomErrorClass("SourceHasMultiSign"); export const CosmosRedelegationInProgress = createCustomErrorClass("CosmosRedelegationInProgress"); export const CosmosDelegateAllFundsWarning = createCustomErrorClass( @@ -128,13 +125,13 @@ export const BluetoothNotSupportedError = createCustomErrorClass("FwUpdateBlueto export const EConnResetError = createCustomErrorClass("EConnReset"); export { ClaimRewardsFeesWarning } from "@ledgerhq/errors"; +export * from "./families/stellar/errors"; export * from "@ledgerhq/coin-framework/errors"; -export * from "@ledgerhq/coin-polkadot/errors"; export * from "@ledgerhq/coin-algorand/errors"; -export * from "./families/stellar/errors"; -export * from "./families/cardano/errors"; -export * from "@ledgerhq/coin-near/errors"; -export * from "@ledgerhq/coin-evm/errors"; export * from "@ledgerhq/coin-bitcoin/errors"; +export * from "@ledgerhq/coin-cardano/errors"; +export * from "@ledgerhq/coin-evm/errors"; +export * from "@ledgerhq/coin-near/errors"; +export * from "@ledgerhq/coin-polkadot/errors"; export * from "@ledgerhq/coin-solana/errors"; export * from "@ledgerhq/coin-tezos/errors"; diff --git a/libs/ledger-live-common/src/families/cardano/bridge.integration.test.ts b/libs/ledger-live-common/src/families/cardano/bridge.integration.test.ts index 4592cb2038c..23507de8a29 100644 --- a/libs/ledger-live-common/src/families/cardano/bridge.integration.test.ts +++ b/libs/ledger-live-common/src/families/cardano/bridge.integration.test.ts @@ -1,111 +1,5 @@ import "../../__tests__/test-helpers/setup"; import { testBridge } from "../../__tests__/test-helpers/bridge"; -import BigNumber from "bignumber.js"; -import { cardanoRawAccount1 } from "./datasets/rawAccount.1"; -import { cardanoScanAccounts } from "./datasets/scanAccounts"; -import { CardanoInvalidPoolId, CardanoMinAmountError } from "./errors"; -import { fromTransactionRaw } from "./transaction"; -import type { Transaction } from "./types"; -import type { DatasetTest } from "@ledgerhq/types-live"; - -const dataset: DatasetTest = { - implementations: ["js"], - currencies: { - cardano_testnet: { - scanAccounts: cardanoScanAccounts, - accounts: [ - { - raw: cardanoRawAccount1, - transactions: [ - { - name: "amount less then minimum", - transaction: fromTransactionRaw({ - family: "cardano", - recipient: - "addr_test1qpl90kc2jl5kr9tev0s7vays9yhwcdnq8nlylyk4dqsdq3g466elxnxwrzwq72pvp5akenj30t5s9et7frfvrxxx8xcsxrzs87", - amount: "0.1", - mode: "send", - poolId: undefined, - }), - expectedStatus: { - amount: new BigNumber("0.1"), - errors: { - amount: new CardanoMinAmountError(), - }, - }, - }, - /* // FIXME broken test - { - name: "token amount more than balance", - transaction: fromTransactionRaw({ - family: "cardano", - recipient: - "addr_test1qpl90kc2jl5kr9tev0s7vays9yhwcdnq8nlylyk4dqsdq3g466elxnxwrzwq72pvp5akenj30t5s9et7frfvrxxx8xcsxrzs87", - amount: "101", - subAccountId: - "js:2:cardano_testnet:806499588e0c4a58f4119f7e6e096bf42c3f774a528d2acec9e82ceebf87d1ceb3d4f3622dd2c77c65cc89c123f79337db22cf8a69f122e36dab1bf5083bf82d:cardano+cardano_testnet%2Fnative%2F47be64fcc8a7fe5321b976282ce4e43e4d29015f6613cfabcea28eab54657374", - mode: "send", - poolId: undefined, - }), - expectedStatus: { - amount: new BigNumber("101"), - errors: { - amount: new NotEnoughBalance(), - }, - }, - }, - { - name: "send max token", - transaction: fromTransactionRaw({ - family: "cardano", - recipient: - "addr_test1qpl90kc2jl5kr9tev0s7vays9yhwcdnq8nlylyk4dqsdq3g466elxnxwrzwq72pvp5akenj30t5s9et7frfvrxxx8xcsxrzs87", - amount: "0", - subAccountId: - "js:2:cardano_testnet:806499588e0c4a58f4119f7e6e096bf42c3f774a528d2acec9e82ceebf87d1ceb3d4f3622dd2c77c65cc89c123f79337db22cf8a69f122e36dab1bf5083bf82d:cardano+cardano_testnet%2Fnative%2F47be64fcc8a7fe5321b976282ce4e43e4d29015f6613cfabcea28eab54657374", - mode: "send", - useAllAmount: true, - poolId: undefined, - }), - expectedStatus: { - amount: new BigNumber("100"), - totalSpent: new BigNumber("100"), - errors: {}, - warnings: {}, - }, - }, - */ - { - name: "delegate to invalid poolId", - transaction: fromTransactionRaw({ - family: "cardano", - recipient: "", - amount: "0", - mode: "delegate", - poolId: "efae72c07a26e4542ba55ef59d35ad45ffaaac312865e3a758ede", - }), - expectedStatus: { - errors: { - poolId: new CardanoInvalidPoolId(), - }, - }, - }, - { - name: "delegate valid poolId", - transaction: fromTransactionRaw({ - family: "cardano", - recipient: "", - amount: "0", - mode: "delegate", - poolId: "efae72c07a26e4542ba55ef59d35ad45ffaaac312865e3a758ede997", - }), - expectedStatus: {}, - }, - ], - }, - ], - }, - }, -}; +import { dataset } from "@ledgerhq/coin-cardano/bridge.integration.test"; testBridge(dataset); diff --git a/libs/ledger-live-common/src/families/cardano/bridge/js.ts b/libs/ledger-live-common/src/families/cardano/bridge/js.ts deleted file mode 100644 index c1511a99841..00000000000 --- a/libs/ledger-live-common/src/families/cardano/bridge/js.ts +++ /dev/null @@ -1,45 +0,0 @@ -import type { Transaction } from "../types"; -import { SignerContext, scanAccounts, sync } from "../js-synchronisation"; -import estimateMaxSpendable from "../js-estimateMaxSpendable"; -import { createTransaction, prepareTransaction } from "../js-transaction"; -import getTransactionStatus from "../js-getTransactionStatus"; -import signOperation from "../js-signOperation"; -import broadcast from "../js-broadcast"; -import { makeAccountBridgeReceive } from "../../../bridge/jsHelpers"; -import { defaultUpdateTransaction } from "@ledgerhq/coin-framework/bridge/jsHelpers"; -import type { AccountBridge, CurrencyBridge } from "@ledgerhq/types-live"; -import { assignToAccountRaw, assignFromAccountRaw } from "../serialization"; -import Ada, { ExtendedPublicKey } from "@cardano-foundation/ledgerjs-hw-app-cardano"; -import { withDevice } from "../../../hw/deviceAccess"; -import { firstValueFrom, from } from "rxjs"; - -const receive = makeAccountBridgeReceive(); - -const signerContext: SignerContext = ( - deviceId: string, - fn: (signer) => Promise, -): Promise => { - return firstValueFrom(withDevice(deviceId)(transport => from(fn(new Ada(transport))))); -}; - -const accountBridge: AccountBridge = { - estimateMaxSpendable, - createTransaction, - updateTransaction: defaultUpdateTransaction, - getTransactionStatus, - prepareTransaction, - sync: sync(signerContext), - receive, - assignToAccountRaw, - assignFromAccountRaw, - signOperation, - broadcast, -}; - -const currencyBridge: CurrencyBridge = { - scanAccounts: scanAccounts(signerContext), - preload: async () => ({}), - hydrate: () => {}, -}; - -export default { currencyBridge, accountBridge }; diff --git a/libs/ledger-live-common/src/families/cardano/bridge/mock.ts b/libs/ledger-live-common/src/families/cardano/bridge/mock.ts index 08242d31afe..692d398d728 100644 --- a/libs/ledger-live-common/src/families/cardano/bridge/mock.ts +++ b/libs/ledger-live-common/src/families/cardano/bridge/mock.ts @@ -4,10 +4,10 @@ import { Token, Transaction as CardanoTransaction, TransactionStatus, -} from "../types"; +} from "@ledgerhq/coin-cardano/types"; import { utils as TyphonUtils } from "@stricahq/typhonjs"; import type { AccountBridge, CurrencyBridge, Account } from "@ledgerhq/types-live"; -import { decodeTokenAssetId, decodeTokenCurrencyId } from "../buildSubAccounts"; +import { decodeTokenAssetId, decodeTokenCurrencyId } from "@ledgerhq/coin-cardano/buildSubAccounts"; import { AmountRequired, FeeNotLoaded, @@ -15,8 +15,8 @@ import { NotEnoughBalance, RecipientRequired, } from "@ledgerhq/errors"; -import { CardanoMinAmountError, CardanoNotEnoughFunds } from "../errors"; -import { buildTransaction } from "../js-buildTransaction"; +import { CardanoMinAmountError, CardanoNotEnoughFunds } from "@ledgerhq/coin-cardano/errors"; +import { buildTransaction } from "@ledgerhq/coin-cardano/js-buildTransaction"; import { defaultUpdateTransaction } from "@ledgerhq/coin-framework/bridge/jsHelpers"; import { scanAccounts, diff --git a/libs/ledger-live-common/src/families/cardano/hw-getAddress.ts b/libs/ledger-live-common/src/families/cardano/hw-getAddress.ts deleted file mode 100644 index ced4508fae3..00000000000 --- a/libs/ledger-live-common/src/families/cardano/hw-getAddress.ts +++ /dev/null @@ -1,54 +0,0 @@ -import type { Resolver } from "../../hw/getAddress/types"; -import Ada, { Networks, AddressType } from "@cardano-foundation/ledgerjs-hw-app-cardano"; -import { str_to_path } from "@cardano-foundation/ledgerjs-hw-app-cardano/dist/utils/address"; -import { getBipPathFromString, getBipPathString } from "./logic"; -import { StakeChain } from "./types"; -import { STAKING_ADDRESS_INDEX } from "./constants"; -import { utils as TyphonUtils } from "@stricahq/typhonjs"; -import { address as TyphonAddress } from "@stricahq/typhonjs"; -import { getNetworkParameters } from "./networks"; - -const resolver: Resolver = async (transport, { path, verify, currency }) => { - const spendingPath = getBipPathFromString(path); - const stakingPathString = getBipPathString({ - account: spendingPath.account, - chain: StakeChain.stake, - index: STAKING_ADDRESS_INDEX, - }); - const networkParams = getNetworkParameters(currency.id); - const network = - networkParams.networkId === Networks.Mainnet.networkId ? Networks.Mainnet : Networks.Testnet; - - const ada = new Ada(transport); - const r = await ada.deriveAddress({ - network, - address: { - type: AddressType.BASE_PAYMENT_KEY_STAKE_KEY, - params: { - spendingPath: str_to_path(path), - stakingPath: str_to_path(stakingPathString), - }, - }, - }); - if (verify) { - await ada.showAddress({ - network, - address: { - type: AddressType.BASE_PAYMENT_KEY_STAKE_KEY, - params: { - spendingPath: str_to_path(path), - stakingPath: str_to_path(stakingPathString), - }, - }, - }); - } - const address = TyphonUtils.getAddressFromHex(r.addressHex) as TyphonAddress.BaseAddress; - return { - address: address.getBech32(), - // Here, we use publicKey hash, as cardano app doesn't export the public key - publicKey: address.paymentCredential.hash, - path, - }; -}; - -export default resolver; diff --git a/libs/ledger-live-common/src/families/cardano/logic.ts b/libs/ledger-live-common/src/families/cardano/logic.ts index eb551f9660f..fe71b0568c3 100644 --- a/libs/ledger-live-common/src/families/cardano/logic.ts +++ b/libs/ledger-live-common/src/families/cardano/logic.ts @@ -1,442 +1,2 @@ -import { - CARDANO_COIN_TYPE, - CARDANO_PURPOSE, - MEMO_LABEL, - STAKING_ADDRESS_INDEX, - TTL_GAP, -} from "./constants"; - -import { - utils as TyphonUtils, - types as TyphonTypes, - address as TyphonAddress, -} from "@stricahq/typhonjs"; - -import { - CardanoAccount, - BipPath, - PaymentChain, - PaymentCredential, - StakeChain, - StakeCredential, - Token, -} from "./types"; -import { Bip32PublicKey } from "@stricahq/bip32ed25519"; -import BigNumber from "bignumber.js"; -import { getNetworkParameters } from "./networks"; -import groupBy from "lodash/groupBy"; -import { APITransaction } from "./api/api-types"; -import { getCryptoCurrencyById } from "@ledgerhq/cryptoassets"; -import ShelleyTypeAddress from "@stricahq/typhonjs/dist/address/ShelleyTypeAddress"; -import type { OperationType } from "@ledgerhq/types-live"; -import type { CryptoCurrency } from "@ledgerhq/types-cryptoassets"; -import bech32 from "bech32"; -import { - AddressType, - AssetGroup, - TxInput, - TxOutput, - TxOutputDestination, - TxOutputDestinationType, -} from "@cardano-foundation/ledgerjs-hw-app-cardano"; -import { str_to_path } from "@cardano-foundation/ledgerjs-hw-app-cardano/dist/utils/address"; - -/** - * returns BipPath object with account, chain and index field for cardano - * - * @param {string} path - */ -export function getBipPathFromString(path: string): BipPath { - const regEx = new RegExp(`^${CARDANO_PURPOSE}'/${CARDANO_COIN_TYPE}'/(\\d*)'/([012])/(\\d*)`); - const result = path.match(regEx); - if (result == null) { - throw new Error("Invalid derivation path"); - } - return getBipPath({ - account: parseInt(result[1]), - chain: parseInt(result[2]), - index: parseInt(result[3]), - }); -} - -/** - * - * @returns true if the account can stake, false otherwise - */ -export function canStake(account: CardanoAccount): boolean { - return !!account.balance?.gt(0); -} - -/** - * - * @returns true if account is staked, false otherwise - */ -export function isAlreadyStaking(account: CardanoAccount): boolean { - return !!account?.cardanoResources?.delegation?.poolId; -} - -/** - * returns complete bipPath with purpose, coin, account, chain and index for cardano - */ -export function getBipPath({ - account, - chain, - index, -}: { - account: number; - chain: PaymentChain | StakeChain; - index: number; -}): BipPath { - return { - purpose: CARDANO_PURPOSE, - coin: CARDANO_COIN_TYPE, - account, - chain, - index, - }; -} - -/** - * returns bipPathString from account, chain and index for cardano - */ -export function getBipPathString({ - account, - chain, - index, -}: { - account: number; - chain: number; - index: number; -}): string { - return `${CARDANO_PURPOSE}'/${CARDANO_COIN_TYPE}'/${account}'/${chain}/${index}`; -} - -export function getExtendedPublicKeyFromHex(keyHex: string): Bip32PublicKey { - return Bip32PublicKey.fromBytes(Buffer.from(keyHex, "hex")); -} - -export function getCredentialKey( - accountKey: Bip32PublicKey, - path: BipPath, -): { key: string; path: BipPath } { - const keyBytes = accountKey.derive(path.chain).derive(path.index).toPublicKey().hash(); - const pubKeyHex = keyBytes.toString("hex"); - return { - key: pubKeyHex, - path, - }; -} - -/** - * returns cardano base address by paymentKey and stakeKey - */ -export function getBaseAddress({ - networkId, - paymentCred, - stakeCred, -}: { - networkId: number; - paymentCred: PaymentCredential; - stakeCred: StakeCredential; -}): TyphonAddress.BaseAddress { - const paymentCredential: TyphonTypes.HashCredential = { - hash: paymentCred.key, - type: TyphonTypes.HashType.ADDRESS, - bipPath: paymentCred.path, - }; - - const stakeCredential: TyphonTypes.HashCredential = { - hash: stakeCred.key, - type: TyphonTypes.HashType.ADDRESS, - bipPath: stakeCred.path, - }; - return new TyphonAddress.BaseAddress(networkId, paymentCredential, stakeCredential); -} - -/** - * Returns true if address is a valid - * - * @param {string} address - */ -export const isValidAddress = (address: string, networkId: number): boolean => { - if (!address) return false; - - try { - const cardanoAddress = TyphonUtils.getAddressFromBech32(address); - if (cardanoAddress instanceof ShelleyTypeAddress) { - const addressNetworkId = Number(cardanoAddress.getHex().toLowerCase().charAt(1)); - if (addressNetworkId !== networkId) { - return false; - } - } - } catch (error) { - return false; - } - return true; -}; - -export const getAbsoluteSlot = function (networkName: string, time: Date): number { - const networkParams = getNetworkParameters(networkName); - const byronChainEndSlots = networkParams.shelleyStartEpoch * networkParams.byronSlotsPerEpoch; - const byronChainEndTime = byronChainEndSlots * networkParams.byronSlotDuration; - - const shelleyChainTime = time.getTime() - networkParams.chainStartTime - byronChainEndTime; - const shelleyChainSlots = Math.floor(shelleyChainTime / networkParams.shelleySlotDuration); - return byronChainEndSlots + shelleyChainSlots; -}; - -/** - * Returns the time to live for transaction - * - * @returns {number} - */ -export function getTTL(networkName: string): number { - return getAbsoluteSlot(networkName, new Date()) + TTL_GAP; -} - -export function getEpoch(networkName: string, time: Date): number { - const networkParams = getNetworkParameters(networkName); - const chainTime = time.getTime() - networkParams.chainStartTime; - const epoch = Math.floor( - chainTime / (networkParams.shelleySlotsPerEpoch * networkParams.shelleySlotDuration), - ); - return epoch; -} - -export function mergeTokens(tokens: Array): Array { - return Object.values(groupBy(tokens, t => `${t.policyId}${t.assetName}`)).map(similarTokens => ({ - policyId: similarTokens[0].policyId, - assetName: similarTokens[0].assetName, - amount: similarTokens.reduce((total, token) => total.plus(token.amount), new BigNumber(0)), - })); -} - -/** - * @param { Array } b - * @param { Array } a - * @returns a - b - */ -export function getTokenDiff( - a: Array, - b: Array, -): Array { - return mergeTokens(a.concat(b.map(t => ({ ...t, amount: t.amount.negated() })))).filter( - t => !t.amount.eq(0), - ); -} - -/** - * returns the formatted transactionOutput for ledger cardano app - * - * @param output - * @param accountIndex - * @returns {TxOutput} - */ -export function prepareLedgerOutput(output: TyphonTypes.Output, accountIndex: number): TxOutput { - const isByronAddress = output.address instanceof TyphonAddress.ByronAddress; - let isDeviceOwnedAddress = false; - let destination: TxOutputDestination; - - if (!isByronAddress) { - const address = output.address as TyphonTypes.ShelleyAddress; - isDeviceOwnedAddress = - address.paymentCredential && - address.paymentCredential.type === TyphonTypes.HashType.ADDRESS && - address.paymentCredential.bipPath !== undefined; - } - - if (isDeviceOwnedAddress) { - const address = output.address as TyphonAddress.BaseAddress; - - const paymentKeyPath = (address.paymentCredential as TyphonTypes.HashCredential) - .bipPath as TyphonTypes.BipPath; - const stakingKeyPath = (address.stakeCredential as TyphonTypes.HashCredential) - .bipPath as TyphonTypes.BipPath; - - const paymentKeyPathString = getBipPathString({ - account: accountIndex, - chain: paymentKeyPath.chain, - index: paymentKeyPath.index, - }); - const stakingKeyPathString = getBipPathString({ - account: accountIndex, - chain: stakingKeyPath.chain, - index: stakingKeyPath.index, - }); - - destination = { - type: TxOutputDestinationType.DEVICE_OWNED, - params: { - type: AddressType.BASE_PAYMENT_KEY_STAKE_KEY, - params: { - spendingPath: str_to_path(paymentKeyPathString), - stakingPath: str_to_path(stakingKeyPathString), - }, - }, - }; - } else { - const address = output.address; - destination = { - type: TxOutputDestinationType.THIRD_PARTY, - params: { - addressHex: address.getHex(), - }, - }; - } - - const tokenBundle: Array = Object.values( - groupBy(output.tokens, ({ policyId }) => policyId), - ).map(tokens => ({ - policyIdHex: tokens[0].policyId, - tokens: tokens.map(token => ({ - assetNameHex: token.assetName, - amount: token.amount.toString(), - })), - })); - - return { - amount: output.amount.toString(), - destination, - tokenBundle, - }; -} - -/** - * returns the formatted transactionInput for ledger cardano app - * - * @param {TyphonTypes.Input} input - * @param {number} accountIndex - * @returns {TxInput} - */ -export function prepareLedgerInput(input: TyphonTypes.Input, accountIndex: number): TxInput { - const paymentKeyPath = - input.address.paymentCredential.type === TyphonTypes.HashType.ADDRESS - ? input.address.paymentCredential.bipPath - : undefined; - return { - txHashHex: input.txId, - outputIndex: input.index, - path: paymentKeyPath - ? str_to_path( - getBipPathString({ - account: accountIndex, - chain: paymentKeyPath.chain, - index: paymentKeyPath.index, - }), - ) - : null, - }; -} - -export function getAccountStakeCredential(xpub: string, index: number): StakeCredential { - const accountXPubKey = getExtendedPublicKeyFromHex(xpub); - const keyPath = getCredentialKey( - accountXPubKey, - getBipPath({ - account: index, - chain: StakeChain.stake, - index: STAKING_ADDRESS_INDEX, - }), - ); - return { - key: keyPath.key, - path: keyPath.path, - }; -} - -export function getOperationType({ - valueChange, - fees, -}: { - valueChange: BigNumber; - fees: BigNumber; -}): OperationType { - return valueChange.isNegative() - ? valueChange.absoluteValue().eq(fees) - ? "FEES" - : "OUT" - : valueChange.isPositive() - ? "IN" - : "NONE"; -} - -export function isTestnet(currency: CryptoCurrency): boolean { - return getCryptoCurrencyById(currency.id).isTestnetFor ? true : false; -} - -export function getAccountChange( - t: APITransaction, - accountCredentialsMap: Record, -): { ada: BigNumber; tokens: Array } { - let accountInputAda = new BigNumber(0); - const accountInputTokens: Array = []; - t.inputs.forEach(i => { - if (accountCredentialsMap[i.paymentKey]) { - accountInputAda = accountInputAda.plus(i.value); - accountInputTokens.push( - ...i.tokens.map(t => ({ - assetName: t.assetName, - policyId: t.policyId, - amount: new BigNumber(t.value), - })), - ); - } - }); - - let accountOutputAda = new BigNumber(0); - const accountOutputTokens: Array = []; - t.outputs.forEach(o => { - if (accountCredentialsMap[o.paymentKey]) { - accountOutputAda = accountOutputAda.plus(o.value); - accountOutputTokens.push( - ...o.tokens.map(t => ({ - assetName: t.assetName, - policyId: t.policyId, - amount: new BigNumber(t.value), - })), - ); - } - }); - - return { - ada: accountOutputAda.minus(accountInputAda), - tokens: getTokenDiff(accountOutputTokens, accountInputTokens), - }; -} - -export function getMemoFromTx(tx: APITransaction): string | undefined { - let memo; - const metadataValue = tx.metadata?.data.find(m => m.label === MEMO_LABEL.toString()); - if (metadataValue) { - try { - const parsedValue = JSON.parse(metadataValue.value); - if (parsedValue.msg && Array.isArray(parsedValue.msg) && parsedValue.msg.length) { - memo = parsedValue.msg.join(", "); - } - // eslint-disable-next-line no-empty - } catch (e) {} - } - return memo; -} - -export function isHexString(value: string): boolean { - const regExp = /^[0-9a-fA-F]+$/; - return regExp.test(value); -} - -export function decodeTokenName(assetName: string): string { - if (assetName.length > 0) { - const bytes = [...Buffer.from(assetName, "hex")]; - if (bytes.filter(byte => byte <= 32 || byte >= 127).length === 0) { - return String.fromCharCode(...bytes); - } - } - return assetName; -} - -export function getBech32PoolId(poolId: string, networkName: string): string { - const networkParams = getNetworkParameters(networkName); - const words = bech32.toWords(Buffer.from(poolId, "hex")); - const encoded = bech32.encode(networkParams.poolIdPrefix, words, 1000); - return encoded; -} +// Encapsulate for LLD et LLM +export * from "@ledgerhq/coin-cardano/logic"; diff --git a/libs/ledger-live-common/src/families/cardano/react.ts b/libs/ledger-live-common/src/families/cardano/react.ts index 6dc25cd2e42..ceac3ba910e 100644 --- a/libs/ledger-live-common/src/families/cardano/react.ts +++ b/libs/ledger-live-common/src/families/cardano/react.ts @@ -1,6 +1,6 @@ import { useEffect, useRef, useState } from "react"; -import { fetchPoolList } from "./api/getPools"; -import { APIGetPoolList, StakePool } from "./api/api-types"; +import { fetchPoolList } from "@ledgerhq/coin-cardano/api/getPools"; +import { APIGetPoolList, StakePool } from "@ledgerhq/coin-cardano/api/api-types"; import { CryptoCurrency } from "@ledgerhq/types-cryptoassets"; export function useCardanoFamilyPools(currency: CryptoCurrency): { diff --git a/libs/ledger-live-common/src/families/cardano/setup.ts b/libs/ledger-live-common/src/families/cardano/setup.ts new file mode 100644 index 00000000000..5d98824b8fb --- /dev/null +++ b/libs/ledger-live-common/src/families/cardano/setup.ts @@ -0,0 +1,85 @@ +// Goal of this file is to inject all necessary device/signer dependency to coin-modules + +import Ada, { Networks, AddressType } from "@cardano-foundation/ledgerjs-hw-app-cardano"; +import { str_to_path } from "@cardano-foundation/ledgerjs-hw-app-cardano/dist/utils/address"; +import Transport from "@ledgerhq/hw-transport"; +import type { Bridge } from "@ledgerhq/types-live"; +import { createBridges } from "@ledgerhq/coin-cardano/bridge"; +import makeCliTools from "@ledgerhq/coin-cardano/cli-transaction"; +import cardanoResolver from "@ledgerhq/coin-cardano/hw-getAddress"; +import type { CardanoLikeNetworkParameters, Transaction } from "@ledgerhq/coin-cardano/types"; +import type { + CardanoAddress, + CardanoExtendedPublicKey, + CardanoSignRequest, + CardanoSignature, + CardanoSigner, + GetAddressRequest, +} from "@ledgerhq/coin-cardano/signer"; +import { CreateSigner, createResolver, executeWithSigner } from "../../bridge/setup"; +import type { Resolver } from "../../hw/getAddress/types"; +import signerSerializer from "./signerSerializer"; + +function findNetwork(networkParams: CardanoLikeNetworkParameters) { + return networkParams.networkId === Networks.Mainnet.networkId + ? Networks.Mainnet + : Networks.Testnet; +} + +const createSigner: CreateSigner = (transport: Transport) => { + const ada = new Ada(transport); + return { + getAddress: async ({ + path, + stakingPathString, + networkParams, + verify, + }: GetAddressRequest): Promise => { + const network = findNetwork(networkParams); + + const addr = await ada.deriveAddress({ + network, + address: { + type: AddressType.BASE_PAYMENT_KEY_STAKE_KEY, + params: { + spendingPath: str_to_path(path), + stakingPath: str_to_path(stakingPathString), + }, + }, + }); + if (verify) { + await ada.showAddress({ + network, + address: { + type: AddressType.BASE_PAYMENT_KEY_STAKE_KEY, + params: { + spendingPath: str_to_path(path), + stakingPath: str_to_path(stakingPathString), + }, + }, + }); + } + + return addr; + }, + getPublicKey: async (accountPath: string): Promise => { + return ada.getExtendedPublicKey({ + path: str_to_path(accountPath), + }); + }, + sign: async ({ transaction, networkParams }: CardanoSignRequest): Promise => { + const network = findNetwork(networkParams); + const trxOptions = signerSerializer(network, transaction); + + return ada.signTransaction(trxOptions); + }, + }; +}; + +const bridge: Bridge = createBridges(executeWithSigner(createSigner)); + +const resolver: Resolver = createResolver(createSigner, cardanoResolver); + +const cliTools = makeCliTools(); + +export { bridge, cliTools, resolver }; diff --git a/libs/ledger-live-common/src/families/cardano/signerSerializer.ts b/libs/ledger-live-common/src/families/cardano/signerSerializer.ts new file mode 100644 index 00000000000..5c5b0f147ef --- /dev/null +++ b/libs/ledger-live-common/src/families/cardano/signerSerializer.ts @@ -0,0 +1,154 @@ +import { + AddressType, + Certificate, + CertificateType, + Network, + SignTransactionRequest, + StakeCredentialParams, + StakeCredentialParamsType, + TransactionSigningMode, + TxAuxiliaryDataType, + TxInput, + TxOutput, + TxOutputDestination, + TxOutputDestinationType, + Withdrawal, +} from "@cardano-foundation/ledgerjs-hw-app-cardano"; +import { str_to_path } from "@cardano-foundation/ledgerjs-hw-app-cardano/dist/utils/address"; +import { + SignerTransaction, + SignerTxCertificate, + SignerTxInput, + SignerTxOutput, + SignerTxWithdrawal, +} from "@ledgerhq/coin-cardano/signer"; + +/** + * Convert a CoinModule transaction format into a transaction to sign with Cardano Foundation signer. + * @param network + * @param transaction coming form CoinModule and will be converted to `SignTransactionRequest` + * @returns + */ +export default function (network: Network, transaction: SignerTransaction): SignTransactionRequest { + return { + signingMode: TransactionSigningMode.ORDINARY_TRANSACTION, + tx: { + network, + inputs: transaction.inputs.map(prepareLedgerInput), + outputs: transaction.outputs.map(prepareLedgerOutput), + certificates: transaction.certificates.map(prepareCertificate), + withdrawals: transaction.withdrawals.map(prepareWithdrawal), + fee: transaction.fee, + ttl: transaction.ttl, + validityIntervalStart: null, + auxiliaryData: transaction.auxiliaryData + ? { + type: TxAuxiliaryDataType.ARBITRARY_HASH, + params: { + hashHex: transaction.auxiliaryData, + }, + } + : null, + }, + additionalWitnessPaths: [], + }; +} + +/** + * returns the formatted transactionInput for ledger cardano app + * + * @param {TyphonTypes.Input} input + * @param {number} accountIndex + * @returns {TxInput} + */ +function prepareLedgerInput({ txHashHex, outputIndex, path }: SignerTxInput): TxInput { + return { + txHashHex, + outputIndex, + path: path ? str_to_path(path) : null, + }; +} + +/** + * returns the formatted transactionOutput for ledger cardano app + * + * @param output + * @param accountIndex + * @returns {TxOutput} + */ +function prepareLedgerOutput(output: SignerTxOutput): TxOutput { + const { amount, tokenBundle } = output; + const destination = convertDestination(output); + + return { + amount, + destination, + tokenBundle, + }; +} + +function convertDestination({ destination }: SignerTxOutput): TxOutputDestination { + if (destination.isDeviceOwnedAddress) { + return { + type: TxOutputDestinationType.DEVICE_OWNED, + params: { + type: AddressType.BASE_PAYMENT_KEY_STAKE_KEY, + params: { + spendingPath: str_to_path(destination.params.spendingPath), + stakingPath: str_to_path(destination.params.stakingPath), + }, + }, + }; + } else { + return { + type: TxOutputDestinationType.THIRD_PARTY, + params: { + addressHex: destination.params.addressHex, + }, + }; + } +} + +function prepareCertificate(cert: SignerTxCertificate): Certificate { + const stakeCredential: StakeCredentialParams = { + type: StakeCredentialParamsType.KEY_PATH, + keyPath: str_to_path(cert.params.stakeCredential.keyPath), + }; + + switch (cert.type) { + case "REGISTRATION": + return { + type: CertificateType.STAKE_REGISTRATION, + params: { + stakeCredential, + }, + }; + case "DELEGATION": + return { + type: CertificateType.STAKE_DELEGATION, + params: { + stakeCredential, + poolKeyHashHex: cert.params.poolKeyHashHex, + }, + }; + case "DEREGISTRATION": + return { + type: CertificateType.STAKE_DEREGISTRATION, + params: { + stakeCredential, + }, + }; + default: + throw new Error("Invalid Certificate type"); + } +} + +function prepareWithdrawal({ stakeCredential, amount }: SignerTxWithdrawal): Withdrawal { + return { + stakeCredential: { + type: StakeCredentialParamsType.KEY_PATH, + keyPath: str_to_path(stakeCredential.keyPath), + }, + amount, + }; +} diff --git a/libs/ledger-live-common/src/families/cardano/staking.ts b/libs/ledger-live-common/src/families/cardano/staking.ts new file mode 100644 index 00000000000..120aa978a97 --- /dev/null +++ b/libs/ledger-live-common/src/families/cardano/staking.ts @@ -0,0 +1,3 @@ +export type { APIGetPoolsDetail, StakePool } from "@ledgerhq/coin-cardano/api/api-types"; +export { fetchPoolDetails } from "@ledgerhq/coin-cardano/api/getPools"; +export { LEDGER_POOL_IDS } from "@ledgerhq/coin-cardano/utils"; diff --git a/libs/ledger-live-common/src/families/cardano/tx-helpers.ts b/libs/ledger-live-common/src/families/cardano/tx-helpers.ts deleted file mode 100644 index 4ca23b19cfe..00000000000 --- a/libs/ledger-live-common/src/families/cardano/tx-helpers.ts +++ /dev/null @@ -1,235 +0,0 @@ -import { - CertificateType, - StakeCredentialParamsType, - StakeDelegationParams, - StakeRegistrationParams, - StakeDeregistrationParams, - Withdrawal, -} from "@cardano-foundation/ledgerjs-hw-app-cardano"; -import { str_to_path } from "@cardano-foundation/ledgerjs-hw-app-cardano/dist/utils/address"; -import { types as TyphonTypes, address as TyphonAddress } from "@stricahq/typhonjs"; -import groupBy from "lodash/groupBy"; -import { - AddressType, - AssetGroup, - TxInput, - TxOutput, - TxOutputDestination, - TxOutputDestinationType, -} from "@cardano-foundation/ledgerjs-hw-app-cardano"; -import { getBipPathString } from "./logic"; - -/** - * returns the formatted transactionInput for ledger cardano app - * - * @param {TyphonTypes.Input} input - * @param {number} accountIndex - * @returns {TxInput} - */ -export function prepareLedgerInput(input: TyphonTypes.Input, accountIndex: number): TxInput { - const paymentKeyPath = - input.address.paymentCredential.type === TyphonTypes.HashType.ADDRESS - ? input.address.paymentCredential.bipPath - : undefined; - return { - txHashHex: input.txId, - outputIndex: input.index, - path: paymentKeyPath - ? str_to_path( - getBipPathString({ - account: accountIndex, - chain: paymentKeyPath.chain, - index: paymentKeyPath.index, - }), - ) - : null, - }; -} - -/** - * returns the formatted transactionOutput for ledger cardano app - * - * @param output - * @param accountIndex - * @returns {TxOutput} - */ -export function prepareLedgerOutput(output: TyphonTypes.Output, accountIndex: number): TxOutput { - const isByronAddress = output.address instanceof TyphonAddress.ByronAddress; - let isDeviceOwnedAddress = false; - let destination: TxOutputDestination; - - if (!isByronAddress) { - const address = output.address as TyphonTypes.ShelleyAddress; - isDeviceOwnedAddress = - address.paymentCredential && - address.paymentCredential.type === TyphonTypes.HashType.ADDRESS && - address.paymentCredential.bipPath !== undefined; - } - - if (isDeviceOwnedAddress) { - const address = output.address as TyphonAddress.BaseAddress; - - const paymentKeyPath = (address.paymentCredential as TyphonTypes.HashCredential) - .bipPath as TyphonTypes.BipPath; - const stakingKeyPath = (address.stakeCredential as TyphonTypes.HashCredential) - .bipPath as TyphonTypes.BipPath; - - const paymentKeyPathString = getBipPathString({ - account: accountIndex, - chain: paymentKeyPath.chain, - index: paymentKeyPath.index, - }); - const stakingKeyPathString = getBipPathString({ - account: accountIndex, - chain: stakingKeyPath.chain, - index: stakingKeyPath.index, - }); - - destination = { - type: TxOutputDestinationType.DEVICE_OWNED, - params: { - type: AddressType.BASE_PAYMENT_KEY_STAKE_KEY, - params: { - spendingPath: str_to_path(paymentKeyPathString), - stakingPath: str_to_path(stakingKeyPathString), - }, - }, - }; - } else { - const address = output.address; - destination = { - type: TxOutputDestinationType.THIRD_PARTY, - params: { - addressHex: address.getHex(), - }, - }; - } - - const tokenBundle: Array = Object.values( - groupBy(output.tokens, ({ policyId }) => policyId), - ).map(tokens => ({ - policyIdHex: tokens[0].policyId, - tokens: tokens.map(token => ({ - assetNameHex: token.assetName, - amount: token.amount.toString(), - })), - })); - - return { - amount: output.amount.toString(), - destination, - tokenBundle, - }; -} - -export function prepareStakeRegistrationCertificate( - certificate: TyphonTypes.StakeRegistrationCertificate, -): { - type: CertificateType.STAKE_REGISTRATION; - params: StakeRegistrationParams; -} { - if ( - certificate.stakeCredential.type === TyphonTypes.HashType.ADDRESS && - certificate.stakeCredential.bipPath - ) { - return { - type: CertificateType.STAKE_REGISTRATION, - params: { - stakeCredential: { - type: StakeCredentialParamsType.KEY_PATH, - keyPath: str_to_path( - getBipPathString({ - account: certificate.stakeCredential.bipPath.account, - chain: certificate.stakeCredential.bipPath.chain, - index: certificate.stakeCredential.bipPath.index, - }), - ), - }, - }, - }; - } else { - throw new Error("Invalid stakeKey type"); - } -} - -export function prepareStakeDelegationCertificate( - certificate: TyphonTypes.StakeDelegationCertificate, -): { - type: CertificateType.STAKE_DELEGATION; - params: StakeDelegationParams; -} { - if ( - certificate.stakeCredential.type === TyphonTypes.HashType.ADDRESS && - certificate.stakeCredential.bipPath - ) { - return { - type: CertificateType.STAKE_DELEGATION, - params: { - stakeCredential: { - type: StakeCredentialParamsType.KEY_PATH, - keyPath: str_to_path( - getBipPathString({ - account: certificate.stakeCredential.bipPath.account, - chain: certificate.stakeCredential.bipPath.chain, - index: certificate.stakeCredential.bipPath.index, - }), - ), - }, - poolKeyHashHex: certificate.poolHash, - }, - }; - } else { - throw new Error("Invalid stakeKey type"); - } -} - -export function prepareStakeDeRegistrationCertificate(certificate: TyphonTypes.Certificate): { - type: CertificateType.STAKE_DEREGISTRATION; - params: StakeDeregistrationParams; -} { - if ( - certificate.stakeCredential.type === TyphonTypes.HashType.ADDRESS && - certificate.stakeCredential.bipPath - ) { - return { - type: CertificateType.STAKE_DEREGISTRATION, - params: { - stakeCredential: { - type: StakeCredentialParamsType.KEY_PATH, - keyPath: str_to_path( - getBipPathString({ - account: certificate.stakeCredential.bipPath.account, - chain: certificate.stakeCredential.bipPath.chain, - index: certificate.stakeCredential.bipPath.index, - }), - ), - }, - }, - }; - } else { - throw new Error("Invalid stakeKey type"); - } -} - -export function prepareWithdrawal(withdrawal: TyphonTypes.Withdrawal): Withdrawal { - if ( - withdrawal.rewardAccount.stakeCredential.type === TyphonTypes.HashType.ADDRESS && - withdrawal.rewardAccount.stakeCredential.bipPath - ) { - return { - stakeCredential: { - type: StakeCredentialParamsType.KEY_PATH, - keyPath: str_to_path( - getBipPathString({ - account: withdrawal.rewardAccount.stakeCredential.bipPath.account, - chain: withdrawal.rewardAccount.stakeCredential.bipPath.chain, - index: withdrawal.rewardAccount.stakeCredential.bipPath.index, - }), - ), - }, - amount: withdrawal.amount.toString(), - }; - } else { - throw new Error("Invalid stakeKey type"); - } -} diff --git a/libs/ledger-live-common/src/families/cardano/types.ts b/libs/ledger-live-common/src/families/cardano/types.ts index 87651df4cd6..93874127b07 100644 --- a/libs/ledger-live-common/src/families/cardano/types.ts +++ b/libs/ledger-live-common/src/families/cardano/types.ts @@ -1,227 +1,2 @@ -import type { BigNumber } from "bignumber.js"; -import { types as TyphonTypes } from "@stricahq/typhonjs"; -import type { - Account, - AccountRaw, - Operation, - OperationRaw, - TransactionCommon, - TransactionCommonRaw, - TransactionStatusCommon, - TransactionStatusCommonRaw, -} from "@ledgerhq/types-live"; - -export enum PaymentChain { - external = 0, - internal = 1, -} - -export enum StakeChain { - stake = 2, -} - -export enum NetworkId { - testnet = 0, - mainnet = 1, -} - -export type BipPath = { - purpose: number; - coin: number; - account: number; - chain: PaymentChain | StakeChain; - index: number; -}; - -export type BipPathRaw = { - purpose: number; - coin: number; - account: number; - chain: PaymentChain | StakeChain; - index: number; -}; - -export type Token = { - assetName: string; - policyId: string; - amount: BigNumber; -}; - -export type TokenRaw = { - assetName: string; - policyId: string; - amount: string; -}; - -export type PaymentKeyPath = { - key: string; - path: BipPath; -}; - -export type PaymentCredential = { - isUsed: boolean; - key: string; - path: BipPath; -}; - -export type PaymentCredentialRaw = { - isUsed: boolean; - key: string; - path: BipPathRaw; -}; - -export type StakeCredential = { - key: string; - path: BipPath; -}; - -export type CardanoOutput = { - hash: string; - index: number; - address: string; - amount: BigNumber; - tokens: Array; - paymentCredential: { - key: string; - path: BipPath; - }; -}; - -export type CardanoOutputRaw = { - hash: string; - index: number; - address: string; - amount: string; - tokens: Array; - paymentCredential: { - key: string; - path: BipPath; - }; -}; - -export type ProtocolParams = { - minFeeA: string; - minFeeB: string; - stakeKeyDeposit: string; - lovelacePerUtxoWord: string; - collateralPercent: string; - priceSteps: string; - priceMem: string; - languageView: TyphonTypes.LanguageView; -}; - -export type ProtocolParamsRaw = { - minFeeA: string; - minFeeB: string; - stakeKeyDeposit: string; - lovelacePerUtxoWord: string; - collateralPercent: string; - priceSteps: string; - priceMem: string; - // TyphonTypes.LanguageView is already a raw type - languageView: TyphonTypes.LanguageView; -}; - -export type CardanoDelegation = { - status: boolean; - poolId: string; - ticker: string; - name: string; - rewards: BigNumber; -}; - -export type CardanoDelegationRaw = { - status: boolean; - poolId: string; - ticker: string; - name: string; - rewards: string; -}; - -/** - * Cardano account resources - */ -export type CardanoResources = { - externalCredentials: Array; - internalCredentials: Array; - delegation: CardanoDelegation | undefined; - utxos: Array; - protocolParams: ProtocolParams; -}; - -/** - * Cardano account resources from raw JSON - */ -export type CardanoResourcesRaw = { - externalCredentials: Array; - internalCredentials: Array; - delegation: CardanoDelegationRaw | undefined; - utxos: Array; - protocolParams: ProtocolParamsRaw; -}; - -export type CardanoOperationMode = "send" | "delegate" | "undelegate"; - -/** - * Cardano transaction - */ -export type Transaction = TransactionCommon & { - mode: CardanoOperationMode; - family: "cardano"; - fees?: BigNumber; - memo?: string; - poolId: string | undefined; - // add here all transaction-specific fields if you implement other modes than "send" -}; - -/** - * Cardano transaction from a raw JSON - */ -export type TransactionRaw = TransactionCommonRaw & { - family: "cardano"; - mode: CardanoOperationMode; - fees?: string; - memo?: string; - poolId: string | undefined; - // also the transaction fields as raw JSON data -}; - -export type CardanoLikeNetworkParameters = { - identifier: string; - networkId: number; - chainStartTime: number; - byronSlotDuration: number; - byronSlotsPerEpoch: number; - shelleyStartEpoch: number; - shelleySlotDuration: number; - shelleySlotsPerEpoch: number; - addressPrefix: string; - poolIdPrefix: string; -}; - -/** - * Cardano currency data that will be preloaded. - */ -export type CardanoPreloadData = { - protocolParams: ProtocolParams; -}; - -export type CardanoAccount = Account & { cardanoResources: CardanoResources }; - -export type CardanoAccountRaw = AccountRaw & { - cardanoResources: CardanoResourcesRaw; -}; - -export type TransactionStatus = TransactionStatusCommon; - -export type TransactionStatusRaw = TransactionStatusCommonRaw; - -export type CardanoOperation = Operation; -export type CardanoOperationRaw = OperationRaw; - -export type CardanoOperationExtra = { - memo?: string; - deposit?: string; - refund?: string; - rewards?: string; -}; +// Encapsulate for LLD et LLM +export * from "@ledgerhq/coin-cardano/types"; diff --git a/libs/ledger-live-common/src/families/stellar/js-getTransactionStatus.ts b/libs/ledger-live-common/src/families/stellar/js-getTransactionStatus.ts index db7732521e0..cd94bdde2d0 100644 --- a/libs/ledger-live-common/src/families/stellar/js-getTransactionStatus.ts +++ b/libs/ledger-live-common/src/families/stellar/js-getTransactionStatus.ts @@ -1,5 +1,6 @@ import { BigNumber } from "bignumber.js"; import { + AccountAwaitingSendPendingOperations, AmountRequired, NotEnoughBalance, FeeNotLoaded, @@ -11,7 +12,6 @@ import { } from "@ledgerhq/errors"; import { StellarWrongMemoFormat, - AccountAwaitingSendPendingOperations, StellarAssetRequired, StellarAssetNotAccepted, StellarAssetNotFound, diff --git a/libs/ledger-live-common/src/generated/account.ts b/libs/ledger-live-common/src/generated/account.ts index f29f4d6a9c0..2844a5841e7 100644 --- a/libs/ledger-live-common/src/generated/account.ts +++ b/libs/ledger-live-common/src/generated/account.ts @@ -1,13 +1,13 @@ -import cardano from "../families/cardano/account"; import crypto_org from "../families/crypto_org/account"; import vechain from "../families/vechain/account"; import bitcoin from "@ledgerhq/coin-bitcoin/account"; +import cardano from "@ledgerhq/coin-cardano/account"; import near from "@ledgerhq/coin-near/account"; export default { - cardano, crypto_org, vechain, bitcoin, + cardano, near, }; diff --git a/libs/ledger-live-common/src/generated/bridge/js.ts b/libs/ledger-live-common/src/generated/bridge/js.ts index 02868c3801b..2246d16405d 100644 --- a/libs/ledger-live-common/src/generated/bridge/js.ts +++ b/libs/ledger-live-common/src/generated/bridge/js.ts @@ -1,4 +1,3 @@ -import cardano from "../../families/cardano/bridge/js"; import casper from "../../families/casper/bridge/js"; import celo from "../../families/celo/bridge/js"; import cosmos from "../../families/cosmos/bridge/js"; @@ -13,6 +12,7 @@ import tron from "../../families/tron/bridge/js"; import vechain from "../../families/vechain/bridge/js"; import { bridge as algorand } from "../../families/algorand/setup"; import { bridge as bitcoin } from "../../families/bitcoin/setup"; +import { bridge as cardano } from "../../families/cardano/setup"; import { bridge as evm } from "../../families/evm/setup"; import { bridge as near } from "../../families/near/setup"; import { bridge as polkadot } from "../../families/polkadot/setup"; @@ -21,7 +21,6 @@ import { bridge as tezos } from "../../families/tezos/setup"; import { bridge as xrp } from "../../families/xrp/setup"; export default { - cardano, casper, celo, cosmos, @@ -36,6 +35,7 @@ export default { vechain, algorand, bitcoin, + cardano, evm, near, polkadot, diff --git a/libs/ledger-live-common/src/generated/cli-transaction.ts b/libs/ledger-live-common/src/generated/cli-transaction.ts index ac7c171ffc2..f0fbea4cafb 100644 --- a/libs/ledger-live-common/src/generated/cli-transaction.ts +++ b/libs/ledger-live-common/src/generated/cli-transaction.ts @@ -1,4 +1,3 @@ -import cardano from "../families/cardano/cli-transaction"; import celo from "../families/celo/cli-transaction"; import cosmos from "../families/cosmos/cli-transaction"; import crypto_org from "../families/crypto_org/cli-transaction"; @@ -11,6 +10,7 @@ import tron from "../families/tron/cli-transaction"; import vechain from "../families/vechain/cli-transaction"; import { cliTools as algorand } from "../families/algorand/setup"; import { cliTools as bitcoin } from "../families/bitcoin/setup"; +import { cliTools as cardano } from "../families/cardano/setup"; import { cliTools as evm } from "../families/evm/setup"; import { cliTools as near } from "../families/near/setup"; import { cliTools as polkadot } from "../families/polkadot/setup"; @@ -19,7 +19,6 @@ import { cliTools as tezos } from "../families/tezos/setup"; import { cliTools as xrp } from "../families/xrp/setup"; export default { - cardano, celo, cosmos, crypto_org, @@ -32,6 +31,7 @@ export default { vechain, algorand, bitcoin, + cardano, evm, near, polkadot, diff --git a/libs/ledger-live-common/src/generated/deviceTransactionConfig.ts b/libs/ledger-live-common/src/generated/deviceTransactionConfig.ts index b9197b3adb8..55493601bcf 100644 --- a/libs/ledger-live-common/src/generated/deviceTransactionConfig.ts +++ b/libs/ledger-live-common/src/generated/deviceTransactionConfig.ts @@ -1,4 +1,3 @@ -import cardano from "../families/cardano/deviceTransactionConfig"; import casper from "../families/casper/deviceTransactionConfig"; import celo from "../families/celo/deviceTransactionConfig"; import cosmos from "../families/cosmos/deviceTransactionConfig"; @@ -12,6 +11,7 @@ import stellar from "../families/stellar/deviceTransactionConfig"; import tron from "../families/tron/deviceTransactionConfig"; import algorand from "@ledgerhq/coin-algorand/deviceTransactionConfig"; import bitcoin from "@ledgerhq/coin-bitcoin/deviceTransactionConfig"; +import cardano from "@ledgerhq/coin-cardano/deviceTransactionConfig"; import evm from "@ledgerhq/coin-evm/deviceTransactionConfig"; import near from "@ledgerhq/coin-near/deviceTransactionConfig"; import polkadot from "@ledgerhq/coin-polkadot/deviceTransactionConfig"; @@ -20,7 +20,6 @@ import tezos from "@ledgerhq/coin-tezos/deviceTransactionConfig"; import xrp from "@ledgerhq/coin-xrp/deviceTransactionConfig"; export default { - cardano, casper, celo, cosmos, @@ -34,6 +33,7 @@ export default { tron, algorand, bitcoin, + cardano, evm, near, polkadot, diff --git a/libs/ledger-live-common/src/generated/hw-getAddress.ts b/libs/ledger-live-common/src/generated/hw-getAddress.ts index 7fd941c3e36..86577185a7b 100644 --- a/libs/ledger-live-common/src/generated/hw-getAddress.ts +++ b/libs/ledger-live-common/src/generated/hw-getAddress.ts @@ -1,4 +1,3 @@ -import cardano from "../families/cardano/hw-getAddress"; import casper from "../families/casper/hw-getAddress"; import celo from "../families/celo/hw-getAddress"; import cosmos from "../families/cosmos/hw-getAddress"; @@ -13,6 +12,7 @@ import tron from "../families/tron/hw-getAddress"; import vechain from "../families/vechain/hw-getAddress"; import { resolver as algorand } from "../families/algorand/setup"; import { resolver as bitcoin } from "../families/bitcoin/setup"; +import { resolver as cardano } from "../families/cardano/setup"; import { resolver as evm } from "../families/evm/setup"; import { resolver as near } from "../families/near/setup"; import { resolver as polkadot } from "../families/polkadot/setup"; @@ -21,7 +21,6 @@ import { resolver as tezos } from "../families/tezos/setup"; import { resolver as xrp } from "../families/xrp/setup"; export default { - cardano, casper, celo, cosmos, @@ -36,6 +35,7 @@ export default { vechain, algorand, bitcoin, + cardano, evm, near, polkadot, diff --git a/libs/ledger-live-common/src/generated/specs.ts b/libs/ledger-live-common/src/generated/specs.ts index e8195957749..01978d40c84 100644 --- a/libs/ledger-live-common/src/generated/specs.ts +++ b/libs/ledger-live-common/src/generated/specs.ts @@ -1,4 +1,3 @@ -import cardano from "../families/cardano/specs"; import casper from "../families/casper/specs"; import celo from "../families/celo/specs"; import cosmos from "../families/cosmos/specs"; @@ -13,6 +12,7 @@ import tron from "../families/tron/specs"; import vechain from "../families/vechain/specs"; import algorand from "@ledgerhq/coin-algorand/specs"; import bitcoin from "@ledgerhq/coin-bitcoin/specs"; +import cardano from "@ledgerhq/coin-cardano/specs"; import evm from "@ledgerhq/coin-evm/specs"; import near from "@ledgerhq/coin-near/specs"; import polkadot from "@ledgerhq/coin-polkadot/specs"; @@ -21,7 +21,6 @@ import tezos from "@ledgerhq/coin-tezos/specs"; import xrp from "@ledgerhq/coin-xrp/specs"; export default { - cardano, casper, celo, cosmos, @@ -36,6 +35,7 @@ export default { vechain, algorand, bitcoin, + cardano, evm, near, polkadot, diff --git a/libs/ledger-live-common/src/generated/transaction.ts b/libs/ledger-live-common/src/generated/transaction.ts index 5a3931c9c32..9292e52e71e 100644 --- a/libs/ledger-live-common/src/generated/transaction.ts +++ b/libs/ledger-live-common/src/generated/transaction.ts @@ -1,4 +1,3 @@ -import cardano from "../families/cardano/transaction"; import casper from "../families/casper/transaction"; import celo from "../families/celo/transaction"; import cosmos from "../families/cosmos/transaction"; @@ -13,6 +12,7 @@ import tron from "../families/tron/transaction"; import vechain from "../families/vechain/transaction"; import algorand from "@ledgerhq/coin-algorand/transaction"; import bitcoin from "@ledgerhq/coin-bitcoin/transaction"; +import cardano from "@ledgerhq/coin-cardano/transaction"; import evm from "@ledgerhq/coin-evm/transaction"; import near from "@ledgerhq/coin-near/transaction"; import polkadot from "@ledgerhq/coin-polkadot/transaction"; @@ -21,7 +21,6 @@ import tezos from "@ledgerhq/coin-tezos/transaction"; import xrp from "@ledgerhq/coin-xrp/transaction"; export default { - cardano, casper, celo, cosmos, @@ -36,6 +35,7 @@ export default { vechain, algorand, bitcoin, + cardano, evm, near, polkadot, diff --git a/libs/ledger-live-common/src/generated/types.ts b/libs/ledger-live-common/src/generated/types.ts index d293a4e66ad..001c1577f89 100644 --- a/libs/ledger-live-common/src/generated/types.ts +++ b/libs/ledger-live-common/src/generated/types.ts @@ -1,87 +1,129 @@ -import { Transaction as algorandTransaction } from "@ledgerhq/coin-algorand/types"; -import { TransactionRaw as algorandTransactionRaw } from "@ledgerhq/coin-algorand/types"; -import { TransactionStatus as algorandTransactionStatus } from "@ledgerhq/coin-algorand/types"; -import { TransactionStatusRaw as algorandTransactionStatusRaw } from "@ledgerhq/coin-algorand/types"; -import { Transaction as bitcoinTransaction } from "@ledgerhq/coin-bitcoin/types"; -import { TransactionRaw as bitcoinTransactionRaw } from "@ledgerhq/coin-bitcoin/types"; -import { TransactionStatus as bitcoinTransactionStatus } from "@ledgerhq/coin-bitcoin/types"; -import { TransactionStatusRaw as bitcoinTransactionStatusRaw } from "@ledgerhq/coin-bitcoin/types"; -import { Transaction as cardanoTransaction } from "../families/cardano/types"; -import { TransactionRaw as cardanoTransactionRaw } from "../families/cardano/types"; -import { TransactionStatus as cardanoTransactionStatus } from "../families/cardano/types"; -import { TransactionStatusRaw as cardanoTransactionStatusRaw } from "../families/cardano/types"; -import { Transaction as casperTransaction } from "../families/casper/types"; -import { TransactionRaw as casperTransactionRaw } from "../families/casper/types"; -import { TransactionStatus as casperTransactionStatus } from "../families/casper/types"; -import { TransactionStatusRaw as casperTransactionStatusRaw } from "../families/casper/types"; -import { Transaction as celoTransaction } from "../families/celo/types"; -import { TransactionRaw as celoTransactionRaw } from "../families/celo/types"; -import { TransactionStatus as celoTransactionStatus } from "../families/celo/types"; -import { TransactionStatusRaw as celoTransactionStatusRaw } from "../families/celo/types"; -import { Transaction as cosmosTransaction } from "../families/cosmos/types"; -import { TransactionRaw as cosmosTransactionRaw } from "../families/cosmos/types"; -import { TransactionStatus as cosmosTransactionStatus } from "../families/cosmos/types"; -import { TransactionStatusRaw as cosmosTransactionStatusRaw } from "../families/cosmos/types"; -import { Transaction as crypto_orgTransaction } from "../families/crypto_org/types"; -import { TransactionRaw as crypto_orgTransactionRaw } from "../families/crypto_org/types"; -import { TransactionStatus as crypto_orgTransactionStatus } from "../families/crypto_org/types"; -import { TransactionStatusRaw as crypto_orgTransactionStatusRaw } from "../families/crypto_org/types"; -import { Transaction as elrondTransaction } from "../families/elrond/types"; -import { TransactionRaw as elrondTransactionRaw } from "../families/elrond/types"; -import { TransactionStatus as elrondTransactionStatus } from "../families/elrond/types"; -import { TransactionStatusRaw as elrondTransactionStatusRaw } from "../families/elrond/types"; -import { Transaction as evmTransaction } from "@ledgerhq/coin-evm/types/index"; -import { TransactionRaw as evmTransactionRaw } from "@ledgerhq/coin-evm/types/index"; -import { TransactionStatus as evmTransactionStatus } from "@ledgerhq/coin-evm/types/index"; -import { TransactionStatusRaw as evmTransactionStatusRaw } from "@ledgerhq/coin-evm/types/index"; -import { Transaction as filecoinTransaction } from "../families/filecoin/types"; -import { TransactionRaw as filecoinTransactionRaw } from "../families/filecoin/types"; -import { TransactionStatus as filecoinTransactionStatus } from "../families/filecoin/types"; -import { TransactionStatusRaw as filecoinTransactionStatusRaw } from "../families/filecoin/types"; -import { Transaction as hederaTransaction } from "../families/hedera/types"; -import { TransactionRaw as hederaTransactionRaw } from "../families/hedera/types"; -import { TransactionStatus as hederaTransactionStatus } from "../families/hedera/types"; -import { TransactionStatusRaw as hederaTransactionStatusRaw } from "../families/hedera/types"; -import { Transaction as internet_computerTransaction } from "../families/internet_computer/types"; -import { TransactionRaw as internet_computerTransactionRaw } from "../families/internet_computer/types"; -import { TransactionStatus as internet_computerTransactionStatus } from "../families/internet_computer/types"; -import { TransactionStatusRaw as internet_computerTransactionStatusRaw } from "../families/internet_computer/types"; -import { Transaction as nearTransaction } from "@ledgerhq/coin-near/types"; -import { TransactionRaw as nearTransactionRaw } from "@ledgerhq/coin-near/types"; -import { TransactionStatus as nearTransactionStatus } from "@ledgerhq/coin-near/types"; -import { TransactionStatusRaw as nearTransactionStatusRaw } from "@ledgerhq/coin-near/types"; -import { Transaction as polkadotTransaction } from "@ledgerhq/coin-polkadot/types/index"; -import { TransactionRaw as polkadotTransactionRaw } from "@ledgerhq/coin-polkadot/types/index"; -import { TransactionStatus as polkadotTransactionStatus } from "@ledgerhq/coin-polkadot/types/index"; -import { TransactionStatusRaw as polkadotTransactionStatusRaw } from "@ledgerhq/coin-polkadot/types/index"; -import { Transaction as solanaTransaction } from "@ledgerhq/coin-solana/types"; -import { TransactionRaw as solanaTransactionRaw } from "@ledgerhq/coin-solana/types"; -import { TransactionStatus as solanaTransactionStatus } from "@ledgerhq/coin-solana/types"; -import { TransactionStatusRaw as solanaTransactionStatusRaw } from "@ledgerhq/coin-solana/types"; -import { Transaction as stacksTransaction } from "../families/stacks/types"; -import { TransactionRaw as stacksTransactionRaw } from "../families/stacks/types"; -import { TransactionStatus as stacksTransactionStatus } from "../families/stacks/types"; -import { TransactionStatusRaw as stacksTransactionStatusRaw } from "../families/stacks/types"; -import { Transaction as stellarTransaction } from "../families/stellar/types"; -import { TransactionRaw as stellarTransactionRaw } from "../families/stellar/types"; -import { TransactionStatus as stellarTransactionStatus } from "../families/stellar/types"; -import { TransactionStatusRaw as stellarTransactionStatusRaw } from "../families/stellar/types"; -import { Transaction as tezosTransaction } from "@ledgerhq/coin-tezos/types/index"; -import { TransactionRaw as tezosTransactionRaw } from "@ledgerhq/coin-tezos/types/index"; -import { TransactionStatus as tezosTransactionStatus } from "@ledgerhq/coin-tezos/types/index"; -import { TransactionStatusRaw as tezosTransactionStatusRaw } from "@ledgerhq/coin-tezos/types/index"; -import { Transaction as tronTransaction } from "../families/tron/types"; -import { TransactionRaw as tronTransactionRaw } from "../families/tron/types"; -import { TransactionStatus as tronTransactionStatus } from "../families/tron/types"; -import { TransactionStatusRaw as tronTransactionStatusRaw } from "../families/tron/types"; -import { Transaction as vechainTransaction } from "../families/vechain/types"; -import { TransactionRaw as vechainTransactionRaw } from "../families/vechain/types"; -import { TransactionStatus as vechainTransactionStatus } from "../families/vechain/types"; -import { TransactionStatusRaw as vechainTransactionStatusRaw } from "../families/vechain/types"; -import { Transaction as xrpTransaction } from "@ledgerhq/coin-xrp/types"; -import { TransactionRaw as xrpTransactionRaw } from "@ledgerhq/coin-xrp/types"; -import { TransactionStatus as xrpTransactionStatus } from "@ledgerhq/coin-xrp/types"; -import { TransactionStatusRaw as xrpTransactionStatusRaw } from "@ledgerhq/coin-xrp/types"; +import type { + Transaction as algorandTransaction, + TransactionRaw as algorandTransactionRaw, + TransactionStatus as algorandTransactionStatus, + TransactionStatusRaw as algorandTransactionStatusRaw, +} from "@ledgerhq/coin-algorand/types"; +import type { + Transaction as bitcoinTransaction, + TransactionRaw as bitcoinTransactionRaw, + TransactionStatus as bitcoinTransactionStatus, + TransactionStatusRaw as bitcoinTransactionStatusRaw, +} from "@ledgerhq/coin-bitcoin/types"; +import type { + Transaction as cardanoTransaction, + TransactionRaw as cardanoTransactionRaw, + TransactionStatus as cardanoTransactionStatus, + TransactionStatusRaw as cardanoTransactionStatusRaw, +} from "@ledgerhq/coin-cardano/types"; +import type { + Transaction as casperTransaction, + TransactionRaw as casperTransactionRaw, + TransactionStatus as casperTransactionStatus, + TransactionStatusRaw as casperTransactionStatusRaw, +} from "../families/casper/types"; +import type { + Transaction as celoTransaction, + TransactionRaw as celoTransactionRaw, + TransactionStatus as celoTransactionStatus, + TransactionStatusRaw as celoTransactionStatusRaw, +} from "../families/celo/types"; +import type { + Transaction as cosmosTransaction, + TransactionRaw as cosmosTransactionRaw, + TransactionStatus as cosmosTransactionStatus, + TransactionStatusRaw as cosmosTransactionStatusRaw, +} from "../families/cosmos/types"; +import type { + Transaction as crypto_orgTransaction, + TransactionRaw as crypto_orgTransactionRaw, + TransactionStatus as crypto_orgTransactionStatus, + TransactionStatusRaw as crypto_orgTransactionStatusRaw, +} from "../families/crypto_org/types"; +import type { + Transaction as elrondTransaction, + TransactionRaw as elrondTransactionRaw, + TransactionStatus as elrondTransactionStatus, + TransactionStatusRaw as elrondTransactionStatusRaw, +} from "../families/elrond/types"; +import type { + Transaction as evmTransaction, + TransactionRaw as evmTransactionRaw, + TransactionStatus as evmTransactionStatus, + TransactionStatusRaw as evmTransactionStatusRaw, +} from "@ledgerhq/coin-evm/types/index"; +import type { + Transaction as filecoinTransaction, + TransactionRaw as filecoinTransactionRaw, + TransactionStatus as filecoinTransactionStatus, + TransactionStatusRaw as filecoinTransactionStatusRaw, +} from "../families/filecoin/types"; +import type { + Transaction as hederaTransaction, + TransactionRaw as hederaTransactionRaw, + TransactionStatus as hederaTransactionStatus, + TransactionStatusRaw as hederaTransactionStatusRaw, +} from "../families/hedera/types"; +import type { + Transaction as internet_computerTransaction, + TransactionRaw as internet_computerTransactionRaw, + TransactionStatus as internet_computerTransactionStatus, + TransactionStatusRaw as internet_computerTransactionStatusRaw, +} from "../families/internet_computer/types"; +import type { + Transaction as nearTransaction, + TransactionRaw as nearTransactionRaw, + TransactionStatus as nearTransactionStatus, + TransactionStatusRaw as nearTransactionStatusRaw, +} from "@ledgerhq/coin-near/types"; +import type { + Transaction as polkadotTransaction, + TransactionRaw as polkadotTransactionRaw, + TransactionStatus as polkadotTransactionStatus, + TransactionStatusRaw as polkadotTransactionStatusRaw, +} from "@ledgerhq/coin-polkadot/types/index"; +import type { + Transaction as solanaTransaction, + TransactionRaw as solanaTransactionRaw, + TransactionStatus as solanaTransactionStatus, + TransactionStatusRaw as solanaTransactionStatusRaw, +} from "@ledgerhq/coin-solana/types"; +import type { + Transaction as stacksTransaction, + TransactionRaw as stacksTransactionRaw, + TransactionStatus as stacksTransactionStatus, + TransactionStatusRaw as stacksTransactionStatusRaw, +} from "../families/stacks/types"; +import type { + Transaction as stellarTransaction, + TransactionRaw as stellarTransactionRaw, + TransactionStatus as stellarTransactionStatus, + TransactionStatusRaw as stellarTransactionStatusRaw, +} from "../families/stellar/types"; +import type { + Transaction as tezosTransaction, + TransactionRaw as tezosTransactionRaw, + TransactionStatus as tezosTransactionStatus, + TransactionStatusRaw as tezosTransactionStatusRaw, +} from "@ledgerhq/coin-tezos/types/index"; +import type { + Transaction as tronTransaction, + TransactionRaw as tronTransactionRaw, + TransactionStatus as tronTransactionStatus, + TransactionStatusRaw as tronTransactionStatusRaw, +} from "../families/tron/types"; +import type { + Transaction as vechainTransaction, + TransactionRaw as vechainTransactionRaw, + TransactionStatus as vechainTransactionStatus, + TransactionStatusRaw as vechainTransactionStatusRaw, +} from "../families/vechain/types"; +import type { + Transaction as xrpTransaction, + TransactionRaw as xrpTransactionRaw, + TransactionStatus as xrpTransactionStatus, + TransactionStatusRaw as xrpTransactionStatusRaw, +} from "@ledgerhq/coin-xrp/types"; export type Transaction = | algorandTransaction diff --git a/libs/ledger-live-common/src/mock/account.ts b/libs/ledger-live-common/src/mock/account.ts index 928f4dca9f4..a5b8217d3ea 100644 --- a/libs/ledger-live-common/src/mock/account.ts +++ b/libs/ledger-live-common/src/mock/account.ts @@ -15,7 +15,7 @@ import { BitcoinAccount } from "@ledgerhq/coin-bitcoin/types"; import { PolkadotAccount } from "@ledgerhq/coin-polkadot/types/index"; import { TezosAccount } from "../families/tezos/types"; import { TronAccount } from "../families/tron/types"; -import { CardanoAccount, PaymentChain } from "../families/cardano/types"; +import { CardanoAccount, PaymentChain } from "@ledgerhq/coin-cardano/types"; import { types } from "@stricahq/typhonjs"; /** diff --git a/libs/ledgerjs/packages/errors/src/index.ts b/libs/ledgerjs/packages/errors/src/index.ts index f00aa8c54b2..18a51d29e1a 100644 --- a/libs/ledgerjs/packages/errors/src/index.ts +++ b/libs/ledgerjs/packages/errors/src/index.ts @@ -10,6 +10,9 @@ export { serializeError, deserializeError, createCustomErrorClass, addCustomErro export const AccountNameRequiredError = createCustomErrorClass("AccountNameRequired"); export const AccountNotSupported = createCustomErrorClass("AccountNotSupported"); +export const AccountAwaitingSendPendingOperations = createCustomErrorClass( + "AccountAwaitingSendPendingOperations", +); export const AmountRequired = createCustomErrorClass("AmountRequired"); export const BluetoothRequired = createCustomErrorClass("BluetoothRequired"); export const BtcUnmatchedApp = createCustomErrorClass("BtcUnmatchedApp"); diff --git a/package.json b/package.json index 9218a81fa28..1a7f3ed51d4 100644 --- a/package.json +++ b/package.json @@ -62,6 +62,7 @@ "coin:coverage:clean": "rm -rf coverage && rm -rf libs/coin-modules/**/coverage", "coin:algorand": "pnpm --filter coin-algorand", "coin:bitcoin": "pnpm --filter coin-bitcoin", + "coin:cardano": "pnpm --filter coin-cardano", "coin:evm": "pnpm --filter coin-evm", "coin:framework": "pnpm --filter coin-framework", "coin:tester": "pnpm --filter coin-tester", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 43a0e93de64..0f44f61ebe0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1870,6 +1870,76 @@ importers: specifier: ^29.1.1 version: 29.1.2(jest@29.7.0)(typescript@5.4.3) + libs/coin-modules/coin-cardano: + dependencies: + '@ledgerhq/coin-framework': + specifier: workspace:^ + version: link:../../coin-framework + '@ledgerhq/cryptoassets': + specifier: workspace:^ + version: link:../../ledgerjs/packages/cryptoassets + '@ledgerhq/devices': + specifier: workspace:* + version: link:../../ledgerjs/packages/devices + '@ledgerhq/errors': + specifier: workspace:^ + version: link:../../ledgerjs/packages/errors + '@ledgerhq/live-env': + specifier: workspace:^ + version: link:../../env + '@ledgerhq/live-network': + specifier: workspace:^ + version: link:../../live-network + '@ledgerhq/logs': + specifier: workspace:^ + version: link:../../ledgerjs/packages/logs + '@ledgerhq/types-cryptoassets': + specifier: workspace:^ + version: link:../../ledgerjs/packages/types-cryptoassets + '@ledgerhq/types-live': + specifier: workspace:^ + version: link:../../ledgerjs/packages/types-live + '@stricahq/bip32ed25519': + specifier: ^1.0.3 + version: 1.0.4 + '@stricahq/typhonjs': + specifier: ^1.2.6 + version: 1.2.8 + bech32: + specifier: ^1.1.3 + version: 1.1.4 + bignumber.js: + specifier: ^9.1.2 + version: 9.1.2 + expect: + specifier: ^27.4.6 + version: 27.5.1 + invariant: + specifier: ^2.2.2 + version: 2.2.4 + lodash: + specifier: ^4.17.21 + version: 4.17.21 + rxjs: + specifier: ^7.8.1 + version: 7.8.1 + devDependencies: + '@types/invariant': + specifier: ^2.2.2 + version: 2.2.37 + '@types/jest': + specifier: ^29.5.10 + version: 29.5.12 + '@types/lodash': + specifier: ^4.14.191 + version: 4.17.0 + jest: + specifier: ^29.7.0 + version: 29.7.0 + ts-jest: + specifier: ^29.1.1 + version: 29.1.2(jest@29.7.0)(typescript@5.4.3) + libs/coin-modules/coin-evm: dependencies: '@ethersproject/shims': @@ -2757,6 +2827,9 @@ importers: '@ledgerhq/coin-bitcoin': specifier: workspace:^ version: link:../coin-modules/coin-bitcoin + '@ledgerhq/coin-cardano': + specifier: workspace:^ + version: link:../coin-modules/coin-cardano '@ledgerhq/coin-evm': specifier: workspace:^ version: link:../coin-modules/coin-evm @@ -2895,9 +2968,6 @@ importers: '@stellar/stellar-sdk': specifier: ^11.3.0 version: 11.3.0 - '@stricahq/bip32ed25519': - specifier: ^1.0.3 - version: 1.0.4 '@stricahq/typhonjs': specifier: ^1.2.6 version: 1.2.8 @@ -36964,7 +37034,7 @@ packages: pretty-format: 29.7.0 slash: 3.0.0 strip-json-comments: 3.1.1 - ts-node: 10.9.2(@swc/core@1.4.11)(typescript@5.4.3) + ts-node: 10.9.2(typescript@5.4.3) transitivePeerDependencies: - babel-plugin-macros - metro @@ -37005,7 +37075,7 @@ packages: pretty-format: 29.7.0 slash: 3.0.0 strip-json-comments: 3.1.1 - ts-node: 10.9.2(@swc/core@1.4.11)(typescript@5.4.3) + ts-node: 10.9.2(typescript@5.4.3) transitivePeerDependencies: - babel-plugin-macros - metro