Skip to content
This repository has been archived by the owner on Jun 16, 2022. It is now read-only.

[LL-8097] sell/fund flow #4361

Merged
merged 17 commits into from
Jan 31, 2022
Merged
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
43 changes: 43 additions & 0 deletions src/internal/commands/completeExchange.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// @flow

import type { Observable } from "rxjs";
import { from } from "rxjs";
import type { ExchangeRaw } from "@ledgerhq/live-common/lib/exchange/platform/types";
import completeExchange from "@ledgerhq/live-common/lib/exchange/platform/completeExchange";
import { fromExchangeRaw } from "@ledgerhq/live-common/lib/exchange/platform/serialization";
import type { TransactionRaw } from "@ledgerhq/live-common/lib/types";
import { fromTransactionRaw } from "@ledgerhq/live-common/lib/transaction";

type Input = {
deviceId: string,
provider: string,
binaryPayload: string,
signature: string,
exchange: ExchangeRaw,
transaction: TransactionRaw,
exchangeType: number,
};

const cmd = ({
deviceId,
provider,
binaryPayload,
signature,
exchange,
transaction,
exchangeType,
}: Input): Observable<any> => {
// TODO type the events?
return from(
completeExchange({
deviceId,
provider,
binaryPayload,
signature,
exchange: fromExchangeRaw(exchange),
transaction: fromTransactionRaw(transaction),
exchangeType,
}),
);
};
export default cmd;
4 changes: 4 additions & 0 deletions src/internal/commands/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ import testCrash from "./testCrash";
import testInterval from "./testInterval";
import appOpExec from "./appOpExec";
import initSwap from "./initSwap";
import startExchange from "./startExchange";
import completeExchange from "./completeExchange";
import websocketBridge from "./websocketBridge";
import checkSignatureAndPrepare from "./checkSignatureAndPrepare";
import getTransactionId from "./getTransactionId";
Expand Down Expand Up @@ -47,6 +49,8 @@ export const commandsById = {
ping,
testApdu,
initSwap,
startExchange,
completeExchange,
checkSignatureAndPrepare,
getTransactionId,
testCrash,
Expand Down
20 changes: 20 additions & 0 deletions src/internal/commands/startExchange.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// @flow

import type { Observable } from "rxjs";
import { from } from "rxjs";
import startExchange from "@ledgerhq/live-common/lib/exchange/platform/startExchange";

type Input = {
deviceId: string,
exchangeType: number,
};

const cmd = ({ deviceId, exchangeType }: Input): Observable<any> => {
return from(
startExchange({
deviceId,
exchangeType,
}),
);
};
export default cmd;
31 changes: 29 additions & 2 deletions src/renderer/components/DeviceAction/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import {
renderListingApps,
renderWarningOutdated,
renderSwapDeviceConfirmationV2,
renderSellDeviceConfirmation,
renderSecureTransferDeviceConfirmation,
} from "./rendering";

type OwnProps<R, H, P> = {
Expand Down Expand Up @@ -107,6 +107,9 @@ const DeviceAction = <R, H, P>({
initSwapRequested,
initSwapError,
initSwapResult,
completeExchangeStarted,
completeExchangeResult,
completeExchangeError,
allowOpeningGranted,
initSellRequested,
initSellResult,
Expand Down Expand Up @@ -157,6 +160,30 @@ const DeviceAction = <R, H, P>({
return renderListingApps();
}

if (completeExchangeStarted && !completeExchangeResult && !completeExchangeError) {
const { exchangeType } = request;

// FIXME: could use a TS enum (when LLD will be in TS) or a JS object instead of raw numbers for switch values for clarity
switch (exchangeType) {
// swap
case 0x00: {
// FIXME: should use `renderSwapDeviceConfirmationV2` but all params not available in hookState for this SDK exchange flow
return <div>{"Confirm swap on your device"}</div>;
}

case 0x01: // sell
case 0x02: // fund
return renderSecureTransferDeviceConfirmation({
exchangeType: exchangeType === 0x01 ? "sell" : "fund",
Copy link
Contributor

Choose a reason for hiding this comment

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

Why not in the case statement?

Copy link
Contributor

Choose a reason for hiding this comment

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

What do you mean exactly?

Copy link
Contributor

Choose a reason for hiding this comment

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

Just wondering why you have the conditional statement within the 0x02 case

Copy link
Contributor

Choose a reason for hiding this comment

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

Is it just less lines of code rather than writing the return statement in both the 0x01 and the 0x02 statements?

Copy link
Contributor

Choose a reason for hiding this comment

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

Yes, it avoids code duplication.
FYI it is not only within the 0x02 case, but also within the 0x01 case (cf. -> https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/switch#methods_for_multi-criteria_case)

modelId,
type,
});

default:
return <div>{"Confirm exchange on your device"}</div>;
}
}

if (initSwapRequested && !initSwapResult && !initSwapError) {
const { transaction, exchange, exchangeRate, status } = request;
const { amountExpectedTo, estimatedFees } = hookState;
Expand All @@ -174,7 +201,7 @@ const DeviceAction = <R, H, P>({
}

if (initSellRequested && !initSellResult && !initSellError) {
return renderSellDeviceConfirmation({ modelId, type });
return renderSecureTransferDeviceConfirmation({ exchangeType: "sell", modelId, type });
}

if (allowOpeningRequestedWording || requestOpenApp) {
Expand Down
8 changes: 5 additions & 3 deletions src/renderer/components/DeviceAction/rendering.js
Original file line number Diff line number Diff line change
Expand Up @@ -733,21 +733,23 @@ export const renderSwapDeviceConfirmationV2 = ({
);
};

export const renderSellDeviceConfirmation = ({
export const renderSecureTransferDeviceConfirmation = ({
exchangeType,
modelId,
type,
}: {
exchangeType: "sell" | "fund",
modelId: DeviceModelId,
type: "light" | "dark",
}) => (
<>
<Alert type="primary" learnMoreUrl={urls.swap.learnMore} horizontal={false}>
<Trans i18nKey="DeviceAction.sell.notice" />
<Trans i18nKey={`DeviceAction.${exchangeType}.notice`} />
</Alert>
{renderVerifyUnwrapped({ modelId, type })}
<Box alignItems={"center"}>
<Text textAlign="center" ff="Inter|SemiBold" color="palette.text.shade100" fontSize={5}>
<Trans i18nKey="DeviceAction.sell.confirm" />
<Trans i18nKey={`DeviceAction.${exchangeType}.confirm`} />
</Text>
</Box>
</>
Expand Down
124 changes: 121 additions & 3 deletions src/renderer/components/WebPlatformPlayer/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { useTranslation } from "react-i18next";
import { getEnv } from "@ledgerhq/live-common/lib/env";
import type { AppManifest } from "@ledgerhq/live-common/lib/platform/types";
import { useToasts } from "@ledgerhq/live-common/lib/notifications/ToastProvider";
import { addPendingOperation } from "@ledgerhq/live-common/lib/account";
import { addPendingOperation, getMainAccount } from "@ledgerhq/live-common/lib/account";
import { listSupportedCurrencies } from "@ledgerhq/live-common/lib/currencies";
import type { ThemedComponent } from "~/renderer/styles/StyleProvider";

Expand Down Expand Up @@ -135,6 +135,7 @@ const WebPlatformPlayer = ({ manifest, onClose, inputs, config }: Props) => {
const account = accounts.find(account => account.id === accountId);
tracking.platformReceiveRequested(manifest);

// FIXME: handle address rejection (if user reject address, we don't end up in onResult nor in onCancel 🤔)
return new Promise((resolve, reject) =>
dispatch(
openModal("MODAL_EXCHANGE_CRYPTO_DEVICE", {
Expand Down Expand Up @@ -172,6 +173,7 @@ const WebPlatformPlayer = ({ manifest, onClose, inputs, config }: Props) => {

let optimisticOperation = signedOperation.operation;

// FIXME: couldn't we use `useBroadcast` here?
if (!getEnv("DISABLE_TRANSACTION_BROADCAST")) {
try {
optimisticOperation = await bridge.broadcast({
Expand Down Expand Up @@ -287,6 +289,118 @@ const WebPlatformPlayer = ({ manifest, onClose, inputs, config }: Props) => {
[manifest, dispatch, accounts],
);

const startExchange = useCallback(
({ exchangeType }: { exchangeType: number }) => {
tracking.platformStartExchangeRequested(manifest);
return new Promise((resolve, reject) =>
dispatch(
openModal("MODAL_PLATFORM_EXCHANGE_START", {
exchangeType,
onResult: nonce => {
tracking.platformStartExchangeSuccess(manifest);
resolve(nonce);
},
onCancel: error => {
tracking.platformStartExchangeFail(manifest);
reject(error);
},
}),
),
);
},
[manifest, dispatch],
);

const completeExchange = useCallback(
({
provider,
fromAccountId,
toAccountId,
transaction,
binaryPayload,
signature,
feesStrategy,
exchangeType,
}: {
provider: string,
fromAccountId: string,
toAccountId: string,
transaction: RawPlatformTransaction,
binaryPayload: string,
signature: string,
feesStrategy: string,
exchangeType: number,
}) => {
// Nb get a hold of the actual accounts, and parent accounts
const fromAccount = accounts.find(a => a.id === fromAccountId);
let fromParentAccount;

const toAccount = accounts.find(a => a.id === toAccountId);
let toParentAccount;

if (!fromAccount) {
return null;
}

if (exchangeType === 0x00 && !toAccount) {
// if we do a swap, a destination account must be provided
return null;
}

if (fromAccount.type === "TokenAccount") {
fromParentAccount = accounts.find(a => a.id === fromAccount.parentId);
}
if (toAccount && toAccount.type === "TokenAccount") {
toParentAccount = accounts.find(a => a.id === toAccount.parentId);
}

const accountBridge = getAccountBridge(fromAccount, fromParentAccount);
const mainFromAccount = getMainAccount(fromAccount, fromParentAccount);

transaction.family = mainFromAccount.currency.family;

const platformTransaction = deserializePlatformTransaction(transaction);

platformTransaction.feesStrategy = feesStrategy;

let processedTransaction = accountBridge.createTransaction(mainFromAccount);
processedTransaction = accountBridge.updateTransaction(
processedTransaction,
platformTransaction,
);

tracking.platformCompleteExchangeRequested(manifest);
return new Promise((resolve, reject) =>
dispatch(
openModal("MODAL_PLATFORM_EXCHANGE_COMPLETE", {
provider,
exchange: {
fromAccount,
fromParentAccount,
toAccount,
toParentAccount,
},
transaction: processedTransaction,
binaryPayload,
signature,
feesStrategy,
exchangeType,

onResult: operation => {
tracking.platformCompleteExchangeSuccess(manifest);
resolve(operation);
},
onCancel: error => {
tracking.platformCompleteExchangeFail(manifest);
reject(error);
},
}),
),
);
},
[accounts, dispatch, manifest],
);

const handlers = useMemo(
() => ({
"account.list": listAccounts,
Expand All @@ -295,14 +409,18 @@ const WebPlatformPlayer = ({ manifest, onClose, inputs, config }: Props) => {
"account.receive": receiveOnAccount,
"transaction.sign": signTransaction,
"transaction.broadcast": broadcastTransaction,
"exchange.start": startExchange,
"exchange.complete": completeExchange,
}),
[
listAccounts,
listCurrencies,
requestAccount,
receiveOnAccount,
signTransaction,
broadcastTransaction,
requestAccount,
listCurrencies,
startExchange,
completeExchange,
],
);

Expand Down
29 changes: 29 additions & 0 deletions src/renderer/components/WebPlatformPlayer/tracking.js
Original file line number Diff line number Diff line change
Expand Up @@ -92,3 +92,32 @@ export function platformBroadcastSuccess(manifest: AppManifest) {
export function platformBroadcastOperationDetailsClick(manifest: AppManifest) {
track("Platform Broadcast OpD Clicked", getEventData(manifest));
}

// Generate Exchange nonce modal open
export function platformStartExchangeRequested(manifest: AppManifest) {
track("Platform start Exchange Nonce request", getEventData(manifest));
}

// Successfully generated an Exchange app nonce
export function platformStartExchangeSuccess(manifest: AppManifest) {
track("Platform start Exchange Nonce success", getEventData(manifest));
}

// Failed to generate an Exchange app nonce
export function platformStartExchangeFail(manifest: AppManifest) {
track("Platform start Exchange Nonce fail", getEventData(manifest));
}

export function platformCompleteExchangeRequested(manifest: AppManifest) {
track("Platform complete Exchange requested", getEventData(manifest));
}

// Successfully completed an Exchange
export function platformCompleteExchangeSuccess(manifest: AppManifest) {
track("Platform complete Exchange success", getEventData(manifest));
}

// Failed to complete an Exchange
export function platformCompleteExchangeFail(manifest: AppManifest) {
track("Platform complete Exchange Nonce fail", getEventData(manifest));
}
Loading