Skip to content

Commit

Permalink
feat: add custom handlers for swap-live-app
Browse files Browse the repository at this point in the history
  • Loading branch information
kallen-ledger committed Jan 17, 2025
1 parent edd7bfb commit f058e5b
Show file tree
Hide file tree
Showing 11 changed files with 471 additions and 12 deletions.
9 changes: 8 additions & 1 deletion apps/ledger-live-mobile/src/screens/Swap/LiveApp/WebView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,23 @@ import React from "react";
import TabBarSafeAreaView from "~/components/TabBar/TabBarSafeAreaView";
import { Web3AppWebview } from "~/components/Web3AppWebview";
import { WebviewState } from "~/components/Web3AppWebview/types";
import { useSwapLiveAppCustomHandlers } from "./hooks/useSwapLiveAppCustomHandlers";
import QueuedDrawer from "~/components/QueuedDrawer";
import { Text } from "@ledgerhq/native-ui";

type Props = {
manifest: LiveAppManifest;
setWebviewState: React.Dispatch<React.SetStateAction<WebviewState>>;
};

export function WebView({ manifest, setWebviewState }: Props) {
const customHandlers = useSwapLiveAppCustomHandlers(manifest);
return (
<TabBarSafeAreaView>
<Web3AppWebview manifest={manifest} customHandlers={{}} onStateChange={setWebviewState} />
<Web3AppWebview manifest={manifest} customHandlers={customHandlers} onStateChange={setWebviewState} />
<QueuedDrawer isRequestingToBeOpened={true}>
<Text>{"hello world."}</Text>
</QueuedDrawer>
</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,128 @@
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 });

if (params.openDrawer) {
// display the drawer to the user and return their selected fee as the values
console.log("hello world xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx")
}

return await 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

0 comments on commit f058e5b

Please sign in to comment.