diff --git a/lib/src/api.test.ts b/lib/src/api.test.ts index d41257d..8f50a68 100644 --- a/lib/src/api.test.ts +++ b/lib/src/api.test.ts @@ -10,7 +10,7 @@ const mockPost = jest.fn(); }; }); -import { cancelSwap, confirmSwap, retriveSwapPayload } from "./api"; +import { cancelSwap, confirmSwap, retrieveSwapPayload } from "./api"; describe("retrieveSwapPayload", () => { afterEach(() => { @@ -32,7 +32,7 @@ describe("retrieveSwapPayload", () => { mockPost.mockResolvedValueOnce({ data: responseData }); // WHEN - const result = await retriveSwapPayload(data); + const result = await retrieveSwapPayload(data); // THEN const expectedResult = { @@ -72,7 +72,7 @@ describe("retrieveSwapPayload", () => { mockPost.mockResolvedValueOnce({ data: responseData }); // WHEN - const result = await retriveSwapPayload(data); + const result = await retrieveSwapPayload(data); // THEN expect(result).not.toBeUndefined(); diff --git a/lib/src/api.ts b/lib/src/api.ts index 7f689c3..10c8221 100644 --- a/lib/src/api.ts +++ b/lib/src/api.ts @@ -2,21 +2,29 @@ import axios from "axios"; import { Account } from "@ledgerhq/wallet-api-client"; import BigNumber from "bignumber.js"; import { decodeSellPayload } from "@ledgerhq/hw-app-exchange"; -import { BEData } from "./sdk"; +import { BEData, ExchangeType } from "./sdk"; const SWAP_BACKEND_URL = "https://swap.ledger.com/v5/swap"; -const SELL_BACKEND_URL = "https://buy.api.aws.prd.ldg-tech.com/sell/v1"; +const SELL_BACKEND_URL = "https://buy.api.aws.prd.ldg-tech.com/"; -let axiosClient = axios.create({ + +let swapAxiosClient = axios.create({ baseURL: SWAP_BACKEND_URL, }); +let sellAxiosClient = axios.create({ + baseURL: SELL_BACKEND_URL, +}); + /** * Override the default axios client base url environment (default is production) * @param {string} url */ export function setBackendUrl(url: string) { - axiosClient = axios.create({ + swapAxiosClient = axios.create({ + baseURL: url, + }); + sellAxiosClient = axios.create({ baseURL: url, }); } @@ -43,7 +51,7 @@ export type SwapPayloadResponse = { payinExtraId?: string; }; -export async function retriveSwapPayload( +export async function retrieveSwapPayload( data: SwapPayloadRequestData ): Promise { const request = { @@ -57,7 +65,7 @@ export async function retriveSwapPayload( amountFromInSmallestDenomination: Number(data.amountInAtomicUnit), rateId: data.quoteId, }; - const res = await axiosClient.post("", request); + const res = await swapAxiosClient.post("", request); return parseSwapBackendInfo(res.data); } @@ -71,8 +79,19 @@ export type ConfirmSwapRequest = { hardwareWalletType?: string; }; +export type ConfirmSellRequest = { + provider: string; + quoteId: string; + transactionId: string; +}; + export async function confirmSwap(payload: ConfirmSwapRequest) { - await axiosClient.post("accepted", payload); + await swapAxiosClient.post("accepted", payload); +} + +export async function confirmSell(data: ConfirmSellRequest) { + const { quoteId, ...payload } = data + await sellAxiosClient.post(`/webhook/v1/transaction/${quoteId}/accepted`, payload); } export type CancelSwapRequest = { @@ -87,8 +106,20 @@ export type CancelSwapRequest = { swapStep?: string; }; +export type CancelSellRequest = { + provider: string; + quoteId: string; + statusCode?: string; + errorMessage?: string; +}; + export async function cancelSwap(payload: CancelSwapRequest) { - await axiosClient.post("cancelled", payload); + await swapAxiosClient.post("cancelled", payload); +} + +export async function cancelSell(data: CancelSellRequest) { + const {quoteId, ...payload} = data + await sellAxiosClient.post(`/webhook/v1/transaction/${quoteId}/cancelled`, payload); } type SwapBackendResponse = { @@ -139,6 +170,7 @@ export interface SellRequestPayload { amountFrom: number; amountTo: number; nonce: string; + type: string; } export interface SellResponsePayload { @@ -156,7 +188,7 @@ export interface SellResponsePayload { const parseSellBackendInfo = (response: SellResponsePayload) => { return { - sellId: response.sellId, + quoteId: response.sellId, payinAddress: response.payinAddress, providerSig: { payload: response.providerSig.payload, @@ -165,7 +197,7 @@ const parseSellBackendInfo = (response: SellResponsePayload) => { }; }; -export async function retriveSellPayload(data: SellRequestPayload) { +export async function retrieveSellPayload(data: SellRequestPayload) { const request = { quoteId: data.quoteId, provider: data.provider, @@ -176,8 +208,8 @@ export async function retriveSellPayload(data: SellRequestPayload) { amountTo: data.amountTo, nonce: data.nonce, }; - const res = await axiosClient.post("/sell", request); - + const pathname = data.type === ExchangeType.SELL ? "sell/v1/remit" : "card/v1/remit"; + const res = await sellAxiosClient.post(pathname, request); return parseSellBackendInfo(res.data); } @@ -205,7 +237,7 @@ export async function decodeSellPayloadAndPost( referralFee: null, }; - axiosClient.post("/forgeTransaction/offRamp", payload); + sellAxiosClient.post("/forgeTransaction/offRamp", payload); } catch (e) { console.log("Error decoding payload", e); } diff --git a/lib/src/sdk.test.ts b/lib/src/sdk.test.ts index b37bb31..cd4b43c 100644 --- a/lib/src/sdk.test.ts +++ b/lib/src/sdk.test.ts @@ -9,10 +9,12 @@ import { ExchangeModule } from "@ledgerhq/wallet-api-exchange-module"; import { AccountModule } from "@ledgerhq/wallet-api-client/lib/modules/Account"; import { CurrencyModule } from "@ledgerhq/wallet-api-client/lib/modules/Currency"; import { - retriveSwapPayload, + retrieveSwapPayload, confirmSwap, cancelSwap, - retriveSellPayload, + retrieveSellPayload, + confirmSell, + cancelSell, } from "./api"; import { ExchangeSDK, FeeStrategy } from "./sdk"; import { getCustomModule } from "./wallet-api"; @@ -89,7 +91,7 @@ beforeEach(() => { describe("swap", () => { beforeAll(() => { - (retriveSwapPayload as jest.Mock).mockResolvedValue({ + (retrieveSwapPayload as jest.Mock).mockResolvedValue({ binaryPayload: "", signature: "", payinAddress: "", @@ -217,6 +219,16 @@ describe("swap", () => { }); describe("sell", () => { + beforeAll(() => { + (retrieveSellPayload as jest.Mock).mockResolvedValue({ + binaryPayload: "", + signature: "", + payinAddress: "", + quoteId: "sell-id", + }); + (confirmSell as jest.Mock).mockResolvedValue({}); + (cancelSell as jest.Mock).mockResolvedValue({}); + }); it("sends back the 'transactionId' from the WalletAPI", async () => { // GIVEN const currencies: Array> = [ @@ -265,8 +277,8 @@ describe("sell", () => { ]; mockCurrenciesList.mockResolvedValue(currencies as any); - // Mock `retriveSellPayload` since `getSellPayload` is not provided - (retriveSellPayload as jest.Mock).mockResolvedValue({ + // Mock `retrieveSellPayload` since `getSellPayload` is not provided + (retrieveSellPayload as jest.Mock).mockResolvedValue({ payinAddress: "0xfff", providerSig: { payload: Buffer.from(""), diff --git a/lib/src/sdk.ts b/lib/src/sdk.ts index 714b5af..fad5e24 100644 --- a/lib/src/sdk.ts +++ b/lib/src/sdk.ts @@ -12,9 +12,11 @@ import { ExchangeModule } from "@ledgerhq/wallet-api-exchange-module"; import { cancelSwap, confirmSwap, + cancelSell, + confirmSell, decodeSellPayloadAndPost, - retriveSellPayload, - retriveSwapPayload, + retrieveSellPayload, + retrieveSwapPayload, setBackendUrl, } from "./api"; import { @@ -31,7 +33,23 @@ import walletApiDecorator, { getCustomModule, } from "./wallet-api"; -export type GetSwapPayload = typeof retriveSwapPayload; +export type FeeStrategy = "SLOW" | "MEDIUM" | "FAST" | "CUSTOM"; + +enum FeeStrategyEnum { + SLOW = "SLOW", + MEDIUM = "MEDIUM", + FAST = "FAST", + CUSTOM = "CUSTOM", +} + +export const ExchangeType = { + FUND: "FUND", + SELL: "SELL", + SWAP: "SWAP", + CARD: "CARD" +} as const; + +export type GetSwapPayload = typeof retrieveSwapPayload; /** * Swap information required to request a user's swap transaction. @@ -78,23 +96,9 @@ export type SellInfo = { rate?: number; customFeeConfig?: { [key: string]: BigNumber }; getSellPayload?: GetSellPayload; + type?: string; }; -export type FeeStrategy = "SLOW" | "MEDIUM" | "FAST" | "CUSTOM"; - -enum FeeStrategyEnum { - SLOW = "SLOW", - MEDIUM = "MEDIUM", - FAST = "FAST", - CUSTOM = "CUSTOM", -} - -export const ExchangeType = { - FUND: "FUND", - SELL: "SELL", - SWAP: "SWAP", -} as const; - /** * ExchangeSDK allows you to send a swap request to a Ledger Device through a Ledger Live request. * Under the hood, it relies on {@link https://github.com/LedgerHQ/wallet-api WalletAPI}. @@ -202,7 +206,7 @@ export class ExchangeSDK { this.logger.debug("DeviceTransactionId retrieved:", deviceTransactionId); // Step 2: Ask for payload creation - const payloadRequest = getSwapPayload ?? retriveSwapPayload; + const payloadRequest = getSwapPayload ?? retrieveSwapPayload; const { binaryPayload, signature, payinAddress, swapId, payinExtraId } = await payloadRequest({ provider: this.providerId, @@ -309,6 +313,7 @@ export class ExchangeSDK { rate, toFiat, getSellPayload, + type = ExchangeType.SELL } = info; const { account, currency } = @@ -341,9 +346,10 @@ export class ExchangeSDK { account, deviceTransactionId, initialAtomicAmount, + type }); - if (this.providerId === "coinify") { + if (getSellPayload) { await decodeSellPayloadAndPost( binaryPayload as string, beData as BEData, @@ -367,6 +373,15 @@ export class ExchangeSDK { amount: fromAmountAtomic, currency, customFeeConfig, + }) + .catch(async (error) => { + await this.cancelSellOnError({ + error, + quoteId, + }); + + this.handleError(error); + throw error; }); const tx = await this.exchangeModule @@ -378,15 +393,30 @@ export class ExchangeSDK { signature, feeStrategy, }) - .catch((error: Error) => { + .catch(async(error: Error) => { + await this.cancelSellOnError({ + error, + quoteId, + }); + + if (error.name === "DisabledTransactionBroadcastError") { + throw error; + } + const err = new SignatureStepError(error); - this.logger.error(err); + this.handleError(err); throw err; }); this.logger.log("Transaction sent:", tx); this.logger.log("*** End Sell ***"); - + await confirmSell({ + provider: this.providerId, + quoteId: quoteId ?? "", + transactionId: tx, + }).catch((error: Error) => { + this.logger.error(error); + }); return tx; } @@ -452,6 +482,23 @@ export class ExchangeSDK { }); } + private async cancelSellOnError({ + error, + quoteId, + }: { + error: Error, + quoteId?: string, + }) { + await cancelSell({ + provider: this.providerId, + quoteId: quoteId ?? "", + statusCode: error.name, + errorMessage: error.message, + }).catch((cancelError: Error) => { + this.logger.error(cancelError); + }); + } + private async sellPayloadRequest({ account, getSellPayload, @@ -461,6 +508,7 @@ export class ExchangeSDK { amount, deviceTransactionId, initialAtomicAmount, + type, }: { amount: BigNumber; getSellPayload?: GetSellPayload; @@ -470,6 +518,7 @@ export class ExchangeSDK { quoteId?: string; rate?: number; toFiat?: string; + type: string; }) { let recipientAddress: string; let binaryPayload: string; @@ -494,7 +543,7 @@ export class ExchangeSDK { signature = data.signature; beData = data.beData; } else { - const data = await retriveSellPayload({ + const data = await retrieveSellPayload({ quoteId: quoteId!, provider: this.providerId, fromCurrency: account.currency, @@ -503,6 +552,7 @@ export class ExchangeSDK { amountFrom: amount.toNumber(), amountTo: rate! * amount.toNumber(), nonce: deviceTransactionId, + type, }).catch((error: Error) => { const err = new PayloadStepError(error); this.handleError(err);