Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

[Feat/LIVE-10693]: implement custom methods for ledger live mobile #8531

Draft
wants to merge 6 commits into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/quiet-games-dance.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"live-mobile": patch
---

Add custom methods for swap live app in ledger live mobile
13 changes: 9 additions & 4 deletions apps/ledger-live-mobile/src/screens/Swap/LiveApp/WebView.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,21 @@
import React from "react";
import { LiveAppManifest } from "@ledgerhq/live-common/platform/types";
import React from "react";
import TabBarSafeAreaView from "~/components/TabBar/TabBarSafeAreaView";
import { Web3AppWebview } from "~/components/Web3AppWebview";
import { useSwapLiveAppCustomHandlers } from "./hooks/useSwapLiveAppCustomHandlers";

type Props = {
manifest: LiveAppManifest;
};

export function WebView({ manifest }: Props) {
const customHandlers = useSwapLiveAppCustomHandlers(manifest);

return (
<TabBarSafeAreaView>
<Web3AppWebview manifest={manifest} customHandlers={{}} />
</TabBarSafeAreaView>
<>
<TabBarSafeAreaView>
<Web3AppWebview manifest={manifest} customHandlers={customHandlers} />
</TabBarSafeAreaView>
</>
);
}
9 changes: 9 additions & 0 deletions apps/ledger-live-mobile/src/screens/Swap/LiveApp/consts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export const SEG_WIT_ABANDON_SEED_ADDRESS = "bc1qed3mqr92zvq2s782aqkyx785u23723w02qfrgs";
export const DEFAULT_SWAP_APP_ID = "swap-live-app-demo-3";

export const SWAP_VERSION = "2.35";

