1import logger from '../logger'
2import * as TeamBuildingGen from '../actions/team-building-gen'
3import * as Constants from '../constants/wallets'
4import * as Container from '../util/container'
5import * as Types from '../constants/types/wallets'
6import * as WalletsGen from '../actions/wallets-gen'
7import HiddenString from '../util/hidden-string'
8import {editTeambuildingDraft} from './team-building'
9import {teamBuilderReducerCreator} from '../team-building/reducer-helper'
10import shallowEqual from 'shallowequal'
11import {mapEqual} from '../util/map'
12
13const initialState: Types.State = Constants.makeState()
14
15const updateAssetMap = (
16  assetMap: Map<Types.AssetID, Types.AssetDescription>,
17  assets: Array<Types.AssetDescription>
18) =>
19  assets.forEach(asset => {
20    const key = Types.assetDescriptionToAssetID(asset)
21    const oldAsset = assetMap.get(key)
22    if (!shallowEqual(asset, oldAsset)) {
23      assetMap.set(key, asset)
24    }
25  })
26
27type Actions = WalletsGen.Actions | TeamBuildingGen.Actions
28export default Container.makeReducer<Actions, Types.State>(initialState, {
29  [WalletsGen.resetStore]: draftState => {
30    return {...initialState, staticConfig: draftState.staticConfig} as Types.State
31  },
32  [WalletsGen.didSetAccountAsDefault]: (draftState, action) => {
33    draftState.accountMap = new Map(action.payload.accounts.map(account => [account.accountID, account]))
34  },
35  [WalletsGen.accountsReceived]: (draftState, action) => {
36    draftState.accountMap = new Map(action.payload.accounts.map(account => [account.accountID, account]))
37  },
38  [WalletsGen.changedAccountName]: (draftState, action) => {
39    const {account} = action.payload
40    // accept the updated account if we've loaded it already
41    // this is because we get the sort order from the full accounts load,
42    // and can't figure it out from these notifications alone.
43    if (account) {
44      // } && state.accountMap.get(account.accountID)) {
45      const {accountID} = account
46      const old = draftState.accountMap.get(accountID)
47      if (old) {
48        draftState.accountMap.set(accountID, {...old, ...account})
49      }
50    }
51  },
52  [WalletsGen.accountUpdateReceived]: (draftState, action) => {
53    const {account} = action.payload
54    // accept the updated account if we've loaded it already
55    // this is because we get the sort order from the full accounts load,
56    // and can't figure it out from these notifications alone.
57    if (account) {
58      const {accountID} = account
59      const old = draftState.accountMap.get(accountID)
60      if (old) {
61        draftState.accountMap.set(accountID, {...old, ...account})
62      }
63    }
64  },
65  [WalletsGen.assetsReceived]: (draftState, action) => {
66    draftState.assetsMap.set(action.payload.accountID, action.payload.assets)
67  },
68  [WalletsGen.buildPayment]: draftState => {
69    draftState.buildCounter++
70  },
71  [WalletsGen.builtPaymentReceived]: (draftState, action) => {
72    if (action.payload.forBuildCounter === draftState.buildCounter) {
73      draftState.builtPayment = {
74        ...draftState.builtPayment,
75        ...Constants.makeBuiltPayment(action.payload.build),
76      }
77    }
78  },
79  [WalletsGen.builtRequestReceived]: (draftState, action) => {
80    if (action.payload.forBuildCounter === draftState.buildCounter) {
81      draftState.builtRequest = {
82        ...draftState.builtRequest,
83        ...Constants.makeBuiltRequest(action.payload.build),
84      }
85    }
86  },
87  [WalletsGen.openSendRequestForm]: (draftState, action) => {
88    if (!draftState.acceptedDisclaimer) {
89      return
90    }
91    draftState.building = {
92      ...Constants.makeBuilding(),
93      amount: action.payload.amount || '',
94      currency:
95        action.payload.currency || // explicitly set
96        (draftState.lastSentXLM && 'XLM') || // lastSentXLM override
97        (action.payload.from &&
98          Constants.getDisplayCurrencyInner(draftState as Types.State, action.payload.from).code) || // display currency of explicitly set 'from' account
99        Constants.getDefaultDisplayCurrencyInner(draftState as Types.State).code || // display currency of default account
100        '', // Empty string -> not loaded
101      from: action.payload.from || Types.noAccountID,
102      isRequest: !!action.payload.isRequest,
103      publicMemo: action.payload.publicMemo || new HiddenString(''),
104      recipientType: action.payload.recipientType || 'keybaseUser',
105      secretNote: action.payload.secretNote || new HiddenString(''),
106      to: action.payload.to || '',
107    }
108    draftState.builtPayment = Constants.makeBuiltPayment()
109    draftState.builtRequest = Constants.makeBuiltRequest()
110    draftState.sentPaymentError = ''
111  },
112  [WalletsGen.abandonPayment]: draftState => {
113    draftState.building = Constants.makeBuilding()
114  },
115  [WalletsGen.clearBuilding]: draftState => {
116    draftState.building = Constants.makeBuilding()
117  },
118  [WalletsGen.clearBuiltPayment]: draftState => {
119    draftState.builtPayment = Constants.makeBuiltPayment()
120  },
121  [WalletsGen.clearBuiltRequest]: draftState => {
122    draftState.builtRequest = Constants.makeBuiltRequest()
123  },
124  [WalletsGen.externalPartnersReceived]: (draftState, action) => {
125    draftState.externalPartners = action.payload.externalPartners
126  },
127  [WalletsGen.paymentDetailReceived]: (draftState, action) => {
128    const emptyMap: Map<Types.PaymentID, Types.Payment> = new Map()
129    const map = draftState.paymentsMap.get(action.payload.accountID) ?? emptyMap
130
131    const paymentDetail = action.payload.payment
132    map.set(paymentDetail.id, {
133      ...(map.get(paymentDetail.id) ?? Constants.makePayment()),
134      ...action.payload.payment,
135    })
136
137    draftState.paymentsMap.set(action.payload.accountID, map)
138  },
139  [WalletsGen.paymentsReceived]: (draftState, action) => {
140    const emptyMap: Map<Types.PaymentID, Types.Payment> = new Map()
141    const map = draftState.paymentsMap.get(action.payload.accountID) ?? emptyMap
142
143    const paymentResults = [...action.payload.payments, ...action.payload.pending]
144    paymentResults.forEach(paymentResult => {
145      map.set(paymentResult.id, {
146        ...(map.get(paymentResult.id) ?? Constants.makePayment()),
147        ...paymentResult,
148      })
149    })
150    draftState.loadPaymentsError = action.payload.error
151    draftState.paymentsMap.set(action.payload.accountID, map)
152    draftState.paymentCursorMap.set(action.payload.accountID, action.payload.paymentCursor)
153    draftState.paymentLoadingMoreMap.set(action.payload.accountID, false)
154    // allowClearOldestUnread dictates whether this action is allowed to delete the value of oldestUnread.
155    // GetPaymentsLocal can erroneously return an empty oldestUnread value when a non-latest page is requested
156    // and oldestUnread points into the latest page.
157    if (
158      action.payload.allowClearOldestUnread ||
159      (action.payload.oldestUnread || Types.noPaymentID) !== Types.noPaymentID
160    ) {
161      draftState.paymentOldestUnreadMap.set(action.payload.accountID, action.payload.oldestUnread)
162    }
163  },
164  [WalletsGen.pendingPaymentsReceived]: (draftState, action) => {
165    const newPending = action.payload.pending.map(p => [p.id, Constants.makePayment(p)] as const)
166    const emptyMap: Map<Types.PaymentID, Types.Payment> = new Map()
167    const oldFiltered = [
168      ...(draftState.paymentsMap.get(action.payload.accountID) ?? emptyMap).entries(),
169    ].filter(([_k, v]) => v.section !== 'pending')
170    const val = new Map([...oldFiltered, ...newPending])
171    draftState.paymentsMap.set(action.payload.accountID, val)
172  },
173  [WalletsGen.recentPaymentsReceived]: (draftState, action) => {
174    const newPayments = action.payload.payments.map(p => [p.id, Constants.makePayment(p)] as const)
175    const emptyMap: Map<Types.PaymentID, Types.Payment> = new Map()
176    const old = (draftState.paymentsMap.get(action.payload.accountID) ?? emptyMap).entries()
177
178    draftState.paymentsMap.set(action.payload.accountID, new Map([...old, ...newPayments]))
179    draftState.paymentCursorMap.set(
180      action.payload.accountID,
181      draftState.paymentCursorMap.get(action.payload.accountID) || action.payload.paymentCursor
182    )
183    draftState.paymentOldestUnreadMap.set(action.payload.accountID, action.payload.oldestUnread)
184  },
185  [WalletsGen.displayCurrenciesReceived]: (draftState, action) => {
186    draftState.currencies = action.payload.currencies
187  },
188  [WalletsGen.displayCurrencyReceived]: (draftState, action) => {
189    const account = Constants.getAccountInner(
190      draftState as Types.State,
191      action.payload.accountID || Types.noAccountID
192    )
193    if (account.accountID === Types.noAccountID) {
194      return
195    }
196    draftState.accountMap.set(account.accountID, {...account, displayCurrency: action.payload.currency})
197  },
198  [WalletsGen.reviewPayment]: draftState => {
199    draftState.builtPayment.reviewBanners = []
200    draftState.reviewCounter++
201    draftState.reviewLastSeqno = undefined
202  },
203  [WalletsGen.reviewedPaymentReceived]: (draftState, action) => {
204    // paymentReviewed notifications can arrive out of order, so check their freshness.
205    const {bid, reviewID, seqno, banners, nextButton} = action.payload
206    const useable =
207      draftState.building.bid === bid &&
208      draftState.reviewCounter === reviewID &&
209      (draftState.reviewLastSeqno || 0) <= seqno
210    if (!useable) {
211      logger.info(`ignored stale reviewPaymentReceived`)
212      return
213    }
214
215    draftState.builtPayment.readyToSend = nextButton
216    draftState.builtPayment.reviewBanners = banners ?? null
217    draftState.reviewLastSeqno = seqno
218  },
219  [WalletsGen.secretKeyReceived]: (draftState, action) => {
220    draftState.exportedSecretKey = action.payload.secretKey
221    draftState.exportedSecretKeyAccountID = draftState.selectedAccount
222  },
223  [WalletsGen.secretKeySeen]: draftState => {
224    draftState.exportedSecretKey = new HiddenString('')
225    draftState.exportedSecretKeyAccountID = Types.noAccountID
226  },
227  [WalletsGen.selectAccount]: (draftState, action) => {
228    if (!action.payload.accountID) {
229      logger.error('Selecting empty account ID')
230    }
231    draftState.exportedSecretKey = new HiddenString('')
232    const old = draftState.selectedAccount
233    draftState.selectedAccount = action.payload.accountID
234    // we clear the old selected payments and cursors
235    if (!old) {
236      return
237    }
238
239    draftState.paymentCursorMap.delete(old)
240    draftState.paymentsMap.delete(old)
241  },
242  [WalletsGen.setBuildingAmount]: (draftState, action) => {
243    draftState.building.amount = action.payload.amount
244    draftState.builtPayment.amountErrMsg = ''
245    draftState.builtPayment.worthDescription = ''
246    draftState.builtPayment.worthInfo = ''
247    draftState.builtRequest.amountErrMsg = ''
248    draftState.builtRequest.worthDescription = ''
249    draftState.builtRequest.worthInfo = ''
250  },
251  [WalletsGen.setBuildingCurrency]: (draftState, action) => {
252    draftState.building.currency = action.payload.currency
253    draftState.builtPayment = Constants.makeBuiltPayment()
254  },
255  [WalletsGen.setBuildingFrom]: (draftState, action) => {
256    draftState.building.from = action.payload.from
257    draftState.builtPayment = Constants.makeBuiltPayment()
258  },
259  [WalletsGen.setBuildingIsRequest]: (draftState, action) => {
260    draftState.building.isRequest = action.payload.isRequest
261    draftState.builtPayment = Constants.makeBuiltPayment()
262    draftState.builtRequest = Constants.makeBuiltRequest()
263  },
264  [WalletsGen.setBuildingPublicMemo]: (draftState, action) => {
265    draftState.building.publicMemo = action.payload.publicMemo
266    draftState.builtPayment.publicMemoErrMsg = new HiddenString('')
267  },
268  [WalletsGen.setBuildingRecipientType]: (draftState, action) => {
269    draftState.building.recipientType = action.payload.recipientType
270    draftState.builtPayment = Constants.makeBuiltPayment()
271  },
272  [WalletsGen.setBuildingSecretNote]: (draftState, action) => {
273    draftState.building.secretNote = action.payload.secretNote
274    draftState.builtPayment.secretNoteErrMsg = new HiddenString('')
275    draftState.builtRequest.secretNoteErrMsg = new HiddenString('')
276  },
277  [WalletsGen.setBuildingTo]: (draftState, action) => {
278    draftState.building.to = action.payload.to
279    draftState.builtPayment.toErrMsg = ''
280    draftState.builtRequest.toErrMsg = ''
281  },
282  [WalletsGen.clearBuildingAdvanced]: draftState => {
283    draftState.buildingAdvanced = Constants.emptyBuildingAdvanced
284    draftState.builtPaymentAdvanced = Constants.emptyBuiltPaymentAdvanced
285  },
286  [WalletsGen.setBuildingAdvancedRecipient]: (draftState, action) => {
287    draftState.buildingAdvanced.recipient = action.payload.recipient
288  },
289  [WalletsGen.setBuildingAdvancedRecipientAmount]: (draftState, action) => {
290    draftState.buildingAdvanced.recipientAmount = action.payload.recipientAmount
291    draftState.builtPaymentAdvanced = Constants.emptyBuiltPaymentAdvanced
292  },
293  [WalletsGen.setBuildingAdvancedRecipientAsset]: (draftState, action) => {
294    draftState.buildingAdvanced.recipientAsset = action.payload.recipientAsset
295    draftState.builtPaymentAdvanced = Constants.emptyBuiltPaymentAdvanced
296  },
297  [WalletsGen.setBuildingAdvancedRecipientType]: (draftState, action) => {
298    draftState.buildingAdvanced.recipientType = action.payload.recipientType
299  },
300  [WalletsGen.setBuildingAdvancedPublicMemo]: (draftState, action) => {
301    draftState.buildingAdvanced.publicMemo = action.payload.publicMemo
302    // TODO PICNIC-142 clear error when we have that
303  },
304  [WalletsGen.setBuildingAdvancedSenderAccountID]: (draftState, action) => {
305    draftState.buildingAdvanced.senderAccountID = action.payload.senderAccountID
306  },
307  [WalletsGen.setBuildingAdvancedSenderAsset]: (draftState, action) => {
308    draftState.buildingAdvanced.senderAsset = action.payload.senderAsset
309    draftState.builtPaymentAdvanced = Constants.emptyBuiltPaymentAdvanced
310  },
311  [WalletsGen.setBuildingAdvancedSecretNote]: (draftState, action) => {
312    draftState.buildingAdvanced.secretNote = action.payload.secretNote
313    // TODO PICNIC-142 clear error when we have that
314  },
315  [WalletsGen.sendAssetChoicesReceived]: (draftState, action) => {
316    const {sendAssetChoices} = action.payload
317    draftState.building.sendAssetChoices = sendAssetChoices
318  },
319  [WalletsGen.buildingPaymentIDReceived]: (draftState, action) => {
320    const {bid} = action.payload
321    draftState.building.bid = bid
322  },
323  [WalletsGen.setLastSentXLM]: (draftState, action) => {
324    draftState.lastSentXLM = action.payload.lastSentXLM
325  },
326  [WalletsGen.setReadyToReview]: (draftState, action) => {
327    draftState.builtPayment.readyToReview = action.payload.readyToReview
328  },
329  [WalletsGen.validateAccountName]: (draftState, action) => {
330    draftState.accountName = action.payload.name
331    draftState.accountNameValidationState = 'waiting'
332  },
333  [WalletsGen.validatedAccountName]: (draftState, action) => {
334    if (action.payload.name !== draftState.accountName) {
335      // this wasn't from the most recent call
336      return
337    }
338    draftState.accountName = ''
339    draftState.accountNameError = action.payload.error ? action.payload.error : ''
340    draftState.accountNameValidationState = action.payload.error ? 'error' : 'valid'
341  },
342  [WalletsGen.validateSecretKey]: (draftState, action) => {
343    draftState.secretKey = action.payload.secretKey
344    draftState.secretKeyValidationState = 'waiting'
345  },
346  [WalletsGen.validatedSecretKey]: (draftState, action) => {
347    if (action.payload.secretKey.stringValue() !== draftState.secretKey.stringValue()) {
348      // this wasn't from the most recent call
349      return
350    }
351    draftState.secretKey = new HiddenString('')
352    draftState.secretKeyError = action.payload.error ? action.payload.error : ''
353    draftState.secretKeyValidationState = action.payload.error ? 'error' : 'valid'
354  },
355  [WalletsGen.changedTrustline]: (draftState, action) => {
356    draftState.changeTrustlineError = action.payload.error || ''
357  },
358  [WalletsGen.clearErrors]: draftState => {
359    draftState.accountName = ''
360    draftState.accountNameError = ''
361    draftState.accountNameValidationState = 'none'
362    draftState.builtPayment.readyToSend = 'spinning'
363    draftState.changeTrustlineError = ''
364    draftState.createNewAccountError = ''
365    draftState.linkExistingAccountError = ''
366    draftState.secretKey = new HiddenString('')
367    draftState.secretKeyError = ''
368    draftState.secretKeyValidationState = 'none'
369    draftState.sentPaymentError = ''
370  },
371  [WalletsGen.createdNewAccount]: (draftState, action) => {
372    if (action.payload.error) {
373      draftState.createNewAccountError = action.payload.error ?? ''
374    } else {
375      draftState.accountName = ''
376      draftState.accountNameError = ''
377      draftState.accountNameValidationState = 'none'
378      draftState.changeTrustlineError = ''
379      draftState.createNewAccountError = ''
380      draftState.linkExistingAccountError = ''
381      draftState.secretKey = new HiddenString('')
382      draftState.secretKeyError = ''
383      draftState.secretKeyValidationState = 'none'
384      draftState.selectedAccount = action.payload.accountID
385    }
386  },
387  [WalletsGen.linkedExistingAccount]: (draftState, action) => {
388    if (action.payload.error) {
389      draftState.linkExistingAccountError = action.payload.error ?? ''
390    } else {
391      draftState.accountName = ''
392      draftState.accountNameError = ''
393      draftState.accountNameValidationState = 'none'
394      draftState.createNewAccountError = ''
395      draftState.linkExistingAccountError = ''
396      draftState.secretKey = new HiddenString('')
397      draftState.secretKeyError = ''
398      draftState.secretKeyValidationState = 'none'
399      draftState.selectedAccount = action.payload.accountID
400    }
401  },
402  [WalletsGen.sentPaymentError]: (draftState, action) => {
403    draftState.sentPaymentError = action.payload.error
404  },
405  [WalletsGen.loadMorePayments]: (draftState, action) => {
406    if (draftState.paymentCursorMap.get(action.payload.accountID)) {
407      draftState.paymentLoadingMoreMap.set(action.payload.accountID, true)
408    }
409  },
410  [WalletsGen.badgesUpdated]: (draftState, action) => {
411    action.payload.accounts.forEach(({accountID, numUnread}) =>
412      draftState.unreadPaymentsMap.set(accountID, numUnread)
413    )
414  },
415  [WalletsGen.walletDisclaimerReceived]: (draftState, action) => {
416    draftState.acceptedDisclaimer = action.payload.accepted
417  },
418  [WalletsGen.acceptDisclaimer]: draftState => {
419    draftState.acceptingDisclaimerDelay = true
420  },
421  [WalletsGen.resetAcceptingDisclaimer]: draftState => {
422    draftState.acceptingDisclaimerDelay = false
423  },
424  [WalletsGen.loadedMobileOnlyMode]: (draftState, action) => {
425    draftState.mobileOnlyMap.set(action.payload.accountID, action.payload.enabled)
426  },
427  [WalletsGen.validateSEP7Link]: draftState => {
428    // Clear out old state just in [
429    draftState.sep7ConfirmError = ''
430    draftState.sep7ConfirmInfo = undefined
431    draftState.sep7ConfirmPath = Constants.emptyBuiltPaymentAdvanced
432    draftState.sep7ConfirmURI = ''
433    draftState.sep7SendError = ''
434  },
435  [WalletsGen.setSEP7SendError]: (draftState, action) => {
436    draftState.sep7SendError = action.payload.error
437  },
438  [WalletsGen.validateSEP7LinkError]: (draftState, action) => {
439    draftState.sep7ConfirmError = action.payload.error
440  },
441  [WalletsGen.setSEP7Tx]: (draftState, action) => {
442    draftState.sep7ConfirmInfo = action.payload.tx
443    draftState.sep7ConfirmFromQR = action.payload.fromQR
444    draftState.sep7ConfirmURI = action.payload.confirmURI
445  },
446  [WalletsGen.setTrustlineExpanded]: (draftState, action) => {
447    if (action.payload.expanded) {
448      draftState.trustline.expandedAssets.add(action.payload.assetID)
449    } else {
450      draftState.trustline.expandedAssets.delete(action.payload.assetID)
451    }
452  },
453  [WalletsGen.setTrustlineAcceptedAssets]: (draftState, action) => {
454    const {accountID, limits} = action.payload
455    const accountAcceptedAssets = draftState.trustline.acceptedAssets.get(accountID)
456    if (!accountAcceptedAssets || !mapEqual(limits, accountAcceptedAssets)) {
457      draftState.trustline.acceptedAssets.set(accountID, limits)
458    }
459    updateAssetMap(draftState.trustline.assetMap, action.payload.assets)
460  },
461  [WalletsGen.setTrustlineAcceptedAssetsByUsername]: (draftState, action) => {
462    const {username, limits, assets} = action.payload
463    const accountAcceptedAssets = draftState.trustline.acceptedAssetsByUsername.get(username)
464    if (!accountAcceptedAssets || !mapEqual(limits, accountAcceptedAssets)) {
465      draftState.trustline.acceptedAssetsByUsername.set(username, limits)
466    }
467    updateAssetMap(draftState.trustline.assetMap, assets)
468  },
469  [WalletsGen.setTrustlinePopularAssets]: (draftState, action) => {
470    draftState.trustline.popularAssets = action.payload.assets.map(asset =>
471      Types.assetDescriptionToAssetID(asset)
472    )
473    updateAssetMap(draftState.trustline.assetMap, action.payload.assets)
474    draftState.trustline.totalAssetsCount = action.payload.totalCount
475    draftState.trustline.loaded = true
476  },
477  [WalletsGen.setTrustlineSearchText]: (draftState, action) => {
478    if (!action.payload.text) {
479      draftState.trustline.searchingAssets = []
480    }
481  },
482  [WalletsGen.setTrustlineSearchResults]: (draftState, action) => {
483    draftState.trustline.searchingAssets = action.payload.assets.map(asset =>
484      Types.assetDescriptionToAssetID(asset)
485    )
486    updateAssetMap(draftState.trustline.assetMap, action.payload.assets)
487  },
488  [WalletsGen.clearTrustlineSearchResults]: draftState => {
489    draftState.trustline.searchingAssets = undefined
490  },
491  [WalletsGen.setBuiltPaymentAdvanced]: (draftState, action) => {
492    if (action.payload.forSEP7) {
493      draftState.sep7ConfirmPath = action.payload.builtPaymentAdvanced
494    } else {
495      draftState.builtPaymentAdvanced = action.payload.builtPaymentAdvanced
496    }
497  },
498  [WalletsGen.staticConfigLoaded]: (draftState, action) => {
499    draftState.staticConfig = action.payload.staticConfig
500  },
501  [WalletsGen.assetDeposit]: draftState => {
502    draftState.sep6Error = false
503    draftState.sep6Message = ''
504  },
505  [WalletsGen.assetWithdraw]: draftState => {
506    draftState.sep6Error = false
507    draftState.sep6Message = ''
508  },
509  [WalletsGen.setSEP6Message]: (draftState, action) => {
510    draftState.sep6Error = action.payload.error
511    draftState.sep6Message = action.payload.message
512  },
513  ...teamBuilderReducerCreator<Types.State>(
514    (draftState: Container.Draft<Types.State>, action: TeamBuildingGen.Actions) => {
515      const val = editTeambuildingDraft('wallets', draftState.teamBuilding, action)
516      if (val !== undefined) {
517        draftState.teamBuilding = val
518      }
519    }
520  ),
521})
522