diff --git a/packages/demo-wallet-client/src/components/AnchorQuotesModal.tsx b/packages/demo-wallet-client/src/components/AnchorQuotesModal.tsx index b364383e..00231798 100644 --- a/packages/demo-wallet-client/src/components/AnchorQuotesModal.tsx +++ b/packages/demo-wallet-client/src/components/AnchorQuotesModal.tsx @@ -9,15 +9,18 @@ import { postSep38QuoteAction, } from "ducks/sep38Quotes"; import { AppDispatch } from "config/store"; -import { ActionStatus } from "types/types"; +import { ActionStatus, AnchorBuyAsset } from "types/types"; interface AnchorQuotesModalProps { token: string; + context: "sep31" | "sep6"; + isDeposit: boolean; onClose: () => void; onSubmit: ( event: React.MouseEvent, quoteId?: string, - destinationAsset?: string, + buyAssset?: string, + sellAsset?: string, ) => void; } @@ -30,6 +33,8 @@ type QuoteAsset = { export const AnchorQuotesModal = ({ token, + context, + isDeposit, onClose, onSubmit, }: AnchorQuotesModalProps) => { @@ -39,6 +44,8 @@ export const AnchorQuotesModal = ({ const [quoteAsset, setQuoteAsset] = useState(); const [assetBuyDeliveryMethod, setAssetBuyDeliveryMethod] = useState(); + const [assetSellDeliveryMethod, setAssetSellDeliveryMethod] = + useState(); const [assetCountryCode, setAssetCountryCode] = useState(); const [assetPrice, setAssetPrice] = useState(); @@ -49,47 +56,122 @@ export const AnchorQuotesModal = ({ return new BigNumber(amount).div(rate).toFixed(2); }; - // Exclude sell asset from quote assets - const renderAssets = data.sellAsset - ? data.assets.filter((a) => a.asset !== data.sellAsset) - : []; + const getRenderAssets = () => { + let selectedAsset: string | undefined; + + if (context === "sep31") { + selectedAsset = isDeposit ? data.sellAsset : data.buyAsset; + } else if (context === "sep6") { + selectedAsset = isDeposit ? data.buyAsset : data.sellAsset; + } + + // Exclude selected asset from quote assets + return selectedAsset + ? data.assets.filter((a) => a.asset !== selectedAsset) + : []; + }; + + const renderAssets = getRenderAssets(); const handleGetAssetRates = () => { - if (data.serverUrl && data.sellAsset && data.sellAmount) { - dispatch( - fetchSep38QuotesPricesAction({ - token, - anchorQuoteServerUrl: data.serverUrl, - options: { - sellAsset: data.sellAsset, - sellAmount: data.sellAmount, - buyDeliveryMethod: assetBuyDeliveryMethod, - countryCode: assetCountryCode, - }, - }), - ); + if (context === "sep31") { + if (data.serverUrl && data.sellAsset && data.amount) { + dispatch( + fetchSep38QuotesPricesAction({ + token, + anchorQuoteServerUrl: data.serverUrl, + options: { + sellAsset: data.sellAsset, + sellAmount: data.amount, + buyDeliveryMethod: assetBuyDeliveryMethod, + sellDeliveryMethod: assetSellDeliveryMethod, + countryCode: assetCountryCode, + }, + }), + ); + } + } else if (context === "sep6") { + const sellAsset = isDeposit ? quoteAsset?.asset : data.sellAsset; + + if (data.serverUrl && data.amount && sellAsset) { + dispatch( + fetchSep38QuotesPricesAction({ + token, + anchorQuoteServerUrl: data.serverUrl, + options: { + sellAsset, + sellAmount: data.amount, + buyDeliveryMethod: assetBuyDeliveryMethod, + sellDeliveryMethod: assetSellDeliveryMethod, + countryCode: assetCountryCode, + }, + }), + ); + } } }; const handleGetQuote = () => { - if ( - data.serverUrl && - data.sellAsset && - data.sellAmount && - quoteAsset?.asset - ) { - dispatch( - postSep38QuoteAction({ - token, - anchorQuoteServerUrl: data.serverUrl, - sell_asset: data.sellAsset, - buy_asset: quoteAsset.asset, - sell_amount: data.sellAmount, - buy_delivery_method: assetBuyDeliveryMethod, - country_code: assetCountryCode, - context: "sep31", - }), - ); + if (context === "sep31") { + if ( + data.serverUrl && + data.sellAsset && + data.amount && + quoteAsset?.asset + ) { + dispatch( + postSep38QuoteAction({ + token, + anchorQuoteServerUrl: data.serverUrl, + sell_asset: data.sellAsset, + buy_asset: quoteAsset.asset, + sell_amount: data.amount, + buy_delivery_method: assetBuyDeliveryMethod, + sell_delivery_method: assetSellDeliveryMethod, + country_code: assetCountryCode, + context, + }), + ); + } + } else if (context === "sep6") { + if (isDeposit) { + if ( + data.serverUrl && + data.buyAsset && + data.amount && + quoteAsset?.asset + ) { + dispatch( + postSep38QuoteAction({ + token, + anchorQuoteServerUrl: data.serverUrl, + sell_asset: quoteAsset.asset, + buy_asset: data.buyAsset, + sell_amount: data.amount, + buy_delivery_method: assetBuyDeliveryMethod, + sell_delivery_method: assetSellDeliveryMethod, + country_code: assetCountryCode, + context, + }), + ); + } + } else { + if (data.serverUrl && data.sellAsset && quoteAsset?.asset) { + dispatch( + postSep38QuoteAction({ + token, + anchorQuoteServerUrl: data.serverUrl, + sell_asset: data.sellAsset, + buy_asset: quoteAsset.asset, + sell_amount: data.amount, + buy_delivery_method: assetBuyDeliveryMethod, + sell_delivery_method: assetSellDeliveryMethod, + country_code: assetCountryCode, + context, + }), + ); + } + } } }; @@ -158,7 +240,14 @@ export const AnchorQuotesModal = ({ @@ -167,9 +256,28 @@ export const AnchorQuotesModal = ({ ); } - if (data.prices?.length > 0) { - const sellAssetCode = data.sellAsset?.split(":")[1]; - const buyAssetCode = quoteAsset?.asset.split(":")[1]; + if (data.prices?.length > 0 && quoteAsset?.asset) { + let sellAssetCode: string | undefined; + let buyAssetCode: string | undefined; + let prices: AnchorBuyAsset[] = []; + + if (context === "sep31") { + sellAssetCode = data.sellAsset?.split(":")[1]; + buyAssetCode = quoteAsset?.asset.split(":")[1]; + prices = data.prices.filter((p) => p.asset === quoteAsset.asset); + } + + if (context === "sep6") { + if (isDeposit) { + sellAssetCode = data.buyAsset?.split(":")[1]; + buyAssetCode = quoteAsset?.asset.split(":")[1]; + prices = data.prices.filter((p) => p.asset === data.buyAsset); + } else { + sellAssetCode = data.sellAsset?.split(":")[1]; + buyAssetCode = quoteAsset?.asset.split(":")[1]; + prices = data.prices.filter((p) => p.asset === quoteAsset.asset); + } + } return ( <> @@ -177,7 +285,7 @@ export const AnchorQuotesModal = ({

Rates (not final)

- {data.prices.map((p) => ( + {prices.map((p) => ( - {data.sellAmount && assetPrice ? ( + {data.amount && assetPrice ? (
{`Estimated total of ${calculateTotal( - data.sellAmount, + data.amount, assetPrice, - )} ${buyAssetCode} for ${ - data.sellAmount - } ${sellAssetCode}`}
+ )} ${buyAssetCode} for ${data.amount} ${sellAssetCode}`}
) : null} @@ -227,47 +333,88 @@ export const AnchorQuotesModal = ({ setQuoteAsset({ asset: a.asset, }); + setAssetCountryCode(""); + setAssetBuyDeliveryMethod(undefined); + setAssetSellDeliveryMethod(undefined); }} checked={a.asset === quoteAsset?.asset} /> {/* TODO: Better UI */}
-
Country codes
-
- {a.country_codes?.map((c) => ( - { - setAssetCountryCode(c); - }} - checked={c === assetCountryCode} - /> - ))} -
- - <> -
Buy delivery methods
-
- {a.buy_delivery_methods?.map((b) => ( - { - setAssetBuyDeliveryMethod(b.name); - }} - checked={b.name === assetBuyDeliveryMethod} - /> - ))} -
- + {a.country_codes && a.country_codes.length > 0 ? ( + <> +
Country codes
+
+ {a.country_codes?.map((c) => ( + { + setAssetCountryCode(c); + }} + checked={ + a.asset === quoteAsset?.asset && + c === assetCountryCode + } + /> + ))} +
+ + ) : null} + + {a.buy_delivery_methods && + a.buy_delivery_methods.length > 0 ? ( + <> +
Buy delivery methods
+
+ {a.buy_delivery_methods?.map((b) => ( + { + setAssetBuyDeliveryMethod(b.name); + }} + checked={ + a.asset === quoteAsset?.asset && + b.name === assetBuyDeliveryMethod + } + /> + ))} +
+ + ) : null} + + {a.sell_delivery_methods && + a.sell_delivery_methods.length > 0 ? ( + <> +
Sell delivery methods
+
+ {a.sell_delivery_methods?.map((b) => ( + { + setAssetSellDeliveryMethod(b.name); + }} + checked={ + a.asset === quoteAsset?.asset && + b.name === assetSellDeliveryMethod + } + /> + ))} +
+ + ) : null}
))} diff --git a/packages/demo-wallet-client/src/components/Sep31Send.tsx b/packages/demo-wallet-client/src/components/Sep31Send.tsx index 125e7625..948af42c 100644 --- a/packages/demo-wallet-client/src/components/Sep31Send.tsx +++ b/packages/demo-wallet-client/src/components/Sep31Send.tsx @@ -22,7 +22,7 @@ import { setStatusAction, } from "ducks/sep31Send"; import { - fetchSep38QuotesInfoAction, + fetchSep38QuotesSep31InfoAction, resetSep38QuotesAction, } from "ducks/sep38Quotes"; @@ -134,10 +134,10 @@ export const Sep31Send = () => { const { assetCode, assetIssuer } = sep31Send.data; dispatch( - fetchSep38QuotesInfoAction({ + fetchSep38QuotesSep31InfoAction({ anchorQuoteServerUrl: sep31Send.data?.anchorQuoteServer, sellAsset: `stellar:${assetCode}:${assetIssuer}`, - sellAmount: formData.amount.amount, + amount: formData.amount.amount, }), ); dispatch(setStatusAction(ActionStatus.ANCHOR_QUOTES)); @@ -241,6 +241,8 @@ export const Sep31Send = () => { return ( diff --git a/packages/demo-wallet-client/src/components/Sep6/Sep6Deposit.tsx b/packages/demo-wallet-client/src/components/Sep6/Sep6Deposit.tsx index 6271f90f..e146eb92 100644 --- a/packages/demo-wallet-client/src/components/Sep6/Sep6Deposit.tsx +++ b/packages/demo-wallet-client/src/components/Sep6/Sep6Deposit.tsx @@ -12,13 +12,20 @@ import { import { CSS_MODAL_PARENT_ID } from "demo-wallet-shared/build/constants/settings"; import { KycField, KycFieldInput } from "components/KycFieldInput"; +import { AnchorQuotesModal } from "components/AnchorQuotesModal"; import { resetActiveAssetAction } from "ducks/activeAsset"; import { resetSep6DepositAction, submitSep6DepositFields, sep6DepositAction, submitSep6CustomerInfoFields, + setStatusAction, + submitSep6DepositWithQuotesFields, } from "ducks/sep6DepositAsset"; +import { + fetchSep38QuotesSep6InfoAction, + resetSep38QuotesAction, +} from "ducks/sep38Quotes"; import { useRedux } from "hooks/useRedux"; import { AppDispatch } from "config/store"; import { ActionStatus } from "types/types"; @@ -81,6 +88,7 @@ export const Sep6Deposit = () => { const handleClose = () => { dispatch(resetSep6DepositAction()); dispatch(resetActiveAssetAction()); + dispatch(resetSep38QuotesAction()); resetLocalState(); }; @@ -121,12 +129,17 @@ export const Sep6Deposit = () => { ) => { const { id, value } = event.target; + let fields = { ...formData.customerFields }; + + if (value) { + fields[id] = value; + } else if (fields[id]) { + delete fields[id]; + } + const updatedState = { ...formData, - customerFields: { - ...formData.customerFields, - [id]: value, - }, + customerFields: fields, }; setFormData(updatedState); @@ -150,6 +163,50 @@ export const Sep6Deposit = () => { dispatch(submitSep6DepositFields({ ...formData })); }; + const handleShowQuotesModal = ( + event: React.MouseEvent, + ) => { + event.preventDefault(); + + if (!formData.amount) { + return; + } + + const { assetCode, assetIssuer } = sep6DepositAsset.data; + + dispatch( + fetchSep38QuotesSep6InfoAction({ + anchorQuoteServerUrl: sep6DepositAsset.data?.anchorQuoteServer, + buyAsset: `stellar:${assetCode}:${assetIssuer}`, + amount: formData.amount, + }), + ); + dispatch(setStatusAction(ActionStatus.ANCHOR_QUOTES)); + }; + + const handleSubmitWithQuotes = ( + event: React.MouseEvent, + quoteId?: string, + buyAsset?: string, + sellAsset?: string, + ) => { + event.preventDefault(); + + if (!(quoteId && buyAsset && sellAsset && formData.amount)) { + return; + } + + dispatch( + submitSep6DepositWithQuotesFields({ + ...formData, + amount: formData.amount, + quoteId, + destinationAssetCode: buyAsset.split(":")[1], + sourceAsset: sellAsset, + }), + ); + }; + const handleSubmitCustomerInfo = ( event: React.MouseEvent, ) => { @@ -167,6 +224,18 @@ export const Sep6Deposit = () => { return `Min: ${minAmount} | Max: ${maxAmount}`; }; + if (sep6DepositAsset.status === ActionStatus.ANCHOR_QUOTES) { + return ( + + ); + } + if (sep6DepositAsset.status === ActionStatus.NEEDS_KYC) { return ( @@ -204,7 +273,7 @@ export const Sep6Deposit = () => { - + @@ -328,6 +397,14 @@ export const Sep6Deposit = () => { + + + + ); + } + if (sep6DepositAsset.data.instructions) { return ( { const handleClose = () => { dispatch(resetSep6WithdrawAction()); dispatch(resetActiveAssetAction()); + dispatch(resetSep38QuotesAction()); resetLocalState(); }; @@ -124,12 +135,17 @@ export const Sep6Withdraw = () => { ) => { const { id, value } = event.target; + let fields = { ...formData.customerFields }; + + if (value) { + fields[id] = value; + } else if (fields[id]) { + delete fields[id]; + } + const updatedState = { ...formData, - customerFields: { - ...formData.customerFields, - [id]: value, - }, + customerFields: fields, }; setFormData(updatedState); @@ -150,13 +166,57 @@ export const Sep6Withdraw = () => { setWithdrawAmount(value); }; - const handleAmountSubmit = ( + const handlePollTransaction = ( event: React.MouseEvent, ) => { event.preventDefault(); dispatch(sep6WithdrawAction(withdrawAmount)); }; + const handleShowQuotesModal = ( + event: React.MouseEvent, + ) => { + event.preventDefault(); + + if (!withdrawAmount) { + return; + } + + const { assetCode, assetIssuer } = sep6WithdrawAsset.data; + + dispatch( + fetchSep38QuotesSep6InfoAction({ + anchorQuoteServerUrl: sep6WithdrawAsset.data?.anchorQuoteServer, + sellAsset: `stellar:${assetCode}:${assetIssuer}`, + amount: withdrawAmount, + }), + ); + dispatch(setStatusAction(ActionStatus.ANCHOR_QUOTES)); + }; + + const handleSubmitWithQuotes = ( + event: React.MouseEvent, + quoteId?: string, + buyAsset?: string, + sellAsset?: string, + ) => { + event.preventDefault(); + + if (!(quoteId && buyAsset && sellAsset && withdrawAmount)) { + return; + } + + dispatch( + submitSep6WithdrawWithQuotesFields({ + ...formData, + amount: withdrawAmount, + quoteId, + destinationAsset: buyAsset, + sourceAssetCode: sellAsset.split(":")[1], + }), + ); + }; + const handleSubmitCustomerInfo = ( event: React.MouseEvent, ) => { @@ -164,6 +224,18 @@ export const Sep6Withdraw = () => { dispatch(submitSep6WithdrawCustomerInfoFields(formData.customerFields)); }; + if (sep6WithdrawAsset.status === ActionStatus.ANCHOR_QUOTES) { + return ( + + ); + } + if (sep6WithdrawAsset.status === ActionStatus.NEEDS_KYC) { return ( @@ -202,7 +274,7 @@ export const Sep6Withdraw = () => { - + @@ -248,6 +320,31 @@ export const Sep6Withdraw = () => { SEP-6 Withdrawal Info + <> + + {withdrawResponse.min_amount || withdrawResponse.max_amount ? ( +
+ {withdrawResponse.min_amount && ( +

+ Min Amount: + {withdrawResponse.min_amount} +

+ )} + {withdrawResponse.max_amount && ( +

+ Max Amount: + {withdrawResponse.max_amount} +

+ )} +
+ ) : null} + + {
+
@@ -319,33 +424,6 @@ export const Sep6Withdraw = () => { ) : null} - {!isRequiredCustomerInfo ? ( - <> - - {withdrawResponse.min_amount || withdrawResponse.max_amount ? ( -
- {withdrawResponse.min_amount && ( -

- Min Amount: - {withdrawResponse.min_amount} -

- )} - {withdrawResponse.max_amount && ( -

- Max Amount: - {withdrawResponse.max_amount} -

- )} -
- ) : null} - - ) : null} - {withdrawResponse.id && (
Transaction ID: @@ -377,7 +455,27 @@ export const Sep6Withdraw = () => { - + + + + ); + } + + if (sep6WithdrawAsset.status === ActionStatus.KYC_DONE) { + return ( + setIsInfoModalVisible(false)} + parentId={CSS_MODAL_PARENT_ID} + > + SEP-6 Withdrawal + + +

Submit the withdrawal.

+
+ + +
); diff --git a/packages/demo-wallet-client/src/ducks/sep38Quotes.ts b/packages/demo-wallet-client/src/ducks/sep38Quotes.ts index e810f93a..fa44b145 100644 --- a/packages/demo-wallet-client/src/ducks/sep38Quotes.ts +++ b/packages/demo-wallet-client/src/ducks/sep38Quotes.ts @@ -19,35 +19,77 @@ import { Sep38QuotesInitialState, } from "types/types"; -export const fetchSep38QuotesInfoAction = createAsyncThunk< +export const fetchSep38QuotesSep31InfoAction = createAsyncThunk< { assets: AnchorQuoteAsset[]; sellAsset: string; - sellAmount: string; + amount: string; serverUrl: string | undefined; }, { anchorQuoteServerUrl: string | undefined; sellAsset: string; - sellAmount: string; + amount: string; }, { rejectValue: RejectMessage; state: RootState } >( - "sep38Quotes/fetchSep38QuotesInfoAction", + "sep38Quotes/fetchSep38QuotesSep31InfoAction", + async ({ anchorQuoteServerUrl, sellAsset, amount }, { rejectWithValue }) => { + try { + const result = await getInfo({ + context: "sep31", + anchorQuoteServerUrl, + options: { sell_amount: amount, sell_asset: sellAsset! }, + }); + + return { + assets: result.assets, + sellAsset, + amount, + serverUrl: anchorQuoteServerUrl, + }; + } catch (error) { + const errorMessage = getErrorMessage(error); + + log.error({ + title: errorMessage, + }); + return rejectWithValue({ + errorString: errorMessage, + }); + } + }, +); + +export const fetchSep38QuotesSep6InfoAction = createAsyncThunk< + { + assets: AnchorQuoteAsset[]; + buyAsset?: string; + sellAsset?: string; + amount: string; + serverUrl: string | undefined; + }, + { + anchorQuoteServerUrl: string | undefined; + buyAsset?: string; + sellAsset?: string; + amount: string; + }, + { rejectValue: RejectMessage; state: RootState } +>( + "sep38Quotes/fetchSep38QuotesSep6InfoAction", async ( - { anchorQuoteServerUrl, sellAsset, sellAmount }, + { anchorQuoteServerUrl, buyAsset, sellAsset, amount }, { rejectWithValue }, ) => { try { - const result = await getInfo(anchorQuoteServerUrl, { - sell_asset: sellAsset, - sell_amount: sellAmount, - }); + const result = await getInfo({ context: "sep6", anchorQuoteServerUrl }); return { assets: result.assets, + buyAsset, sellAsset, - sellAmount, + amount, serverUrl: anchorQuoteServerUrl, }; } catch (error) { @@ -170,7 +212,8 @@ const initialState: Sep38QuotesInitialState = { data: { serverUrl: undefined, sellAsset: undefined, - sellAmount: undefined, + buyAsset: undefined, + amount: undefined, assets: [], prices: [], quote: undefined, @@ -187,26 +230,61 @@ const sep38QuotesSlice = createSlice({ }, extraReducers: (builder) => { builder.addCase( - fetchSep38QuotesInfoAction.pending, + fetchSep38QuotesSep31InfoAction.pending, (state = initialState) => { state.status = ActionStatus.PENDING; state.data = { ...state.data, prices: [], quote: undefined }; }, ); - builder.addCase(fetchSep38QuotesInfoAction.fulfilled, (state, action) => { - state.data = { - ...state.data, - assets: action.payload.assets, - sellAsset: action.payload.sellAsset, - sellAmount: action.payload.sellAmount, - serverUrl: action.payload.serverUrl, - }; - state.status = ActionStatus.SUCCESS; - }); - builder.addCase(fetchSep38QuotesInfoAction.rejected, (state, action) => { - state.errorString = action.payload?.errorString; - state.status = ActionStatus.ERROR; - }); + builder.addCase( + fetchSep38QuotesSep31InfoAction.fulfilled, + (state, action) => { + state.data = { + ...state.data, + assets: action.payload.assets, + sellAsset: action.payload.sellAsset, + amount: action.payload.amount, + serverUrl: action.payload.serverUrl, + }; + state.status = ActionStatus.SUCCESS; + }, + ); + builder.addCase( + fetchSep38QuotesSep31InfoAction.rejected, + (state, action) => { + state.errorString = action.payload?.errorString; + state.status = ActionStatus.ERROR; + }, + ); + + builder.addCase( + fetchSep38QuotesSep6InfoAction.pending, + (state = initialState) => { + state.status = ActionStatus.PENDING; + state.data = { ...state.data, prices: [], quote: undefined }; + }, + ); + builder.addCase( + fetchSep38QuotesSep6InfoAction.fulfilled, + (state, action) => { + state.data = { + ...state.data, + assets: action.payload.assets, + buyAsset: action.payload.buyAsset, + sellAsset: action.payload.sellAsset, + amount: action.payload.amount, + serverUrl: action.payload.serverUrl, + }; + state.status = ActionStatus.SUCCESS; + }, + ); + builder.addCase( + fetchSep38QuotesSep6InfoAction.rejected, + (state, action) => { + state.errorString = action.payload?.errorString; + state.status = ActionStatus.ERROR; + }, + ); builder.addCase( fetchSep38QuotesPricesAction.pending, diff --git a/packages/demo-wallet-client/src/ducks/sep6DepositAsset.ts b/packages/demo-wallet-client/src/ducks/sep6DepositAsset.ts index 62b1f99d..e6c47009 100644 --- a/packages/demo-wallet-client/src/ducks/sep6DepositAsset.ts +++ b/packages/demo-wallet-client/src/ducks/sep6DepositAsset.ts @@ -9,6 +9,7 @@ import { log } from "demo-wallet-shared/build/helpers/log"; import { checkDepositWithdrawInfo } from "demo-wallet-shared/build/methods/checkDepositWithdrawInfo"; import { pollDepositUntilComplete, + programmaticDepositExchangeFlow, programmaticDepositFlow, } from "demo-wallet-shared/build/methods/sep6"; import { @@ -33,6 +34,7 @@ import { AnyObject, TransactionStatus, SepInstructions, + Sep12CustomerStatus, } from "types/types"; type InitiateDepositActionPayload = Sep6DepositAssetInitialState["data"] & { @@ -80,6 +82,21 @@ export const initiateDepositAction = createAsyncThunk< assetCode, }); + let anchorQuoteServer; + + // Check SEP-38 quote server key in toml, if supported + if (infoData["deposit-exchange"]) { + const tomlSep38Response = await checkTomlForFields({ + sepName: "SEP-38 Anchor RFQ", + assetIssuer, + requiredKeys: [TomlFields.ANCHOR_QUOTE_SERVER], + networkUrl: networkConfig.url, + homeDomain, + }); + + anchorQuoteServer = tomlSep38Response.ANCHOR_QUOTE_SERVER; + } + const assetInfoData = infoData[AnchorActionType.DEPOSIT][assetCode]; const { @@ -99,6 +116,7 @@ export const initiateDepositAction = createAsyncThunk< status: ActionStatus.NEEDS_INPUT, token: "", transferServerUrl: tomlResponse.TRANSFER_SERVER, + anchorQuoteServer, } as InitiateDepositActionPayload; if (isAuthenticationRequired) { @@ -165,42 +183,31 @@ export const initiateDepositAction = createAsyncThunk< }, ); +// Submit transaction to start polling for the status export const submitSep6DepositFields = createAsyncThunk< { status: ActionStatus; depositResponse: Sep6DepositResponse; - customerFields?: AnyObject; }, { amount?: string; depositType: AnyObject; infoFields: AnyObject; - customerFields: AnyObject; }, { rejectValue: RejectMessage; state: RootState } >( "sep6DepositAsset/submitSep6DepositFields", async ( - { amount, depositType, customerFields, infoFields }, + { amount, depositType, infoFields }, { rejectWithValue, getState }, ) => { try { const { data } = accountSelector(getState()); const publicKey = data?.id || ""; const { claimableBalanceSupported } = settingsSelector(getState()); - const { secretKey } = accountSelector(getState()); const { data: sep6Data } = sep6DepositSelector(getState()); - const { assetCode, kycServer, transferServerUrl, token } = sep6Data; - - if (Object.keys(customerFields).length) { - await putSep12FieldsRequest({ - fields: customerFields, - kycServer, - secretKey, - token, - }); - } + const { assetCode, transferServerUrl, token } = sep6Data; const depositResponse = (await programmaticDepositFlow({ amount, @@ -213,31 +220,72 @@ export const submitSep6DepositFields = createAsyncThunk< claimableBalanceSupported, })) as Sep6DepositResponse; - if ( - depositResponse.type === - TransactionStatus.NON_INTERACTIVE_CUSTOMER_INFO_NEEDED - ) { - log.instruction({ - title: "Anchor requires additional customer information (KYC)", - }); + return { + status: ActionStatus.CAN_PROCEED, + depositResponse, + }; + } catch (e) { + const errorMessage = getErrorMessage(e); - // Get SEP-12 fields - log.instruction({ - title: "Making GET `/customer` request for user", - }); + log.error({ + title: errorMessage, + }); - const customerFields = await collectSep12Fields({ - publicKey: data?.id!, - token, - kycServer, - }); + return rejectWithValue({ + errorString: errorMessage, + }); + } + }, +); - return { - status: ActionStatus.NEEDS_KYC, - depositResponse, - customerFields, - }; - } +// Submit transaction with SEP-38 quotes to start polling for the status +export const submitSep6DepositWithQuotesFields = createAsyncThunk< + { + status: ActionStatus; + depositResponse: Sep6DepositResponse; + }, + { + amount: string; + quoteId: string; + destinationAssetCode: string; + sourceAsset: string; + depositType: AnyObject; + infoFields: AnyObject; + }, + { rejectValue: RejectMessage; state: RootState } +>( + "sep6DepositAsset/submitSep6DepositWithQuotesFields", + async ( + { + amount, + quoteId, + destinationAssetCode, + sourceAsset, + depositType, + infoFields, + }, + { rejectWithValue, getState }, + ) => { + try { + const { data } = accountSelector(getState()); + const publicKey = data?.id || ""; + const { claimableBalanceSupported } = settingsSelector(getState()); + const { data: sep6Data } = sep6DepositSelector(getState()); + + const { transferServerUrl, token } = sep6Data; + + const depositResponse = (await programmaticDepositExchangeFlow({ + amount, + sourceAsset, + destinationAssetCode, + quoteId, + publicKey, + transferServerUrl, + token, + type: depositType.type, + depositFields: infoFields, + claimableBalanceSupported, + })) as Sep6DepositResponse; return { status: ActionStatus.CAN_PROCEED, @@ -258,14 +306,14 @@ export const submitSep6DepositFields = createAsyncThunk< ); export const submitSep6CustomerInfoFields = createAsyncThunk< - { status: ActionStatus }, + { status: ActionStatus; customerFields?: AnyObject }, AnyObject, { rejectValue: RejectMessage; state: RootState } >( "sep6DepositAsset/submitSep6CustomerInfoFields", async (customerFields, { rejectWithValue, getState }) => { try { - const { secretKey } = accountSelector(getState()); + const { data: account, secretKey } = accountSelector(getState()); const { data: sep6Data } = sep6DepositSelector(getState()); const { kycServer, token } = sep6Data; @@ -276,11 +324,31 @@ export const submitSep6CustomerInfoFields = createAsyncThunk< kycServer, secretKey, token, + transactionId: sep6Data.depositResponse?.id, }); } + // Get SEP-12 fields + log.instruction({ + title: "Making GET `/customer` request for user", + }); + + const sep12Response = await collectSep12Fields({ + publicKey: account?.id!, + token, + kycServer, + transactionId: sep6Data.depositResponse?.id, + }); + + if (sep12Response.status !== Sep12CustomerStatus.ACCEPTED) { + return { + status: ActionStatus.NEEDS_KYC, + customerFields: sep12Response.fieldsToCollect, + }; + } + return { - status: ActionStatus.CAN_PROCEED, + status: ActionStatus.KYC_DONE, }; } catch (e) { const errorMessage = getErrorMessage(e); @@ -346,7 +414,7 @@ export const sep6DepositAction = createAsyncThunk< trustedAssetAdded = "", requiredCustomerInfoUpdates, } = await pollDepositUntilComplete({ - transactionId: depositResponse.id || "", + transactionId: depositResponse?.id || "", token, transferServerUrl, trustAssetCallback, @@ -363,18 +431,21 @@ export const sep6DepositAction = createAsyncThunk< title: "Making GET `/customer` request for user", }); - customerFields = await collectSep12Fields({ - publicKey: data?.id!, - token, - kycServer, - }); + customerFields = ( + await collectSep12Fields({ + publicKey: data?.id!, + token, + kycServer, + transactionId: depositResponse?.id, + }) + ).fieldsToCollect; } return { currentStatus, status: currentStatus === TransactionStatus.PENDING_CUSTOMER_INFO_UPDATE - ? ActionStatus.NEEDS_INPUT + ? ActionStatus.NEEDS_KYC : ActionStatus.SUCCESS, trustedAssetAdded, requiredCustomerInfoUpdates, @@ -415,6 +486,7 @@ const initialState: Sep6DepositAssetInitialState = { trustedAssetAdded: "", requiredCustomerInfoUpdates: undefined, instructions: undefined, + anchorQuoteServer: undefined, }, status: "" as ActionStatus, errorString: undefined, @@ -428,6 +500,9 @@ const sep6DepositAssetSlice = createSlice({ updateInstructionsAction: (state, action) => { state.data.instructions = action.payload; }, + setStatusAction: (state, action) => { + state.status = action.payload; + }, }, extraReducers: (builder) => { builder.addCase(initiateDepositAction.pending, (state) => { @@ -450,22 +525,38 @@ const sep6DepositAssetSlice = createSlice({ builder.addCase(submitSep6DepositFields.fulfilled, (state, action) => { state.status = action.payload.status; state.data.depositResponse = action.payload.depositResponse; - state.data.customerFields = { - ...state.data.customerFields, - ...action.payload.customerFields, - }; }); builder.addCase(submitSep6DepositFields.rejected, (state, action) => { state.errorString = action.payload?.errorString; state.status = ActionStatus.ERROR; }); + builder.addCase(submitSep6DepositWithQuotesFields.pending, (state) => { + state.errorString = undefined; + state.status = ActionStatus.PENDING; + }); + builder.addCase( + submitSep6DepositWithQuotesFields.fulfilled, + (state, action) => { + state.status = action.payload.status; + state.data.depositResponse = action.payload.depositResponse; + }, + ); + builder.addCase( + submitSep6DepositWithQuotesFields.rejected, + (state, action) => { + state.errorString = action.payload?.errorString; + state.status = ActionStatus.ERROR; + }, + ); + builder.addCase(submitSep6CustomerInfoFields.pending, (state) => { state.errorString = undefined; state.status = ActionStatus.PENDING; }); builder.addCase(submitSep6CustomerInfoFields.fulfilled, (state, action) => { state.status = action.payload.status; + state.data.customerFields = { ...action.payload.customerFields }; }); builder.addCase(submitSep6CustomerInfoFields.rejected, (state, action) => { state.errorString = action.payload?.errorString; @@ -481,7 +572,6 @@ const sep6DepositAssetSlice = createSlice({ state.data.trustedAssetAdded = action.payload.trustedAssetAdded; const customerFields = { - ...state.data.customerFields, ...action.payload.customerFields, }; @@ -505,5 +595,8 @@ const sep6DepositAssetSlice = createSlice({ export const sep6DepositSelector = (state: RootState) => state.sep6DepositAsset; export const { reducer } = sep6DepositAssetSlice; -export const { resetSep6DepositAction, updateInstructionsAction } = - sep6DepositAssetSlice.actions; +export const { + resetSep6DepositAction, + updateInstructionsAction, + setStatusAction, +} = sep6DepositAssetSlice.actions; diff --git a/packages/demo-wallet-client/src/ducks/sep6WithdrawAsset.ts b/packages/demo-wallet-client/src/ducks/sep6WithdrawAsset.ts index 77f1a223..39511d16 100644 --- a/packages/demo-wallet-client/src/ducks/sep6WithdrawAsset.ts +++ b/packages/demo-wallet-client/src/ducks/sep6WithdrawAsset.ts @@ -9,6 +9,7 @@ import { log } from "demo-wallet-shared/build/helpers/log"; import { checkDepositWithdrawInfo } from "demo-wallet-shared/build/methods/checkDepositWithdrawInfo"; import { pollWithdrawUntilComplete, + programmaticWithdrawExchangeFlow, programmaticWithdrawFlow, } from "demo-wallet-shared/build/methods/sep6"; import { @@ -31,6 +32,7 @@ import { AnchorActionType, AnyObject, TransactionStatus, + Sep12CustomerStatus, } from "types/types"; type InitiateWithdrawActionPayload = Sep6WithdrawAssetInitialState["data"] & { @@ -78,6 +80,21 @@ export const initiateWithdrawAction = createAsyncThunk< assetCode, }); + let anchorQuoteServer; + + // Check SEP-38 quote server key in toml, if supported + if (infoData["withdraw-exchange"]) { + const tomlSep38Response = await checkTomlForFields({ + sepName: "SEP-38 Anchor RFQ", + assetIssuer, + requiredKeys: [TomlFields.ANCHOR_QUOTE_SERVER], + networkUrl: networkConfig.url, + homeDomain, + }); + + anchorQuoteServer = tomlSep38Response.ANCHOR_QUOTE_SERVER; + } + const assetInfoData = infoData[AnchorActionType.WITHDRAWAL][assetCode]; const { authentication_required: isAuthenticationRequired } = @@ -92,6 +109,7 @@ export const initiateWithdrawAction = createAsyncThunk< status: ActionStatus.NEEDS_INPUT, token: "", transferServerUrl: tomlResponse.TRANSFER_SERVER, + anchorQuoteServer, } as InitiateWithdrawActionPayload; if (isAuthenticationRequired) { @@ -158,40 +176,27 @@ export const initiateWithdrawAction = createAsyncThunk< }, ); +// Submit transaction to start polling for the status export const submitSep6WithdrawFields = createAsyncThunk< { status: ActionStatus; withdrawResponse: Sep6WithdrawResponse; - customerFields?: AnyObject; }, { withdrawType: AnyObject; infoFields: AnyObject; - customerFields: AnyObject; }, { rejectValue: RejectMessage; state: RootState } >( "sep6WithdrawAsset/submitSep6WithdrawFields", - async ( - { withdrawType, infoFields, customerFields }, - { rejectWithValue, getState }, - ) => { + async ({ withdrawType, infoFields }, { rejectWithValue, getState }) => { try { - const { data, secretKey } = accountSelector(getState()); + const { data } = accountSelector(getState()); const { claimableBalanceSupported } = settingsSelector(getState()); const publicKey = data?.id || ""; const { data: sep6Data } = sepWithdrawSelector(getState()); - const { assetCode, kycServer, transferServerUrl, token } = sep6Data; - - if (Object.keys(customerFields).length) { - await putSep12FieldsRequest({ - fields: customerFields, - kycServer, - secretKey, - token, - }); - } + const { assetCode, transferServerUrl, token } = sep6Data; const withdrawResponse = (await programmaticWithdrawFlow({ assetCode, @@ -203,31 +208,72 @@ export const submitSep6WithdrawFields = createAsyncThunk< claimableBalanceSupported, })) as Sep6WithdrawResponse; - if ( - withdrawResponse.type === - TransactionStatus.NON_INTERACTIVE_CUSTOMER_INFO_NEEDED - ) { - log.instruction({ - title: "Anchor requires additional customer information (KYC)", - }); + return { + status: ActionStatus.CAN_PROCEED, + withdrawResponse, + }; + } catch (e) { + const errorMessage = getErrorMessage(e); - // Get SEP-12 fields - log.instruction({ - title: "Making GET `/customer` request for user", - }); + log.error({ + title: errorMessage, + }); - const customerFields = await collectSep12Fields({ - publicKey, - token, - kycServer, - }); + return rejectWithValue({ + errorString: errorMessage, + }); + } + }, +); - return { - status: ActionStatus.NEEDS_KYC, - withdrawResponse, - customerFields, - }; - } +// Submit transaction with SEP-38 quotes to start polling for the status +export const submitSep6WithdrawWithQuotesFields = createAsyncThunk< + { + status: ActionStatus; + withdrawResponse: Sep6WithdrawResponse; + }, + { + amount: string; + sourceAssetCode: string; + destinationAsset: string; + quoteId: string | undefined; + withdrawType: AnyObject; + infoFields: AnyObject; + }, + { rejectValue: RejectMessage; state: RootState } +>( + "sep6WithdrawAsset/submitSep6WithdrawWithQuotesFields", + async ( + { + amount, + sourceAssetCode, + destinationAsset, + quoteId, + withdrawType, + infoFields, + }, + { rejectWithValue, getState }, + ) => { + try { + const { data } = accountSelector(getState()); + const { claimableBalanceSupported } = settingsSelector(getState()); + const publicKey = data?.id || ""; + + const { data: sep6Data } = sepWithdrawSelector(getState()); + const { transferServerUrl, token } = sep6Data; + + const withdrawResponse = (await programmaticWithdrawExchangeFlow({ + amount, + sourceAssetCode, + destinationAsset, + quoteId, + publicKey, + transferServerUrl, + token, + type: withdrawType.type, + withdrawFields: infoFields, + claimableBalanceSupported, + })) as Sep6WithdrawResponse; return { status: ActionStatus.CAN_PROCEED, @@ -297,11 +343,14 @@ export const sep6WithdrawAction = createAsyncThunk< title: "Making GET `/customer` request for user", }); - customerFields = await collectSep12Fields({ - publicKey: data?.id!, - token, - kycServer, - }); + customerFields = ( + await collectSep12Fields({ + publicKey: data?.id!, + token, + kycServer, + transactionId: withdrawResponse?.id, + }) + ).fieldsToCollect; } return { @@ -309,7 +358,7 @@ export const sep6WithdrawAction = createAsyncThunk< transactionResponse: transaction, status: currentStatus === TransactionStatus.PENDING_CUSTOMER_INFO_UPDATE - ? ActionStatus.NEEDS_INPUT + ? ActionStatus.NEEDS_KYC : ActionStatus.SUCCESS, requiredCustomerInfoUpdates, customerFields, @@ -330,14 +379,14 @@ export const sep6WithdrawAction = createAsyncThunk< ); export const submitSep6WithdrawCustomerInfoFields = createAsyncThunk< - { status: ActionStatus }, + { status: ActionStatus; customerFields?: AnyObject }, AnyObject, { rejectValue: RejectMessage; state: RootState } >( "sep6WithdrawAsset/submitSep6WithdrawCustomerInfoFields", async (customerFields, { rejectWithValue, getState }) => { try { - const { secretKey } = accountSelector(getState()); + const { data: account, secretKey } = accountSelector(getState()); const { data: sep6Data } = sepWithdrawSelector(getState()); const { kycServer, token } = sep6Data; @@ -347,11 +396,31 @@ export const submitSep6WithdrawCustomerInfoFields = createAsyncThunk< kycServer, secretKey, token, + transactionId: sep6Data.withdrawResponse?.id, }); } + // Get SEP-12 fields + log.instruction({ + title: "Making GET `/customer` request for user", + }); + + const sep12Response = await collectSep12Fields({ + publicKey: account?.id!, + token, + kycServer, + transactionId: sep6Data.withdrawResponse?.id, + }); + + if (sep12Response.status !== Sep12CustomerStatus.ACCEPTED) { + return { + status: ActionStatus.NEEDS_KYC, + customerFields: sep12Response.fieldsToCollect, + }; + } + return { - status: ActionStatus.CAN_PROCEED, + status: ActionStatus.KYC_DONE, }; } catch (e) { const errorMessage = getErrorMessage(e); @@ -383,6 +452,7 @@ const initialState: Sep6WithdrawAssetInitialState = { transactionResponse: {}, withdrawResponse: { account_id: "" }, requiredCustomerInfoUpdates: undefined, + anchorQuoteServer: undefined, }, status: undefined, errorString: undefined, @@ -393,6 +463,9 @@ const sep6WithdrawAssetSlice = createSlice({ initialState, reducers: { resetSep6WithdrawAction: () => initialState, + setStatusAction: (state, action) => { + state.status = action.payload; + }, }, extraReducers: (builder) => { builder.addCase(initiateWithdrawAction.pending, (state) => { @@ -415,16 +488,31 @@ const sep6WithdrawAssetSlice = createSlice({ builder.addCase(submitSep6WithdrawFields.fulfilled, (state, action) => { state.status = action.payload.status; state.data.withdrawResponse = action.payload.withdrawResponse; - state.data.fields = { - ...state.data.fields, - ...action.payload.customerFields, - }; }); builder.addCase(submitSep6WithdrawFields.rejected, (state, action) => { state.errorString = action.payload?.errorString; state.status = ActionStatus.ERROR; }); + builder.addCase(submitSep6WithdrawWithQuotesFields.pending, (state) => { + state.errorString = undefined; + state.status = ActionStatus.PENDING; + }); + builder.addCase( + submitSep6WithdrawWithQuotesFields.fulfilled, + (state, action) => { + state.status = action.payload.status; + state.data.withdrawResponse = action.payload.withdrawResponse; + }, + ); + builder.addCase( + submitSep6WithdrawWithQuotesFields.rejected, + (state, action) => { + state.errorString = action.payload?.errorString; + state.status = ActionStatus.ERROR; + }, + ); + builder.addCase(submitSep6WithdrawCustomerInfoFields.pending, (state) => { state.errorString = undefined; state.status = ActionStatus.PENDING; @@ -433,6 +521,7 @@ const sep6WithdrawAssetSlice = createSlice({ submitSep6WithdrawCustomerInfoFields.fulfilled, (state, action) => { state.status = action.payload.status; + state.data.fields = { ...action.payload.customerFields }; }, ); builder.addCase( @@ -453,7 +542,6 @@ const sep6WithdrawAssetSlice = createSlice({ state.data.transactionResponse = action.payload.transactionResponse; const customerFields = { - ...state.data.fields, ...action.payload.customerFields, }; @@ -478,4 +566,5 @@ export const sepWithdrawSelector = (state: RootState) => state.sep6WithdrawAsset; export const { reducer } = sep6WithdrawAssetSlice; -export const { resetSep6WithdrawAction } = sep6WithdrawAssetSlice.actions; +export const { resetSep6WithdrawAction, setStatusAction } = + sep6WithdrawAssetSlice.actions; diff --git a/packages/demo-wallet-client/src/types/types.ts b/packages/demo-wallet-client/src/types/types.ts index 0f4c07b4..449a6195 100644 --- a/packages/demo-wallet-client/src/types/types.ts +++ b/packages/demo-wallet-client/src/types/types.ts @@ -243,6 +243,7 @@ export interface Sep6DepositAssetInitialState { trustedAssetAdded: string; requiredCustomerInfoUpdates: AnyObject[] | undefined; instructions: SepInstructions | undefined; + anchorQuoteServer: string | undefined; }; errorString?: string; status: ActionStatus; @@ -290,6 +291,7 @@ export interface Sep6WithdrawAssetInitialState { transactionResponse: AnyObject; withdrawResponse: Sep6WithdrawResponse; requiredCustomerInfoUpdates: AnyObject[] | undefined; + anchorQuoteServer: string | undefined; }; errorString?: string; status: ActionStatus | undefined; @@ -330,7 +332,8 @@ export interface Sep38QuotesInitialState { data: { serverUrl: string | undefined; sellAsset: string | undefined; - sellAmount: string | undefined; + buyAsset: string | undefined; + amount: string | undefined; assets: AnchorQuoteAsset[]; prices: AnchorBuyAsset[]; quote: AnchorQuote | undefined; @@ -410,6 +413,7 @@ export enum ActionStatus { SUCCESS = "SUCCESS", NEEDS_INPUT = "NEEDS_INPUT", NEEDS_KYC = "NEEDS_KYC", + KYC_DONE = "KYC_DONE", CAN_PROCEED = "CAN_PROCEED", ANCHOR_QUOTES = "ANCHOR_QUOTES", } @@ -492,6 +496,8 @@ export enum MemoTypeString { export enum AnchorActionType { DEPOSIT = "deposit", WITHDRAWAL = "withdraw", + DEPOSIT_EXCHANGE = "deposit-exchange", + WITHDRAW_EXCHANGE = "withdraw-exchange", } interface InfoTypeData { @@ -513,6 +519,12 @@ export interface CheckInfoData { [AnchorActionType.WITHDRAWAL]: { [asset: string]: InfoTypeData; }; + [AnchorActionType.DEPOSIT_EXCHANGE]?: { + [asset: string]: InfoTypeData; + }; + [AnchorActionType.WITHDRAW_EXCHANGE]?: { + [asset: string]: InfoTypeData; + }; } export enum Sep8ApprovalStatus { diff --git a/packages/demo-wallet-shared/methods/sep12/collectSep12Fields.ts b/packages/demo-wallet-shared/methods/sep12/collectSep12Fields.ts index 82246c70..51472e5a 100644 --- a/packages/demo-wallet-shared/methods/sep12/collectSep12Fields.ts +++ b/packages/demo-wallet-shared/methods/sep12/collectSep12Fields.ts @@ -10,6 +10,7 @@ export const collectSep12Fields = async ({ publicKey, token, type, + transactionId, isNewCustomer, }: { kycServer: string; @@ -17,6 +18,7 @@ export const collectSep12Fields = async ({ publicKey: string; token: string; type?: string; + transactionId?: string; isNewCustomer?: boolean; }) => { // The anchor needs a memo to disambiguate the sending and receiving clients @@ -25,6 +27,7 @@ export const collectSep12Fields = async ({ ...(type ? { type } : {}), account: publicKey, ...(memo ? { memo, memo_type: "hash" } : {}), + ...(transactionId ? { transaction_id: transactionId } : {}), }; log.request({ title: "GET `/customer`", body: params }); @@ -54,15 +57,15 @@ export const collectSep12Fields = async ({ const fieldsToCollect = Object.entries(resultJson.fields ?? {}).reduce( (collectResult: any, field: any) => { - const [key, props] = field; + const [key, value] = field; + + const providedField = resultJson?.provided_fields?.[key]; if ( - !props.status || - props.status === Sep12CustomerFieldStatus.NOT_PROVIDED || - (props.status === Sep12CustomerFieldStatus.REJECTED && - resultJson.status === Sep12CustomerStatus.NEEDS_INFO) + !providedField || + providedField.status !== Sep12CustomerFieldStatus.ACCEPTED ) { - return { ...collectResult, [key]: props }; + return { ...collectResult, [key]: value }; } return collectResult; @@ -88,5 +91,5 @@ export const collectSep12Fields = async ({ }); } - return fieldsToCollect; + return { fieldsToCollect, status: resultJson.status }; }; diff --git a/packages/demo-wallet-shared/methods/sep12/putSep12FieldsRequest.ts b/packages/demo-wallet-shared/methods/sep12/putSep12FieldsRequest.ts index c2598cfc..db795b23 100644 --- a/packages/demo-wallet-shared/methods/sep12/putSep12FieldsRequest.ts +++ b/packages/demo-wallet-shared/methods/sep12/putSep12FieldsRequest.ts @@ -7,6 +7,7 @@ interface PutSep12FieldsRequestProps { memo?: string; token: string; kycServer: string; + transactionId?: string; isSender?: boolean; } @@ -16,12 +17,14 @@ export const putSep12FieldsRequest = async ({ memo, token, kycServer, + transactionId, isSender, }: PutSep12FieldsRequestProps) => { const publicKey = Keypair.fromSecret(secretKey).publicKey(); const data: { [key: string]: string } = { account: publicKey, ...(memo ? { memo, memo_type: "hash" } : {}), + ...(transactionId ? { transaction_id: transactionId } : {}), ...fields, }; diff --git a/packages/demo-wallet-shared/methods/sep31Send/getSep12Fields.ts b/packages/demo-wallet-shared/methods/sep31Send/getSep12Fields.ts index 8ec9dee4..24ed74a2 100644 --- a/packages/demo-wallet-shared/methods/sep31Send/getSep12Fields.ts +++ b/packages/demo-wallet-shared/methods/sep31Send/getSep12Fields.ts @@ -31,14 +31,16 @@ export const getSep12Fields = async ({ if (senderType) { const memo = crypto.randomBytes(32).toString("base64"); - result.senderSep12Fields = await collectSep12Fields({ - type: senderType, - memo, - publicKey, - token, - kycServer, - isNewCustomer: true, - }); + result.senderSep12Fields = ( + await collectSep12Fields({ + type: senderType, + memo, + publicKey, + token, + kycServer, + isNewCustomer: true, + }) + ).fieldsToCollect; result.info.senderSep12Memo = memo; } @@ -46,14 +48,16 @@ export const getSep12Fields = async ({ if (receiverType) { const memo = crypto.randomBytes(32).toString("base64"); - result.receiverSep12Fields = await collectSep12Fields({ - type: receiverType, - memo, - publicKey, - token, - kycServer, - isNewCustomer: true, - }); + result.receiverSep12Fields = ( + await collectSep12Fields({ + type: receiverType, + memo, + publicKey, + token, + kycServer, + isNewCustomer: true, + }) + ).fieldsToCollect; result.info.receiverSep12Memo = memo; } diff --git a/packages/demo-wallet-shared/methods/sep38Quotes/getInfo.ts b/packages/demo-wallet-shared/methods/sep38Quotes/getInfo.ts index 388d8697..96755c31 100644 --- a/packages/demo-wallet-shared/methods/sep38Quotes/getInfo.ts +++ b/packages/demo-wallet-shared/methods/sep38Quotes/getInfo.ts @@ -1,35 +1,50 @@ import { log } from "../../helpers/log"; import { AnchorQuoteAsset } from "../../types/types"; -export const getInfo = async ( - anchorQuoteServerUrl: string | undefined, - options?: { - /* eslint-disable camelcase */ - sell_asset: string; - sell_amount: string; - sell_delivery_method?: string; - buy_delivery_method?: string; - country_code?: string; - /* eslint-enable camelcase */ - }, -): Promise<{ assets: AnchorQuoteAsset[] }> => { +type Sep38GetInfo = ( + | { + context: "sep6"; + options?: undefined; + } + | { + context: "sep31"; + options?: { + /* eslint-disable camelcase */ + sell_asset: string; + sell_amount: string; + sell_delivery_method?: string; + buy_delivery_method?: string; + country_code?: string; + /* eslint-enable camelcase */ + }; + } +) & { + anchorQuoteServerUrl: string | undefined; +}; + +export const getInfo = async ({ + context, + anchorQuoteServerUrl, + options, +}: Sep38GetInfo): Promise<{ assets: AnchorQuoteAsset[] }> => { if (!anchorQuoteServerUrl) { throw new Error("Anchor quote server URL is required"); } - const params = options - ? Object.entries(options).reduce((res: any, [key, value]) => { - if (value) { - res[key] = value; - } + const params = + context === "sep31" && options + ? Object.entries(options).reduce((res: any, [key, value]) => { + if (value) { + res[key] = value; + } - return res; - }, {}) - : undefined; + return res; + }, {}) + : undefined; const urlParams = params ? new URLSearchParams(params) : undefined; log.instruction({ - title: `Checking \`/info\` endpoint for \`${anchorQuoteServerUrl}\` to get anchor quotes details`, + title: `Checking \`/info\` endpoint for \`${anchorQuoteServerUrl}\` to get anchor quotes details for ${context.toLocaleUpperCase()}`, ...(params ? { body: params } : {}), }); @@ -39,7 +54,9 @@ export const getInfo = async ( }); const result = await fetch( - `${anchorQuoteServerUrl}/info?${urlParams?.toString()}`, + `${anchorQuoteServerUrl}/info${ + urlParams ? `?${urlParams?.toString()}` : "" + }`, ); const resultJson = await result.json(); diff --git a/packages/demo-wallet-shared/methods/sep38Quotes/getPrices.ts b/packages/demo-wallet-shared/methods/sep38Quotes/getPrices.ts index fbc17092..d870097f 100644 --- a/packages/demo-wallet-shared/methods/sep38Quotes/getPrices.ts +++ b/packages/demo-wallet-shared/methods/sep38Quotes/getPrices.ts @@ -57,12 +57,14 @@ Promise<{ buy_assets: AnchorBuyAsset[] }> => { ); if (result.status !== 200) { + const responseJson = await result.json(); + log.error({ title: "GET `/prices` failed", - body: { status: result.status }, + body: { status: result.status, ...responseJson }, }); - throw new Error("Something went wrong"); + throw new Error(responseJson.error ?? "Something went wrong"); } const resultJson = await result.json(); diff --git a/packages/demo-wallet-shared/methods/sep6/index.ts b/packages/demo-wallet-shared/methods/sep6/index.ts index a2e66978..ffe2c639 100644 --- a/packages/demo-wallet-shared/methods/sep6/index.ts +++ b/packages/demo-wallet-shared/methods/sep6/index.ts @@ -1,11 +1,15 @@ import { pollDepositUntilComplete } from "./pollDepositUntilComplete"; import { pollWithdrawUntilComplete } from "./pollWithdrawUntilComplete"; import { programmaticDepositFlow } from "./programmaticDepositFlow"; +import { programmaticDepositExchangeFlow } from "./programmaticDepositExchangeFlow"; import { programmaticWithdrawFlow } from "./programmaticWithdrawFlow"; +import { programmaticWithdrawExchangeFlow } from "./programmaticWithdrawExchangeFlow"; export { pollDepositUntilComplete, pollWithdrawUntilComplete, programmaticDepositFlow, + programmaticDepositExchangeFlow, programmaticWithdrawFlow, + programmaticWithdrawExchangeFlow, }; diff --git a/packages/demo-wallet-shared/methods/sep6/programmaticDepositExchangeFlow.ts b/packages/demo-wallet-shared/methods/sep6/programmaticDepositExchangeFlow.ts new file mode 100644 index 00000000..98ea05c7 --- /dev/null +++ b/packages/demo-wallet-shared/methods/sep6/programmaticDepositExchangeFlow.ts @@ -0,0 +1,73 @@ +import { each } from "lodash"; +import { log } from "../../helpers/log"; +import { AnyObject } from "../../types/types"; + +type ProgrammaticDepositExchangeFlowProps = { + amount: string; + destinationAssetCode: string; + sourceAsset: string; + quoteId: string; + publicKey: string; + transferServerUrl: string; + token: string; + type: string; + depositFields: AnyObject; + claimableBalanceSupported: boolean; +}; + +export const programmaticDepositExchangeFlow = async ({ + amount = "", + destinationAssetCode, + sourceAsset, + quoteId, + publicKey, + transferServerUrl, + token, + type, + depositFields, + claimableBalanceSupported, +}: ProgrammaticDepositExchangeFlowProps) => { + log.instruction({ + title: "Starting SEP-6 programmatic flow for deposit-exchange", + }); + + const API_METHOD = "GET"; + const REQUEST_URL_STR = `${transferServerUrl}/deposit-exchange`; + const REQUEST_URL = new URL(REQUEST_URL_STR); + + const getDepositParams = { + amount, + destination_asset: destinationAssetCode, + source_asset: sourceAsset, + quote_id: quoteId, + account: publicKey, + claimable_balance_supported: claimableBalanceSupported.toString(), + type, + ...depositFields, + }; + + each(getDepositParams, (value, key) => + REQUEST_URL.searchParams.append(key, value), + ); + + log.request({ + title: `${API_METHOD} \`${REQUEST_URL_STR}\``, + body: getDepositParams, + }); + + const response = await fetch(`${REQUEST_URL}`, { + method: API_METHOD, + headers: { + Authorization: `Bearer ${token}`, + }, + }); + + const depositJson = await response.json(); + + log.response({ + title: `${API_METHOD} \`${REQUEST_URL_STR}\``, + body: depositJson, + }); + + return depositJson; +}; diff --git a/packages/demo-wallet-shared/methods/sep6/programmaticWithdrawExchangeFlow.ts b/packages/demo-wallet-shared/methods/sep6/programmaticWithdrawExchangeFlow.ts new file mode 100644 index 00000000..76c09f5f --- /dev/null +++ b/packages/demo-wallet-shared/methods/sep6/programmaticWithdrawExchangeFlow.ts @@ -0,0 +1,84 @@ +import { each } from "lodash"; +import { log } from "../../helpers/log"; +import { AnyObject, TransactionStatus } from "../../types/types"; + +type ProgrammaticWithdrawExchangeFlowProps = { + amount: string; + sourceAssetCode: string; + destinationAsset: string; + quoteId?: string; + publicKey: string; + transferServerUrl: string; + token: string; + type: string; + withdrawFields: AnyObject; + claimableBalanceSupported: boolean; +}; + +export const programmaticWithdrawExchangeFlow = async ({ + amount, + sourceAssetCode, + destinationAsset, + quoteId, + publicKey, + transferServerUrl, + token, + type, + withdrawFields, + claimableBalanceSupported, +}: ProgrammaticWithdrawExchangeFlowProps) => { + log.instruction({ + title: "Starting SEP-6 programmatic flow for withdraw-exchange", + }); + + const API_METHOD = "GET"; + const REQUEST_URL_STR = `${transferServerUrl}/withdraw-exchange`; + const REQUEST_URL = new URL(REQUEST_URL_STR); + + const getWithdrawParams = { + amount, + source_asset: sourceAssetCode, + destination_asset: destinationAsset, + quote_id: quoteId, + account: publicKey, + claimable_balance_supported: claimableBalanceSupported.toString(), + type, + ...withdrawFields, + }; + + each(getWithdrawParams, (value, key) => { + if (value) { + REQUEST_URL.searchParams.append(key, value); + } + }); + + log.request({ + title: `${API_METHOD} \`${REQUEST_URL_STR}\``, + body: getWithdrawParams, + }); + + const response = await fetch(`${REQUEST_URL}`, { + method: API_METHOD, + headers: { + Authorization: `Bearer ${token}`, + }, + }); + + const withdrawJson = await response.json(); + + // "non_interactive_customer_info_needed" (403) case is handled later + if ( + withdrawJson.type === + TransactionStatus.NON_INTERACTIVE_CUSTOMER_INFO_NEEDED || + response.status === 200 + ) { + log.response({ + title: `${API_METHOD} \`${REQUEST_URL_STR}\``, + body: withdrawJson, + }); + + return withdrawJson; + } + + throw new Error(withdrawJson.error); +}; diff --git a/packages/demo-wallet-shared/types/types.ts b/packages/demo-wallet-shared/types/types.ts index 48bff97d..643bd88b 100644 --- a/packages/demo-wallet-shared/types/types.ts +++ b/packages/demo-wallet-shared/types/types.ts @@ -464,6 +464,8 @@ export enum MemoTypeString { export enum AnchorActionType { DEPOSIT = "deposit", WITHDRAWAL = "withdraw", + DEPOSIT_EXCHANGE = "deposit-exchange", + WITHDRAW_EXCHANGE = "withdraw-exchange", } interface InfoTypeData { @@ -485,6 +487,12 @@ export interface CheckInfoData { [AnchorActionType.WITHDRAWAL]: { [asset: string]: InfoTypeData; }; + [AnchorActionType.DEPOSIT_EXCHANGE]?: { + [asset: string]: InfoTypeData; + }; + [AnchorActionType.WITHDRAW_EXCHANGE]?: { + [asset: string]: InfoTypeData; + }; } export enum Sep8ApprovalStatus {