From 214359c1453df42b4a41e68843770b2f6bbcba09 Mon Sep 17 00:00:00 2001 From: Iveta Date: Wed, 26 Jun 2024 16:50:08 -0400 Subject: [PATCH 1/5] SEP-6 deposit updated --- .../src/components/Sep6/Sep6Deposit.tsx | 4 +- .../src/ducks/sep6DepositAsset.ts | 92 ++++++++----------- .../src/ducks/sep6WithdrawAsset.ts | 24 +++-- .../demo-wallet-client/src/types/types.ts | 2 +- .../methods/sep12/collectSep12Fields.ts | 17 ++-- .../methods/sep12/putSep12FieldsRequest.ts | 3 + .../methods/sep31Send/getSep12Fields.ts | 36 ++++---- 7 files changed, 88 insertions(+), 90 deletions(-) diff --git a/packages/demo-wallet-client/src/components/Sep6/Sep6Deposit.tsx b/packages/demo-wallet-client/src/components/Sep6/Sep6Deposit.tsx index 6271f90f..29283117 100644 --- a/packages/demo-wallet-client/src/components/Sep6/Sep6Deposit.tsx +++ b/packages/demo-wallet-client/src/components/Sep6/Sep6Deposit.tsx @@ -204,7 +204,7 @@ export const Sep6Deposit = () => { - + @@ -337,7 +337,7 @@ export const Sep6Deposit = () => { ); } - if (sep6DepositAsset.status === ActionStatus.CAN_PROCEED) { + if (sep6DepositAsset.status === ActionStatus.CAN_PROCEED && depositResponse) { return ( SEP-6 Deposit Details diff --git a/packages/demo-wallet-client/src/ducks/sep6DepositAsset.ts b/packages/demo-wallet-client/src/ducks/sep6DepositAsset.ts index 62b1f99d..a57c6ac4 100644 --- a/packages/demo-wallet-client/src/ducks/sep6DepositAsset.ts +++ b/packages/demo-wallet-client/src/ducks/sep6DepositAsset.ts @@ -33,6 +33,7 @@ import { AnyObject, TransactionStatus, SepInstructions, + Sep12CustomerStatus, } from "types/types"; type InitiateDepositActionPayload = Sep6DepositAssetInitialState["data"] & { @@ -165,42 +166,31 @@ export const initiateDepositAction = createAsyncThunk< }, ); +// Submit transaction to start polling for the status export const submitSep6DepositFields = createAsyncThunk< { status: ActionStatus; - depositResponse: Sep6DepositResponse; - customerFields?: AnyObject; + depositResponse?: Sep6DepositResponse; }, { 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,32 +203,6 @@ 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)", - }); - - // Get SEP-12 fields - log.instruction({ - title: "Making GET `/customer` request for user", - }); - - const customerFields = await collectSep12Fields({ - publicKey: data?.id!, - token, - kycServer, - }); - - return { - status: ActionStatus.NEEDS_KYC, - depositResponse, - customerFields, - }; - } - return { status: ActionStatus.CAN_PROCEED, depositResponse, @@ -258,14 +222,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,9 +240,29 @@ 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, }; @@ -346,7 +330,7 @@ export const sep6DepositAction = createAsyncThunk< trustedAssetAdded = "", requiredCustomerInfoUpdates, } = await pollDepositUntilComplete({ - transactionId: depositResponse.id || "", + transactionId: depositResponse?.id || "", token, transferServerUrl, trustAssetCallback, @@ -363,18 +347,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, @@ -450,10 +437,6 @@ 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; @@ -466,6 +449,7 @@ const sep6DepositAssetSlice = createSlice({ }); 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; diff --git a/packages/demo-wallet-client/src/ducks/sep6WithdrawAsset.ts b/packages/demo-wallet-client/src/ducks/sep6WithdrawAsset.ts index 77f1a223..b76cff84 100644 --- a/packages/demo-wallet-client/src/ducks/sep6WithdrawAsset.ts +++ b/packages/demo-wallet-client/src/ducks/sep6WithdrawAsset.ts @@ -216,11 +216,13 @@ export const submitSep6WithdrawFields = createAsyncThunk< title: "Making GET `/customer` request for user", }); - const customerFields = await collectSep12Fields({ - publicKey, - token, - kycServer, - }); + const customerFields = ( + await collectSep12Fields({ + publicKey, + token, + kycServer, + }) + ).fieldsToCollect; return { status: ActionStatus.NEEDS_KYC, @@ -297,11 +299,13 @@ 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, + }) + ).fieldsToCollect; } return { diff --git a/packages/demo-wallet-client/src/types/types.ts b/packages/demo-wallet-client/src/types/types.ts index 0f4c07b4..f6e85f0a 100644 --- a/packages/demo-wallet-client/src/types/types.ts +++ b/packages/demo-wallet-client/src/types/types.ts @@ -239,7 +239,7 @@ export interface Sep6DepositAssetInitialState { customerFields: { [key: string]: AnyObject; }; - depositResponse: Sep6DepositResponse; + depositResponse?: Sep6DepositResponse; trustedAssetAdded: string; requiredCustomerInfoUpdates: AnyObject[] | undefined; instructions: SepInstructions | undefined; 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; } From 65a8501331f621226ed3ecdc90b2fa7d01557524 Mon Sep 17 00:00:00 2001 From: Iveta Date: Wed, 26 Jun 2024 17:52:13 -0400 Subject: [PATCH 2/5] SEP-6 withdrawal updated --- .../src/components/Sep6/Sep6Deposit.tsx | 2 +- .../src/components/Sep6/Sep6Withdraw.tsx | 2 +- .../src/ducks/sep6DepositAsset.ts | 3 +- .../src/ducks/sep6WithdrawAsset.ts | 80 +++++++------------ .../demo-wallet-client/src/types/types.ts | 2 +- 5 files changed, 32 insertions(+), 57 deletions(-) diff --git a/packages/demo-wallet-client/src/components/Sep6/Sep6Deposit.tsx b/packages/demo-wallet-client/src/components/Sep6/Sep6Deposit.tsx index 29283117..f848e23b 100644 --- a/packages/demo-wallet-client/src/components/Sep6/Sep6Deposit.tsx +++ b/packages/demo-wallet-client/src/components/Sep6/Sep6Deposit.tsx @@ -337,7 +337,7 @@ export const Sep6Deposit = () => { ); } - if (sep6DepositAsset.status === ActionStatus.CAN_PROCEED && depositResponse) { + if (sep6DepositAsset.status === ActionStatus.CAN_PROCEED) { return ( SEP-6 Deposit Details diff --git a/packages/demo-wallet-client/src/components/Sep6/Sep6Withdraw.tsx b/packages/demo-wallet-client/src/components/Sep6/Sep6Withdraw.tsx index 134a8ee5..c11fa576 100644 --- a/packages/demo-wallet-client/src/components/Sep6/Sep6Withdraw.tsx +++ b/packages/demo-wallet-client/src/components/Sep6/Sep6Withdraw.tsx @@ -202,7 +202,7 @@ export const Sep6Withdraw = () => { - + diff --git a/packages/demo-wallet-client/src/ducks/sep6DepositAsset.ts b/packages/demo-wallet-client/src/ducks/sep6DepositAsset.ts index a57c6ac4..37a0ddbb 100644 --- a/packages/demo-wallet-client/src/ducks/sep6DepositAsset.ts +++ b/packages/demo-wallet-client/src/ducks/sep6DepositAsset.ts @@ -170,7 +170,7 @@ export const initiateDepositAction = createAsyncThunk< export const submitSep6DepositFields = createAsyncThunk< { status: ActionStatus; - depositResponse?: Sep6DepositResponse; + depositResponse: Sep6DepositResponse; }, { amount?: string; @@ -465,7 +465,6 @@ const sep6DepositAssetSlice = createSlice({ state.data.trustedAssetAdded = action.payload.trustedAssetAdded; const customerFields = { - ...state.data.customerFields, ...action.payload.customerFields, }; diff --git a/packages/demo-wallet-client/src/ducks/sep6WithdrawAsset.ts b/packages/demo-wallet-client/src/ducks/sep6WithdrawAsset.ts index b76cff84..8ab18e53 100644 --- a/packages/demo-wallet-client/src/ducks/sep6WithdrawAsset.ts +++ b/packages/demo-wallet-client/src/ducks/sep6WithdrawAsset.ts @@ -31,6 +31,7 @@ import { AnchorActionType, AnyObject, TransactionStatus, + Sep12CustomerStatus, } from "types/types"; type InitiateWithdrawActionPayload = Sep6WithdrawAssetInitialState["data"] & { @@ -158,40 +159,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,34 +191,6 @@ 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)", - }); - - // Get SEP-12 fields - log.instruction({ - title: "Making GET `/customer` request for user", - }); - - const customerFields = ( - await collectSep12Fields({ - publicKey, - token, - kycServer, - }) - ).fieldsToCollect; - - return { - status: ActionStatus.NEEDS_KYC, - withdrawResponse, - customerFields, - }; - } - return { status: ActionStatus.CAN_PROCEED, withdrawResponse, @@ -334,14 +294,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; @@ -351,9 +311,29 @@ 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, }; @@ -419,10 +399,6 @@ 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; @@ -437,6 +413,7 @@ const sep6WithdrawAssetSlice = createSlice({ submitSep6WithdrawCustomerInfoFields.fulfilled, (state, action) => { state.status = action.payload.status; + state.data.fields = { ...action.payload.customerFields }; }, ); builder.addCase( @@ -457,7 +434,6 @@ const sep6WithdrawAssetSlice = createSlice({ state.data.transactionResponse = action.payload.transactionResponse; const customerFields = { - ...state.data.fields, ...action.payload.customerFields, }; diff --git a/packages/demo-wallet-client/src/types/types.ts b/packages/demo-wallet-client/src/types/types.ts index f6e85f0a..0f4c07b4 100644 --- a/packages/demo-wallet-client/src/types/types.ts +++ b/packages/demo-wallet-client/src/types/types.ts @@ -239,7 +239,7 @@ export interface Sep6DepositAssetInitialState { customerFields: { [key: string]: AnyObject; }; - depositResponse?: Sep6DepositResponse; + depositResponse: Sep6DepositResponse; trustedAssetAdded: string; requiredCustomerInfoUpdates: AnyObject[] | undefined; instructions: SepInstructions | undefined; From efcbda6ba1c9f94e52c77f210a6de6e7e56c2444 Mon Sep 17 00:00:00 2001 From: Iveta Date: Thu, 27 Jun 2024 11:29:40 -0400 Subject: [PATCH 3/5] Pass txn id to withdraw customer request --- packages/demo-wallet-client/src/ducks/sep6WithdrawAsset.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/demo-wallet-client/src/ducks/sep6WithdrawAsset.ts b/packages/demo-wallet-client/src/ducks/sep6WithdrawAsset.ts index 8ab18e53..0a1e08c7 100644 --- a/packages/demo-wallet-client/src/ducks/sep6WithdrawAsset.ts +++ b/packages/demo-wallet-client/src/ducks/sep6WithdrawAsset.ts @@ -264,6 +264,7 @@ export const sep6WithdrawAction = createAsyncThunk< publicKey: data?.id!, token, kycServer, + transactionId: withdrawResponse?.id, }) ).fieldsToCollect; } From fae0ce33e7c3cc3531591aab4d008b0919e166b7 Mon Sep 17 00:00:00 2001 From: Iveta Date: Thu, 27 Jun 2024 15:34:44 -0400 Subject: [PATCH 4/5] Update flows --- .../src/components/Sep6/Sep6Deposit.tsx | 33 ++++++++++++++++--- .../src/components/Sep6/Sep6Withdraw.tsx | 33 ++++++++++++++++--- .../src/ducks/sep6DepositAsset.ts | 2 +- .../src/ducks/sep6WithdrawAsset.ts | 4 +-- .../demo-wallet-client/src/types/types.ts | 1 + 5 files changed, 62 insertions(+), 11 deletions(-) diff --git a/packages/demo-wallet-client/src/components/Sep6/Sep6Deposit.tsx b/packages/demo-wallet-client/src/components/Sep6/Sep6Deposit.tsx index f848e23b..53f32a38 100644 --- a/packages/demo-wallet-client/src/components/Sep6/Sep6Deposit.tsx +++ b/packages/demo-wallet-client/src/components/Sep6/Sep6Deposit.tsx @@ -121,12 +121,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); @@ -360,6 +365,26 @@ export const Sep6Deposit = () => { ); } + if (sep6DepositAsset.status === ActionStatus.KYC_DONE) { + return ( + setIsInfoModalVisible(false)} + parentId={CSS_MODAL_PARENT_ID} + > + SEP-6 Deposit + + +

Submit the deposit.

+
+ + + + +
+ ); + } + if (sep6DepositAsset.data.instructions) { return ( { ) => { 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); @@ -383,6 +388,26 @@ export const Sep6Withdraw = () => { ); } + if (sep6WithdrawAsset.status === ActionStatus.KYC_DONE) { + return ( + setIsInfoModalVisible(false)} + parentId={CSS_MODAL_PARENT_ID} + > + SEP-6 Withdrawal + + +

Submit the withdrawal.

+
+ + + + +
+ ); + } + if (sep6WithdrawAsset.status === ActionStatus.SUCCESS) { return ( Date: Wed, 3 Jul 2024 13:04:35 -0400 Subject: [PATCH 5/5] SEP-6 quotes (#355) * SEP-6 deposit quotes in progress * Show only selected asset prices * Fix quotes post assets * SEP-38 flow updates * Quotes deposit works * Quotes withdrawal works --- .../src/components/AnchorQuotesModal.tsx | 311 +++++++++++++----- .../src/components/Sep31Send.tsx | 8 +- .../src/components/Sep6/Sep6Deposit.tsx | 72 ++++ .../src/components/Sep6/Sep6Withdraw.tsx | 135 ++++++-- .../src/ducks/sep38Quotes.ts | 130 ++++++-- .../src/ducks/sep6DepositAsset.ts | 114 ++++++- .../src/ducks/sep6WithdrawAsset.ts | 110 ++++++- .../demo-wallet-client/src/types/types.ts | 13 +- .../methods/sep38Quotes/getInfo.ts | 61 ++-- .../methods/sep38Quotes/getPrices.ts | 6 +- .../demo-wallet-shared/methods/sep6/index.ts | 4 + .../sep6/programmaticDepositExchangeFlow.ts | 73 ++++ .../sep6/programmaticWithdrawExchangeFlow.ts | 84 +++++ packages/demo-wallet-shared/types/types.ts | 8 + 14 files changed, 959 insertions(+), 170 deletions(-) create mode 100644 packages/demo-wallet-shared/methods/sep6/programmaticDepositExchangeFlow.ts create mode 100644 packages/demo-wallet-shared/methods/sep6/programmaticWithdrawExchangeFlow.ts 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 53f32a38..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(); }; @@ -155,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, ) => { @@ -172,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 ( @@ -333,6 +397,14 @@ export const Sep6Deposit = () => { + @@ -324,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: @@ -382,7 +455,7 @@ export const Sep6Withdraw = () => { - + ); @@ -402,7 +475,7 @@ export const Sep6Withdraw = () => { - + ); 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 b9294a6f..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 { @@ -81,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 { @@ -100,6 +116,7 @@ export const initiateDepositAction = createAsyncThunk< status: ActionStatus.NEEDS_INPUT, token: "", transferServerUrl: tomlResponse.TRANSFER_SERVER, + anchorQuoteServer, } as InitiateDepositActionPayload; if (isAuthenticationRequired) { @@ -221,6 +238,73 @@ export const submitSep6DepositFields = createAsyncThunk< }, ); +// 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, + depositResponse, + }; + } catch (e) { + const errorMessage = getErrorMessage(e); + + log.error({ + title: errorMessage, + }); + + return rejectWithValue({ + errorString: errorMessage, + }); + } + }, +); + export const submitSep6CustomerInfoFields = createAsyncThunk< { status: ActionStatus; customerFields?: AnyObject }, AnyObject, @@ -402,6 +486,7 @@ const initialState: Sep6DepositAssetInitialState = { trustedAssetAdded: "", requiredCustomerInfoUpdates: undefined, instructions: undefined, + anchorQuoteServer: undefined, }, status: "" as ActionStatus, errorString: undefined, @@ -415,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) => { @@ -443,6 +531,25 @@ const sep6DepositAssetSlice = createSlice({ 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; @@ -488,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 35258630..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 { @@ -79,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 } = @@ -93,6 +109,7 @@ export const initiateWithdrawAction = createAsyncThunk< status: ActionStatus.NEEDS_INPUT, token: "", transferServerUrl: tomlResponse.TRANSFER_SERVER, + anchorQuoteServer, } as InitiateWithdrawActionPayload; if (isAuthenticationRequired) { @@ -209,6 +226,73 @@ export const submitSep6WithdrawFields = createAsyncThunk< }, ); +// 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, + withdrawResponse, + }; + } catch (e) { + const errorMessage = getErrorMessage(e); + + log.error({ + title: errorMessage, + }); + + return rejectWithValue({ + errorString: errorMessage, + }); + } + }, +); + export const sep6WithdrawAction = createAsyncThunk< { currentStatus: TransactionStatus; @@ -368,6 +452,7 @@ const initialState: Sep6WithdrawAssetInitialState = { transactionResponse: {}, withdrawResponse: { account_id: "" }, requiredCustomerInfoUpdates: undefined, + anchorQuoteServer: undefined, }, status: undefined, errorString: undefined, @@ -378,6 +463,9 @@ const sep6WithdrawAssetSlice = createSlice({ initialState, reducers: { resetSep6WithdrawAction: () => initialState, + setStatusAction: (state, action) => { + state.status = action.payload; + }, }, extraReducers: (builder) => { builder.addCase(initiateWithdrawAction.pending, (state) => { @@ -406,6 +494,25 @@ const sep6WithdrawAssetSlice = createSlice({ 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; @@ -459,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 19651906..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; @@ -493,6 +496,8 @@ export enum MemoTypeString { export enum AnchorActionType { DEPOSIT = "deposit", WITHDRAWAL = "withdraw", + DEPOSIT_EXCHANGE = "deposit-exchange", + WITHDRAW_EXCHANGE = "withdraw-exchange", } interface InfoTypeData { @@ -514,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/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 {