Skip to content

Commit 6be31b8

Browse files
committed
feat(LIVE-29447): add wallet best quote api
1 parent 28306d5 commit 6be31b8

8 files changed

Lines changed: 188 additions & 1 deletion

File tree

.changeset/clever-otters-greet.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@ledgerhq/wallet-api-exchange-module": minor
3+
"@ledgerhq/live-common": minor
4+
---
5+
6+
add wallet-api method to fetch the best swap quote

libs/exchange-module/src/index.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ import {
1010
ExchangeSwapParams,
1111
SwapResult,
1212
SwapLiveError,
13+
type GetBestQuoteResponse,
14+
type GetBestQuoteWireArgs,
1315
type GetQuotesResponse,
1416
type GetQuotesWireArgs,
1517
type GetTransactionStatusResponse,
@@ -189,6 +191,16 @@ export class ExchangeModule extends CustomModule {
189191
return this.request<GetQuotesWireArgs, GetQuotesResponse>("custom.exchange.getQuotes", params);
190192
}
191193

194+
/**
195+
* Fetch the best swap quote from the Ledger swap backend (via Wallet API host).
196+
*/
197+
async getBestQuote(params: GetBestQuoteWireArgs): Promise<GetBestQuoteResponse> {
198+
return this.request<GetBestQuoteWireArgs, GetBestQuoteResponse>(
199+
"custom.exchange.getBestQuote",
200+
params,
201+
);
202+
}
203+
192204
/**
193205
* Fetch swap transaction status from the Wallet API host.
194206
*/

libs/exchange-module/src/types.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,9 @@ export type GetQuotesArgs = {
139139

140140
export type GetQuotesWireArgs = Omit<GetQuotesArgs, "signal">;
141141

142+
export type GetBestQuoteArgs = GetQuotesArgs;
143+
export type GetBestQuoteWireArgs = GetQuotesWireArgs;
144+
142145
// Swap transaction status (`custom.exchange.getTransactionStatus`).
143146

144147
export type TransactionStatusInput = {
@@ -468,3 +471,10 @@ export type GetQuotesResponse = {
468471
*/
469472
errors: QuotesError[];
470473
};
474+
475+
export type GetBestQuoteResponse =
476+
| Quote
477+
| {
478+
providerErrors: QuoteProviderError[];
479+
errors: QuotesError[];
480+
};
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import { getBestQuote } from "./getBestQuote";
2+
import { getQuotes, type GetQuotesContext } from "./getQuotes";
3+
import {
4+
ProviderErrorCodes,
5+
QuotesErrorCodes,
6+
type GetBestQuoteArgs,
7+
type GetQuotesResponse,
8+
type Quote,
9+
} from "./types";
10+
11+
jest.mock("./getQuotes", () => ({
12+
getQuotes: jest.fn(),
13+
}));
14+
15+
const getQuotesMock = jest.mocked(getQuotes);
16+
17+
function makeQuote(key: string): Quote {
18+
return {
19+
key,
20+
provider: "lifi",
21+
providerDetails: {
22+
name: "LiFi",
23+
type: "DEX",
24+
isUniswapX: false,
25+
requiresKYC: false,
26+
continuesInProviderLiveApp: false,
27+
},
28+
quoteDetails: {
29+
type: "float",
30+
sendAmount: 1,
31+
receiveAmount: 1,
32+
gasLess: false,
33+
networkFees: { currencyId: "ethereum" },
34+
slippage: 1,
35+
exchangeRate: 1,
36+
},
37+
warnings: [],
38+
errors: [],
39+
};
40+
}
41+
42+
const args: GetBestQuoteArgs = {
43+
providers: ["lifi"],
44+
data: {
45+
amount: "1",
46+
sendAccountId: "send-account",
47+
receiveAccountId: "receive-account",
48+
sendAddress: "0xfrom",
49+
receiveAddress: "0xto",
50+
sendCurrencyId: "ethereum",
51+
receiveCurrencyId: "bitcoin",
52+
},
53+
sortBy: "netCounterValue",
54+
};
55+
56+
const context: GetQuotesContext = {
57+
accounts: [],
58+
spotPrices: { bitcoin: 30_000, ethereum: 2_000 },
59+
locale: "en",
60+
counterValueCurrency: "usd",
61+
};
62+
63+
describe("getBestQuote", () => {
64+
beforeEach(() => {
65+
jest.clearAllMocks();
66+
});
67+
68+
it("returns the first quote from the sorted getQuotes response", async () => {
69+
const bestQuote = makeQuote("best");
70+
const response: GetQuotesResponse = {
71+
quotes: [bestQuote, makeQuote("second")],
72+
providerErrors: [],
73+
warnings: [],
74+
errors: [],
75+
};
76+
getQuotesMock.mockResolvedValue(response);
77+
78+
await expect(getBestQuote(args, context)).resolves.toBe(bestQuote);
79+
expect(getQuotesMock).toHaveBeenCalledWith(args, context);
80+
});
81+
82+
it("returns quote diagnostics when getQuotes has no quotes", async () => {
83+
const response: GetQuotesResponse = {
84+
quotes: [],
85+
providerErrors: [
86+
{
87+
code: ProviderErrorCodes.AMOUNT_OFF_LIMITS,
88+
type: "float",
89+
provider: "lifi",
90+
message: "min",
91+
parameter: { minAmount: "10" },
92+
},
93+
],
94+
warnings: [],
95+
errors: [
96+
{ code: QuotesErrorCodes.NO_QUOTES },
97+
{ code: QuotesErrorCodes.AMOUNT_TOO_LOW, minAmount: "10" },
98+
],
99+
};
100+
getQuotesMock.mockResolvedValue(response);
101+
102+
await expect(getBestQuote(args, context)).resolves.toEqual({
103+
providerErrors: response.providerErrors,
104+
errors: response.errors,
105+
});
106+
});
107+
});
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { getQuotes, type GetQuotesContext } from "./getQuotes";
2+
import type { GetBestQuoteArgs, GetBestQuoteResponse } from "./types";
3+
4+
export async function getBestQuote(
5+
args: GetBestQuoteArgs,
6+
context: GetQuotesContext,
7+
): Promise<GetBestQuoteResponse> {
8+
const response = await getQuotes(args, context);
9+
const [bestQuote] = response.quotes;
10+
11+
if (bestQuote) {
12+
return bestQuote;
13+
}
14+
15+
return {
16+
providerErrors: response.providerErrors,
17+
errors: response.errors,
18+
};
19+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
export type * from "./types";
2+
export { getBestQuote } from "./getBestQuote";
23
export { getQuotes } from "./getQuotes";
34
export type { GetQuotesContext } from "./getQuotes";

libs/ledger-live-common/src/wallet-api/Exchange/quotes/types.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ export type {
66
QuotesInput,
77
QuotesAppPlatform,
88
QuoteSortBy,
9+
GetBestQuoteArgs,
10+
GetBestQuoteWireArgs,
11+
GetBestQuoteResponse,
912
GetQuotesArgs,
1013
GetQuotesWireArgs,
1114
GetQuotesResponse,

libs/ledger-live-common/src/wallet-api/Exchange/server.ts

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ import {
2727
ExchangeType,
2828
SwapLiveError,
2929
SwapResult,
30+
type GetBestQuoteResponse,
31+
type GetBestQuoteWireArgs,
3032
type GetQuotesResponse,
3133
type GetQuotesWireArgs,
3234
} from "@ledgerhq/wallet-api-exchange-module";
@@ -61,7 +63,7 @@ import { createStepError, StepError, toError } from "./parser";
6163
import { handleErrors } from "./handleSwapErrors";
6264
import get from "lodash/get";
6365
import { SwapError } from "./SwapError";
64-
import { getQuotes } from "./quotes";
66+
import { getBestQuote, getQuotes } from "./quotes";
6567
import { resolveQuotesInput } from "./quotes/resolveQuotesInput";
6668
import { fetchSpotPrices } from "./quotes/service/fetchSpotPrices";
6769
import {
@@ -82,6 +84,7 @@ type Handlers = {
8284
"custom.isReady": RPCHandler<void, void>;
8385
"custom.exchange.swap": RPCHandler<SwapResult, ExchangeSwapParams>;
8486
"custom.exchange.getQuotes": RPCHandler<GetQuotesResponse, GetQuotesWireArgs>;
87+
"custom.exchange.getBestQuote": RPCHandler<GetBestQuoteResponse, GetBestQuoteWireArgs>;
8588
"custom.exchange.getTransactionStatus": RPCHandler<
8689
GetTransactionStatusResponse,
8790
GetTransactionStatusWireArgs
@@ -800,6 +803,32 @@ export const handlers = ({
800803
},
801804
),
802805

806+
"custom.exchange.getBestQuote": customWrapper<GetBestQuoteWireArgs, GetBestQuoteResponse>(
807+
async params => {
808+
if (!params) {
809+
throw new ServerError(createUnknownError({ message: "params is undefined" }));
810+
}
811+
const quotesInput = resolveQuotesInput(params.data, accounts);
812+
const spotPrices = await fetchSpotPrices({
813+
currencyIds: quotesInput
814+
? [
815+
quotesInput.sendCurrencyId,
816+
quotesInput.receiveCurrencyId,
817+
quotesInput.networkFeesCurrencyId,
818+
]
819+
: [],
820+
counterValue: counterValueCurrency,
821+
});
822+
return getBestQuote(params, {
823+
accounts,
824+
spotPrices,
825+
locale,
826+
counterValueCurrency,
827+
deviceModelId,
828+
});
829+
},
830+
),
831+
803832
"custom.exchange.getTransactionStatus": customWrapper<
804833
GetTransactionStatusWireArgs,
805834
GetTransactionStatusResponse

0 commit comments

Comments
 (0)