export const SWAP_TRACKING_PROPERTIES = {
swapVersion: SWAP_VERSION,
flow: "swap",
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import { Strategy } from "@ledgerhq/coin-evm/lib/types/index";
import { getMainAccount, getParentAccount } from "@ledgerhq/live-common/account/index";
import { getAccountBridge } from "@ledgerhq/live-common/bridge/index";
import { getAbandonSeedAddress } from "@ledgerhq/live-common/currencies/index";
import { Transaction, TransactionStatus } from "@ledgerhq/live-common/generated/types";
import { getAccountIdFromWalletAccountId } from "@ledgerhq/live-common/wallet-api/converters";
import { Account, AccountLike } from "@ledgerhq/types-live";
import BigNumber from "bignumber.js";
import { SEG_WIT_ABANDON_SEED_ADDRESS } from "../consts";
import {
convertToAtomicUnit,
convertToNonAtomicUnit,
getCustomFeesPerFamily,
transformToBigNumbers,
} from "../utils";

interface FeeParams {
fromAccountId: string;
fromAmount: string;
feeStrategy: Strategy;
openDrawer: boolean;
customFeeConfig: Record<string, unknown>;
SWAP_VERSION: string;
}

interface FeeData {
feesStrategy: Strategy;
estimatedFees: BigNumber | undefined;
errors: TransactionStatus["errors"];
warnings: TransactionStatus["warnings"];
customFeeConfig: Record<string, unknown>;
}

interface GenerateFeeDataParams {
account: AccountLike;
feePayingAccount: Account;
feesStrategy: Strategy;
fromAmount: BigNumber | undefined;
customFeeConfig: Record<string, unknown>;
}

const getRecipientAddress = (
transactionFamily: Transaction["family"],
currencyId: string,
): string => {
switch (transactionFamily) {
case "evm":
return getAbandonSeedAddress(currencyId);
case "bitcoin":
return SEG_WIT_ABANDON_SEED_ADDRESS;
default:
throw new Error(`Unsupported transaction family: ${transactionFamily}`);
}
};

const generateFeeData = async ({
account,
feePayingAccount,
feesStrategy = "medium",
fromAmount,
customFeeConfig,
}: GenerateFeeDataParams): Promise<FeeData> => {
const bridge = getAccountBridge(account, feePayingAccount);
const baseTransaction = bridge.createTransaction(feePayingAccount);

const recipient = getRecipientAddress(baseTransaction.family, feePayingAccount.currency.id);

const transactionConfig: Transaction = {
...baseTransaction,
subAccountId: account.type !== "Account" ? account.id : undefined,
recipient,
amount: fromAmount ?? new BigNumber(0),
feesStrategy,
...transformToBigNumbers(customFeeConfig),
};

const preparedTransaction = await bridge.updateTransaction(feePayingAccount, transactionConfig);
const transactionStatus = await bridge.getTransactionStatus(
feePayingAccount,
preparedTransaction,
);

return {
feesStrategy,
estimatedFees: convertToNonAtomicUnit({
amount: transactionStatus.estimatedFees,
account: feePayingAccount,
}),
errors: transactionStatus.errors,
warnings: transactionStatus.warnings,
customFeeConfig: getCustomFeesPerFamily(preparedTransaction),
};
};

export const getFee =
(accounts: AccountLike[]) =>
async ({ params }: { params: FeeParams }): Promise<FeeData> => {
const accountId = getAccountIdFromWalletAccountId(params.fromAccountId);
if (!accountId) {
throw new Error(`Invalid wallet account ID: ${params.fromAccountId}`);
}

const account = accounts.find(acc => acc.id === accountId);
if (!account) {
throw new Error(`Account not found: ${accountId}`);
}

const parentAccount =
account.type === "TokenAccount" ? getParentAccount(account, accounts) : undefined;

const feePayingAccount = getMainAccount(account, parentAccount);

const amount = new BigNumber(params.fromAmount);
const atomicAmount = convertToAtomicUnit({ amount, account });

return generateFeeData({
account,
feePayingAccount,
feesStrategy: params.feeStrategy,
fromAmount: atomicAmount,
customFeeConfig: params.customFeeConfig,
});
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { getMainAccount, getParentAccount } from "@ledgerhq/live-common/account/index";
import { getAccountIdFromWalletAccountId } from "@ledgerhq/live-common/wallet-api/converters";
import { AccountLike } from "@ledgerhq/types-live";
import { getNodeApi } from "@ledgerhq/coin-evm/api/node/index";

export function getTransactionByHash(accounts: AccountLike[]) {
return async ({
params,
}: {
params: {
transactionHash: string;
fromAccountId: string;
SWAP_VERSION: string;
};
}): Promise<
| {
hash: string;
blockHeight: number | undefined;
blockHash: string | undefined;
nonce: number;
gasUsed: string;
gasPrice: string;
value: string;
}
| object
> => {
const realFromAccountId = getAccountIdFromWalletAccountId(params.fromAccountId);
if (!realFromAccountId) {
return Promise.reject(new Error(`accountId ${params.fromAccountId} unknown`));
}

const fromAccount = accounts.find(acc => acc.id === realFromAccountId);
if (!fromAccount) {
return Promise.reject(new Error(`accountId ${params.fromAccountId} unknown`));
}

const fromParentAccount = getParentAccount(fromAccount, accounts);
const mainAccount = getMainAccount(fromAccount, fromParentAccount);

const nodeAPI = getNodeApi(mainAccount.currency);

try {
const tx = await nodeAPI.getTransaction(mainAccount.currency, params.transactionHash);
return Promise.resolve(tx);
} catch (error) {
// not a real error, the node just didn't find the transaction yet
return Promise.resolve({});
}
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { AccountLike } from "@ledgerhq/types-live";
import { Dispatch } from "redux";

import { getFee } from "./getFee";
import { getTransactionByHash } from "./getTransactionByHash";
import { saveSwapToHistory } from "./saveSwapToHistory";
import { swapRedirectToHistory } from "./swapRedirectToHistory";

export const swapCustomHandlers = ({
accounts,
dispatch,
}: {
accounts: AccountLike[];
dispatch: Dispatch;
}) => ({
"custom.getFee": getFee(accounts),
"custom.getTransactionByHash": getTransactionByHash(accounts),
"custom.saveSwapToHistory": saveSwapToHistory(accounts, dispatch),
"custom.swapRedirectToHistory": swapRedirectToHistory,
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { getParentAccount } from "@ledgerhq/live-common/account/index";
import { getAccountIdFromWalletAccountId } from "@ledgerhq/live-common/wallet-api/converters";
import { Account, AccountLike, SwapOperation } from "@ledgerhq/types-live";
import { convertToAtomicUnit } from "../utils";
import BigNumber from "bignumber.js";
import { updateAccountWithUpdater } from "~/actions/accounts";
import { Dispatch } from "redux";

export type SwapProps = {
provider: string;
fromAccountId: string;
fromParentAccountId?: string;
toAccountId: string;
fromAmount: string;
toAmount?: string;
quoteId: string;
rate: string;
feeStrategy: string;
customFeeConfig: string;
cacheKey: string;
loading: boolean;
error: boolean;
providerRedirectURL: string;
toNewTokenId: string;
swapApiBase: string;
estimatedFees: string;
estimatedFeesUnit: string;
swapId?: string;
};

export function saveSwapToHistory(accounts: AccountLike[], dispatch: Dispatch) {
return async ({ params }: { params: { swap: SwapProps; transaction_id: string } }) => {
const { swap, transaction_id } = params;
if (
!swap ||
!transaction_id ||
!swap.provider ||
!swap.fromAmount ||
!swap.toAmount ||
!swap.swapId
) {
return Promise.reject("Cannot save swap missing params");
}
const fromId = getAccountIdFromWalletAccountId(swap.fromAccountId);
const toId = getAccountIdFromWalletAccountId(swap.toAccountId);
if (!fromId || !toId) return Promise.reject("Accounts not found");
const operationId = `${fromId}-${transaction_id}-OUT`;
const fromAccount = accounts.find(acc => acc.id === fromId);
const toAccount = accounts.find(acc => acc.id === toId);
if (!fromAccount || !toAccount) {
return Promise.reject(new Error(`accountId ${fromId} unknown`));
}
const accountId =
fromAccount.type === "TokenAccount" ? getParentAccount(fromAccount, accounts).id : fromId;
const swapOperation: SwapOperation = {
status: "pending",
provider: swap.provider,
operationId,
swapId: swap.swapId,
receiverAccountId: toId,
tokenId: toId,
fromAmount: convertToAtomicUnit({
amount: new BigNumber(swap.fromAmount),
account: fromAccount,
})!,
toAmount: convertToAtomicUnit({
amount: new BigNumber(swap.toAmount),
account: toAccount,
})!,
};

dispatch(
updateAccountWithUpdater(accountId, (account: Account) => {
if (fromId === account.id) {
return { ...account, swapHistory: [...account.swapHistory, swapOperation] };
}
return {
...account,
subAccounts: account.subAccounts?.map<SubAccount>((a: SubAccount) => {
const subAccount = {
...a,
swapHistory: [...a.swapHistory, swapOperation],
};
return a.id === fromId ? subAccount : a;
}),
};
}),
);
return Promise.resolve();
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export const swapRedirectToHistory = async () => {
console.log("swapRedirectToHistory");
return Promise.resolve(undefined);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { LiveAppManifest } from "@ledgerhq/live-common/platform/types";
import { WalletAPICustomHandlers } from "@ledgerhq/live-common/wallet-api/types";
import { useMemo } from "react";
import { useDispatch, useSelector } from "react-redux";
import { usePTXCustomHandlers } from "~/components/WebPTXPlayer/CustomHandlers";
import { accountsSelector } from "~/reducers/accounts";
import { swapCustomHandlers } from "../customHandlers";

export function useSwapLiveAppCustomHandlers(manifest: LiveAppManifest) {
const accounts = useSelector(accountsSelector);
const ptxCustomHandlers = usePTXCustomHandlers(manifest, accounts);
const dispatch = useDispatch();

return useMemo<WalletAPICustomHandlers>(
() =>
({
...ptxCustomHandlers,
...swapCustomHandlers({
accounts,
dispatch,
}),
}) as WalletAPICustomHandlers,
[ptxCustomHandlers, accounts, dispatch],
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { useFeature } from "@ledgerhq/live-common/featureFlags/index";
import { useRemoteLiveAppManifest } from "@ledgerhq/live-common/platform/providers/RemoteLiveAppProvider/index";
import { LiveAppManifest } from "@ledgerhq/live-common/platform/types";
import { useLocalLiveAppManifest } from "@ledgerhq/live-common/wallet-api/LocalLiveAppProvider/index";
import { useMemo } from "react";
import { DEFAULT_SWAP_APP_ID } from "../consts";

export function useSwapLiveAppManifest() {
const ptxSwapCoreExperiment = useFeature("ptxSwapCoreExperiment");

const manifestIdToUse = useMemo(() => {
return ptxSwapCoreExperiment?.enabled && ptxSwapCoreExperiment.params?.manifest_id
? ptxSwapCoreExperiment.params?.manifest_id
: DEFAULT_SWAP_APP_ID;
}, [ptxSwapCoreExperiment?.enabled, ptxSwapCoreExperiment?.params?.manifest_id]);

const localManifest: LiveAppManifest | undefined = useLocalLiveAppManifest(manifestIdToUse);
const remoteManifest: LiveAppManifest | undefined = useRemoteLiveAppManifest(manifestIdToUse);

return !localManifest ? remoteManifest : localManifest;
}
Loading
Loading