github.com/keybase/client/go@v0.0.0-20240309051027-028f7c731f8b/stellar/stellar.go (about)

     1  package stellar
     2  
     3  import (
     4  	"context"
     5  	"encoding/hex"
     6  	"errors"
     7  	"fmt"
     8  	"sort"
     9  	"strings"
    10  	"unicode/utf8"
    11  
    12  	"github.com/keybase/client/go/engine"
    13  	"github.com/keybase/client/go/externals"
    14  	"github.com/keybase/client/go/libkb"
    15  	"github.com/keybase/client/go/protocol/chat1"
    16  	"github.com/keybase/client/go/protocol/gregor1"
    17  	"github.com/keybase/client/go/protocol/keybase1"
    18  	"github.com/keybase/client/go/protocol/stellar1"
    19  	"github.com/keybase/client/go/stellar/bundle"
    20  	"github.com/keybase/client/go/stellar/relays"
    21  	"github.com/keybase/client/go/stellar/remote"
    22  	"github.com/keybase/client/go/stellar/stellarcommon"
    23  	"github.com/keybase/stellarnet"
    24  	stellarAddress "github.com/stellar/go/address"
    25  	"github.com/stellar/go/build"
    26  	federationProto "github.com/stellar/go/protocols/federation"
    27  )
    28  
    29  const AccountNameMaxRunes = 24
    30  
    31  // CreateWallet creates and posts an initial stellar bundle for a user.
    32  // Only succeeds if they do not already have one.
    33  // Safe (but wasteful) to call even if the user has a bundle already.
    34  func CreateWallet(mctx libkb.MetaContext) (created bool, err error) {
    35  	defer mctx.Trace("Stellar.CreateWallet", &err)()
    36  	loggedInUsername := mctx.ActiveDevice().Username(mctx)
    37  	if !loggedInUsername.IsValid() {
    38  		return false, fmt.Errorf("could not get logged-in username")
    39  	}
    40  	perUserKeyUpgradeSoft(mctx, "create-wallet")
    41  	clearBundle, err := bundle.NewInitial(fmt.Sprintf("%v's account", loggedInUsername))
    42  	if err != nil {
    43  		return false, err
    44  	}
    45  	meUV, err := mctx.G().GetMeUV(mctx.Ctx())
    46  	if err != nil {
    47  		return false, err
    48  	}
    49  	err = remote.PostWithChainlink(mctx, *clearBundle)
    50  	switch e := err.(type) {
    51  	case nil:
    52  		// ok
    53  	case libkb.AppStatusError:
    54  		if keybase1.StatusCode(e.Code) == keybase1.StatusCode_SCStellarWrongRevision {
    55  			// Assume this happened because a bundle already existed.
    56  			// And suppress the error.
    57  			mctx.Debug("suppressing error: %v", err)
    58  			return false, nil
    59  		}
    60  		return false, err
    61  	default:
    62  		return false, err
    63  	}
    64  	getGlobal(mctx.G()).InformHasWallet(mctx.Ctx(), meUV)
    65  	go getGlobal(mctx.G()).KickAutoClaimRunner(mctx.BackgroundWithLogTags(), gregor1.MsgID{})
    66  	return true, nil
    67  }
    68  
    69  type CreateWalletGatedResult struct {
    70  	JustCreated        bool  // whether the user's wallet was created by this call
    71  	HasWallet          bool  // whether the user now has a wallet
    72  	AcceptedDisclaimer bool  // whether the user has accepted the disclaimer
    73  	ErrorCreating      error // error encountered while attempting to create the wallet
    74  }
    75  
    76  // CreateWalletGated may create a wallet for the user.
    77  // Taking into account settings from the server.
    78  // It should be speedy to call repeatedly _if_ the user gets a wallet.
    79  func CreateWalletGated(mctx libkb.MetaContext) (res CreateWalletGatedResult, err error) {
    80  	defer mctx.Trace("Stellar.CreateWalletGated", &err)()
    81  	defer func() {
    82  		mctx.Debug("CreateWalletGated: (res:%+v, err:%v)", res, err != nil)
    83  	}()
    84  	res, err = createWalletGatedHelper(mctx)
    85  	if err == nil && res.ErrorCreating != nil {
    86  		// An error was encountered while creating the wallet.
    87  		// This could have been the result of losing a race against other threads.
    88  		// When multiple threads create a wallet only one will succeed.
    89  		// In that case we _do_ have a wallet now even though this thread failed,
    90  		// so run again for an accurate reply.
    91  		return createWalletGatedHelper(mctx)
    92  	}
    93  	return res, err
    94  }
    95  
    96  func createWalletGatedHelper(mctx libkb.MetaContext) (res CreateWalletGatedResult, err error) {
    97  	defer mctx.Trace("Stellar.createWalletGatedHelper", &err)()
    98  	defer func() {
    99  		mctx.Debug("createWalletGatedHelper: (res:%+v, err:%v)", res, err != nil)
   100  	}()
   101  	meUV, err := mctx.G().GetMeUV(mctx.Ctx())
   102  	if err != nil {
   103  		return res, err
   104  	}
   105  	if getGlobal(mctx.G()).CachedHasWallet(mctx.Ctx(), meUV) {
   106  		mctx.Debug("createWalletGatedHelper: local cache says we already have a wallet")
   107  		return CreateWalletGatedResult{
   108  			JustCreated:        false,
   109  			HasWallet:          true,
   110  			AcceptedDisclaimer: true, // because it should be impossible to have created a wallet without accepting.
   111  		}, nil
   112  	}
   113  	scr, err := remote.ShouldCreate(mctx.Ctx(), mctx.G())
   114  	if err != nil {
   115  		return res, err
   116  	}
   117  	res.HasWallet = scr.HasWallet
   118  	res.AcceptedDisclaimer = scr.AcceptedDisclaimer
   119  	if scr.HasWallet {
   120  		mctx.Debug("createWalletGatedHelper: server says we already have a wallet")
   121  		getGlobal(mctx.G()).InformHasWallet(mctx.Ctx(), meUV)
   122  		return res, nil
   123  	}
   124  	if !scr.ShouldCreate {
   125  		mctx.Debug("createWalletGatedHelper: server did not recommend wallet creation")
   126  		return res, nil
   127  	}
   128  	justCreated, err := CreateWallet(mctx)
   129  	if err != nil {
   130  		mctx.Debug("createWalletGatedHelper: error creating wallet: %v", err)
   131  		res.ErrorCreating = err
   132  		return res, nil
   133  	}
   134  	res.JustCreated = justCreated
   135  	if justCreated {
   136  		res.HasWallet = true
   137  	}
   138  	return res, nil
   139  }
   140  
   141  // CreateWalletSoft creates a user's initial wallet if they don't already have one.
   142  // Does not get in the way of intentional user actions.
   143  func CreateWalletSoft(mctx libkb.MetaContext) {
   144  	var err error
   145  	defer mctx.Trace("CreateWalletSoft", &err)()
   146  	if !mctx.G().LocalSigchainGuard().IsAvailable(mctx.Ctx(), "CreateWalletSoft") {
   147  		err = fmt.Errorf("yielding to guard")
   148  		return
   149  	}
   150  	_, err = CreateWalletGated(mctx)
   151  }
   152  
   153  func pushSimpleUpdateForAccount(mctx libkb.MetaContext, accountID stellar1.AccountID) (err error) {
   154  	defer mctx.Trace("Stellar.Upkeep pushSimpleUpdateForAccount", &err)()
   155  	prevBundle, err := remote.FetchAccountBundle(mctx, accountID)
   156  	if err != nil {
   157  		return err
   158  	}
   159  	nextBundle := bundle.AdvanceAccounts(*prevBundle, []stellar1.AccountID{accountID})
   160  	return remote.Post(mctx, nextBundle)
   161  }
   162  
   163  // Upkeep makes sure the bundle is encrypted for the user's latest PUK.
   164  func Upkeep(mctx libkb.MetaContext) (err error) {
   165  	defer mctx.Trace("Stellar.Upkeep", &err)()
   166  	_, _, prevAccountPukGens, err := remote.FetchBundleWithGens(mctx)
   167  	if err != nil {
   168  		return err
   169  	}
   170  	pukring, err := mctx.G().GetPerUserKeyring(mctx.Ctx())
   171  	if err != nil {
   172  		return err
   173  	}
   174  	err = pukring.Sync(mctx)
   175  	if err != nil {
   176  		return err
   177  	}
   178  	currentPukGen := pukring.CurrentGeneration()
   179  	var madeAnyChanges bool
   180  	for accountID, accountPukGen := range prevAccountPukGens {
   181  		if accountPukGen < currentPukGen {
   182  			madeAnyChanges = true
   183  			mctx.Debug("Stellar.Upkeep: reencrypting %s... for gen %v from gen %v", accountID[:5], currentPukGen, accountPukGen)
   184  			if err = pushSimpleUpdateForAccount(mctx, accountID); err != nil {
   185  				mctx.Debug("Stellar.Upkeep: error reencrypting %v: %v", accountID[:5], err)
   186  				return err
   187  			}
   188  		}
   189  	}
   190  	if !madeAnyChanges {
   191  		mctx.Debug("Stellar.Upkeep: no need to reencrypt. Everything is at gen %v", currentPukGen)
   192  	}
   193  	return nil
   194  }
   195  
   196  func ImportSecretKey(mctx libkb.MetaContext, secretKey stellar1.SecretKey, makePrimary bool, accountName string) (err error) {
   197  	prevBundle, err := remote.FetchSecretlessBundle(mctx)
   198  	if err != nil {
   199  		return err
   200  	}
   201  	nextBundle := bundle.AdvanceBundle(*prevBundle)
   202  	err = bundle.AddAccount(&nextBundle, secretKey, accountName, makePrimary)
   203  	if err != nil {
   204  		return err
   205  	}
   206  
   207  	if makePrimary {
   208  		// primary account changes need sigchain link
   209  		// (so other users can find user's primary account id)
   210  		err = remote.PostWithChainlink(mctx, nextBundle)
   211  	} else {
   212  		err = remote.Post(mctx, nextBundle)
   213  	}
   214  	if err != nil {
   215  		return err
   216  	}
   217  
   218  	// inform the global stellar object that there is a new bundle.
   219  	mctx.G().GetStellar().InformBundle(mctx, nextBundle.Revision, nextBundle.Accounts)
   220  
   221  	// after import, mark all the transactions in this account as "read"
   222  	// any errors in this process are not fatal, since the important task
   223  	// has been accomplished.
   224  	_, accountID, _, err := libkb.ParseStellarSecretKey(string(secretKey))
   225  	if err != nil {
   226  		mctx.Debug("ImportSecretKey, failed to parse secret key after import: %s", err)
   227  		return nil
   228  	}
   229  	arg := remote.RecentPaymentsArg{
   230  		AccountID:       accountID,
   231  		SkipPending:     true,
   232  		IncludeAdvanced: true,
   233  	}
   234  	page, err := remote.RecentPayments(mctx.Ctx(), mctx.G(), arg)
   235  	if err != nil {
   236  		mctx.Debug("ImportSecretKey, RecentPayments error: %s", err)
   237  		return nil
   238  	}
   239  	if len(page.Payments) == 0 {
   240  		return nil
   241  	}
   242  	mostRecentID, err := page.Payments[0].TransactionID()
   243  	if err != nil {
   244  		mctx.Debug("ImportSecretKey, tx id from most recent payment error: %s", err)
   245  		return nil
   246  	}
   247  	if err = remote.MarkAsRead(mctx.Ctx(), mctx.G(), accountID, mostRecentID); err != nil {
   248  		mctx.Debug("ImportSecretKey, markAsRead error: %s", err)
   249  		return nil
   250  	}
   251  
   252  	return nil
   253  }
   254  
   255  func ExportSecretKey(mctx libkb.MetaContext, accountID stellar1.AccountID) (res stellar1.SecretKey, err error) {
   256  	prevBundle, err := remote.FetchAccountBundle(mctx, accountID)
   257  	if err != nil {
   258  		return res, err
   259  	}
   260  	for _, account := range prevBundle.Accounts {
   261  		if account.AccountID.Eq(accountID) {
   262  			signers := prevBundle.AccountBundles[account.AccountID].Signers
   263  			if len(signers) == 0 {
   264  				return res, fmt.Errorf("no secret keys found for account")
   265  			}
   266  			if len(signers) != 1 {
   267  				return res, fmt.Errorf("expected 1 secret key but found %v", len(signers))
   268  			}
   269  			return signers[0], nil
   270  		}
   271  	}
   272  	_, _, _, parseSecErr := libkb.ParseStellarSecretKey(accountID.String())
   273  	if parseSecErr == nil {
   274  		// Just in case a secret key worked its way in here
   275  		return res, fmt.Errorf("account not found: unexpected secret key")
   276  	}
   277  	return res, fmt.Errorf("account not found: %v", accountID)
   278  }
   279  
   280  func OwnAccount(mctx libkb.MetaContext, accountID stellar1.AccountID) (own, isPrimary bool, err error) {
   281  	own, isPrimary, _, err = OwnAccountPlusName(mctx, accountID)
   282  	return own, isPrimary, err
   283  }
   284  
   285  func OwnAccountPlusName(mctx libkb.MetaContext, accountID stellar1.AccountID) (own, isPrimary bool, accountName string, err error) {
   286  	bundle, err := remote.FetchSecretlessBundle(mctx)
   287  	if err != nil {
   288  		return false, false, "", err
   289  	}
   290  	for _, account := range bundle.Accounts {
   291  		if account.AccountID.Eq(accountID) {
   292  			return true, account.IsPrimary, account.Name, nil
   293  		}
   294  	}
   295  	return false, false, "", nil
   296  }
   297  
   298  func OwnAccountCached(mctx libkb.MetaContext, accountID stellar1.AccountID) (own, isPrimary bool, err error) {
   299  	return getGlobal(mctx.G()).OwnAccountCached(mctx, accountID)
   300  }
   301  
   302  func OwnAccountPlusNameCached(mctx libkb.MetaContext, accountID stellar1.AccountID) (own, isPrimary bool, accountName string, err error) {
   303  	return getGlobal(mctx.G()).OwnAccountPlusNameCached(mctx, accountID)
   304  }
   305  
   306  func lookupSenderEntry(mctx libkb.MetaContext, accountID stellar1.AccountID) (stellar1.BundleEntry, stellar1.AccountBundle, error) {
   307  	if accountID == "" {
   308  		bundle, err := remote.FetchSecretlessBundle(mctx)
   309  		if err != nil {
   310  			return stellar1.BundleEntry{}, stellar1.AccountBundle{}, err
   311  		}
   312  		entry, err := bundle.PrimaryAccount()
   313  		if err != nil {
   314  			return stellar1.BundleEntry{}, stellar1.AccountBundle{}, err
   315  		}
   316  		accountID = entry.AccountID
   317  	}
   318  
   319  	bundle, err := remote.FetchAccountBundle(mctx, accountID)
   320  	switch err := err.(type) {
   321  	case nil:
   322  		// ok
   323  	case libkb.AppStatusError:
   324  		if libkb.IsAppStatusCode(err, keybase1.StatusCode_SCStellarMissingAccount) {
   325  			mctx.Debug("suppressing error: %v", err)
   326  			err = err.WithDesc("Sender account not found")
   327  		}
   328  		return stellar1.BundleEntry{}, stellar1.AccountBundle{}, err
   329  	default:
   330  		return stellar1.BundleEntry{}, stellar1.AccountBundle{}, err
   331  	}
   332  
   333  	for _, entry := range bundle.Accounts {
   334  		if entry.AccountID.Eq(accountID) {
   335  			return entry, bundle.AccountBundles[entry.AccountID], nil
   336  		}
   337  	}
   338  
   339  	return stellar1.BundleEntry{}, stellar1.AccountBundle{}, libkb.NotFoundError{Msg: "Sender account not found"}
   340  }
   341  
   342  func LookupSenderPrimary(mctx libkb.MetaContext) (stellar1.BundleEntry, stellar1.AccountBundle, error) {
   343  	return LookupSender(mctx, "" /* empty account id returns primary */)
   344  }
   345  
   346  func LookupSender(mctx libkb.MetaContext, accountID stellar1.AccountID) (stellar1.BundleEntry, stellar1.AccountBundle, error) {
   347  	entry, ab, err := lookupSenderEntry(mctx, accountID)
   348  	if err != nil {
   349  		return stellar1.BundleEntry{}, stellar1.AccountBundle{}, err
   350  	}
   351  	if len(ab.Signers) == 0 {
   352  		return stellar1.BundleEntry{}, stellar1.AccountBundle{}, errors.New("no signer for bundle")
   353  	}
   354  	if len(ab.Signers) > 1 {
   355  		return stellar1.BundleEntry{}, stellar1.AccountBundle{}, errors.New("only single signer supported")
   356  	}
   357  
   358  	return entry, ab, nil
   359  }
   360  
   361  // Wrapper around LookupByAddress that acts likes Context is plumbed through.
   362  // After context is canceled, any return from LookupByAddress is ignored.
   363  func federationLookupByAddressCtx(mctx libkb.MetaContext, addy string) (*federationProto.NameResponse, error) {
   364  	fedCli := getGlobal(mctx.G()).federationClient
   365  	type packT struct {
   366  		res *federationProto.NameResponse
   367  		err error
   368  	}
   369  	ch := make(chan packT, 1)
   370  	go func() {
   371  		var pack packT
   372  		pack.res, pack.err = fedCli.LookupByAddress(addy)
   373  		ch <- pack
   374  	}()
   375  	select {
   376  	case <-mctx.Ctx().Done():
   377  		return nil, mctx.Ctx().Err()
   378  	case pack := <-ch:
   379  		return pack.res, pack.err
   380  	}
   381  }
   382  
   383  // LookupRecipient finds a recipient.
   384  // `to` can be a username, social assertion, account ID, or federation address.
   385  func LookupRecipient(m libkb.MetaContext, to stellarcommon.RecipientInput, isCLI bool) (res stellarcommon.Recipient, err error) {
   386  	defer m.Trace("Stellar.LookupRecipient", &err)()
   387  
   388  	res = stellarcommon.Recipient{
   389  		Input: to,
   390  	}
   391  	if len(to) == 0 {
   392  		return res, fmt.Errorf("empty recipient parameter")
   393  	}
   394  
   395  	storeAddress := func(address string) error {
   396  		_, err := libkb.ParseStellarAccountID(address)
   397  		if err != nil {
   398  			if verr, ok := err.(libkb.VerboseError); ok {
   399  				m.Debug(verr.Verbose())
   400  			}
   401  			return err
   402  		}
   403  		accountID, err := stellarnet.NewAddressStr(address)
   404  		if err != nil {
   405  			return err
   406  		}
   407  		res.AccountID = &accountID
   408  		return nil
   409  	}
   410  
   411  	// Federation address
   412  	if strings.Contains(string(to), stellarAddress.Separator) {
   413  		name, domain, err := stellarAddress.Split(string(to))
   414  		if err != nil {
   415  			return res, err
   416  		}
   417  
   418  		if domain == "keybase.io" {
   419  			// Keybase.io federation address. Fall through to identify
   420  			// path.
   421  			m.Debug("Got federation address %q but it's under keybase.io domain!", to)
   422  			m.Debug("Instead going to lookup Keybase assertion: %q", name)
   423  			to = stellarcommon.RecipientInput(name)
   424  		} else {
   425  			// Actual federation address that is not under keybase.io
   426  			// domain. Use federation client.
   427  			nameResponse, err := federationLookupByAddressCtx(m, string(to))
   428  			if err != nil {
   429  				errStr := err.Error()
   430  				m.Debug("federation.LookupByAddress returned error: %s", errStr)
   431  				if strings.Contains(errStr, "lookup federation server failed") {
   432  					return res, fmt.Errorf("Server at url %q does not respond to federation requests", domain)
   433  				} else if strings.Contains(errStr, "get federation failed") {
   434  					return res, fmt.Errorf("Federation server %q did not find record %q", domain, name)
   435  				}
   436  				return res, err
   437  			}
   438  			// We got an address! Fall through to the "Stellar
   439  			// address" path.
   440  			m.Debug("federation.LookupByAddress returned: %+v", nameResponse)
   441  			to = stellarcommon.RecipientInput(nameResponse.AccountID)
   442  
   443  			// if there is a memo, include it in the result
   444  			if nameResponse.Memo.Value != "" {
   445  				res.PublicMemo = &nameResponse.Memo.Value
   446  				if nameResponse.MemoType == "" {
   447  					return res, fmt.Errorf("Federation server %q returned invalid memo", domain)
   448  				}
   449  				res.PublicMemoType = &nameResponse.MemoType
   450  			}
   451  		}
   452  	}
   453  
   454  	// Stellar account ID
   455  	if to[0] == 'G' && len(to) > 16 {
   456  		err := storeAddress(string(to))
   457  		return res, err
   458  	}
   459  
   460  	maybeUsername, err := lookupRecipientAssertion(m, string(to), isCLI)
   461  	if err != nil {
   462  		return res, err
   463  	}
   464  	if maybeUsername == "" {
   465  		expr, err := externals.AssertionParse(m, string(to))
   466  		if err != nil {
   467  			m.Debug("error parsing assertion: %s", err)
   468  			return res, fmt.Errorf("invalid recipient %q: %s", to, err)
   469  		}
   470  
   471  		// valid assertion, but not a user yet
   472  		m.Debug("assertion %s (%s) is valid, but not a user yet", to, expr)
   473  		social, err := expr.ToSocialAssertion()
   474  		if err != nil {
   475  			m.Debug("not a social assertion: %s (%s)", to, expr)
   476  			if _, ok := expr.(libkb.AssertionKeybase); ok {
   477  				return res, libkb.NotFoundError{Msg: fmt.Sprintf("user not found: %q", to)}
   478  			}
   479  			return res, fmt.Errorf("invalid recipient %q: %s", to, err)
   480  		}
   481  		res.Assertion = &social
   482  		return res, nil
   483  	}
   484  
   485  	// load the user to get their wallet
   486  	user, err := libkb.LoadUser(
   487  		libkb.NewLoadUserByNameArg(m.G(), maybeUsername).
   488  			WithNetContext(m.Ctx()).
   489  			WithPublicKeyOptional())
   490  	if err != nil {
   491  		return res, err
   492  	}
   493  	res.User = &stellarcommon.User{
   494  		UV:       user.ToUserVersion(),
   495  		Username: user.GetNormalizedName(),
   496  	}
   497  	accountID := user.StellarAccountID()
   498  	if accountID == nil {
   499  		return res, nil
   500  	}
   501  	err = storeAddress(accountID.String())
   502  	return res, err
   503  }
   504  
   505  type DisplayBalance struct {
   506  	Amount   string
   507  	Currency string
   508  }
   509  
   510  func getTimeboundsForSending(m libkb.MetaContext, walletState *WalletState) (*build.Timebounds, error) {
   511  	// Timeout added as Timebounds.MaxTime to Stellar transactions that client
   512  	// creates, effectively adding a "deadline" to the transaction. We can
   513  	// safely assume that a transaction will never end up in a ledger if it's
   514  	// not included before the deadline.
   515  
   516  	// We ask server for timebounds because local clock might not be accurate,
   517  	// and typically we will be setting timeout as 30 seconds.
   518  	start := m.G().Clock().Now()
   519  	serverTimes, err := walletState.ServerTimeboundsRecommendation(m.Ctx())
   520  	if err != nil {
   521  		return nil, err
   522  	}
   523  	took := m.G().Clock().Since(start)
   524  	m.Debug("Server timebounds recommendation is: %+v. Request took %fs", serverTimes, took.Seconds())
   525  	if serverTimes.TimeNow == 0 {
   526  		return nil, fmt.Errorf("Invalid server response for transaction timebounds")
   527  	}
   528  	if serverTimes.Timeout == 0 {
   529  		m.Debug("Returning nil timebounds")
   530  		return nil, nil
   531  	}
   532  
   533  	// Offset server time by our latency to the server. We are making two
   534  	// requests to submit a transaction: one here to get the server time, and
   535  	// another one to send the signed transaction. Assuming server roundtrip
   536  	// time will be the same for both requests, we can offset timebounds here
   537  	// by entire roundtrip time and then we will have MaxTime set as 30 seconds
   538  	// counting from when the server gets our signed tx.
   539  	deadline := serverTimes.TimeNow.Time().Add(took).Unix() + serverTimes.Timeout
   540  	tb := build.Timebounds{
   541  		MaxTime: uint64(deadline),
   542  	}
   543  	m.Debug("Returning timebounds for tx: %+v", tb)
   544  	return &tb, nil
   545  }
   546  
   547  type SendPaymentArg struct {
   548  	From           stellar1.AccountID // Optional. Defaults to primary account.
   549  	To             stellarcommon.RecipientInput
   550  	Amount         string // Amount of XLM to send.
   551  	DisplayBalance DisplayBalance
   552  	SecretNote     string           // Optional.
   553  	PublicMemo     *stellarnet.Memo // Optional.
   554  	ForceRelay     bool
   555  	QuickReturn    bool
   556  }
   557  
   558  type SendPaymentResult struct {
   559  	KbTxID stellar1.KeybaseTransactionID
   560  	// Direct: tx ID of the payment tx
   561  	// Relay : tx ID of the funding payment tx
   562  	TxID    stellar1.TransactionID
   563  	Pending bool
   564  	// Implicit team that the relay secret is encrypted for.
   565  	// Present if this was a relay transfer.
   566  	RelayTeamID *keybase1.TeamID
   567  	JumpToChat  string
   568  }
   569  
   570  // SendPaymentCLI sends XLM from CLI.
   571  func SendPaymentCLI(m libkb.MetaContext, walletState *WalletState, sendArg SendPaymentArg) (res SendPaymentResult, err error) {
   572  	return sendPayment(m, walletState, sendArg, true)
   573  }
   574  
   575  // SendPaymentGUI sends XLM from GUI.
   576  func SendPaymentGUI(m libkb.MetaContext, walletState *WalletState, sendArg SendPaymentArg) (res SendPaymentResult, err error) {
   577  	return sendPayment(m, walletState, sendArg, false)
   578  }
   579  
   580  // sendPayment sends XLM.
   581  // Recipient:
   582  // Stellar address        : Standard payment
   583  // User with wallet ready : Standard payment
   584  // User without a wallet  : Relay payment
   585  // Unresolved assertion   : Relay payment
   586  func sendPayment(mctx libkb.MetaContext, walletState *WalletState, sendArg SendPaymentArg, isCLI bool) (res SendPaymentResult, err error) {
   587  	defer mctx.Trace("Stellar.SendPayment", &err)()
   588  
   589  	// look up sender account
   590  	senderEntry, senderAccountBundle, err := LookupSender(mctx, sendArg.From)
   591  	if err != nil {
   592  		return res, err
   593  	}
   594  	senderSeed := senderAccountBundle.Signers[0]
   595  	senderAccountID := senderEntry.AccountID
   596  
   597  	// look up recipient
   598  	recipient, err := LookupRecipient(mctx, sendArg.To, isCLI)
   599  	if err != nil {
   600  		return res, err
   601  	}
   602  
   603  	mctx.Debug("using stellar network passphrase: %q", stellarnet.Network().Passphrase)
   604  
   605  	baseFee := walletState.BaseFee(mctx)
   606  
   607  	if recipient.AccountID == nil || sendArg.ForceRelay {
   608  		return sendRelayPayment(mctx, walletState,
   609  			senderSeed, recipient, sendArg.Amount, sendArg.DisplayBalance,
   610  			sendArg.SecretNote, sendArg.PublicMemo, sendArg.QuickReturn, senderEntry.IsPrimary, baseFee)
   611  	}
   612  
   613  	ownRecipient, _, err := OwnAccount(mctx, stellar1.AccountID(recipient.AccountID.String()))
   614  	if err != nil {
   615  		mctx.Debug("error determining if user own's recipient: %v", err)
   616  		return res, err
   617  	}
   618  	if ownRecipient {
   619  		// When sending to an account that we own, act as though sending to a user as opposed to just an account ID.
   620  		uv, un := mctx.G().ActiveDevice.GetUsernameAndUserVersionIfValid(mctx)
   621  		if uv.IsNil() || un.IsNil() {
   622  			mctx.Debug("error finding self: uv:%v un:%v", uv, un)
   623  			return res, fmt.Errorf("error getting logged-in user")
   624  		}
   625  		recipient.User = &stellarcommon.User{
   626  			UV:       uv,
   627  			Username: un,
   628  		}
   629  	}
   630  
   631  	senderSeed2, err := stellarnet.NewSeedStr(senderSeed.SecureNoLogString())
   632  	if err != nil {
   633  		return res, err
   634  	}
   635  
   636  	post := stellar1.PaymentDirectPost{
   637  		FromDeviceID:    mctx.G().ActiveDevice.DeviceID(),
   638  		DisplayAmount:   sendArg.DisplayBalance.Amount,
   639  		DisplayCurrency: sendArg.DisplayBalance.Currency,
   640  		QuickReturn:     sendArg.QuickReturn,
   641  	}
   642  	if recipient.User != nil {
   643  		post.To = &recipient.User.UV
   644  	}
   645  
   646  	// check if recipient account exists
   647  	funded, err := isAccountFunded(mctx.Ctx(), walletState, stellar1.AccountID(recipient.AccountID.String()))
   648  	if err != nil {
   649  		return res, fmt.Errorf("error checking destination account balance: %v", err)
   650  	}
   651  	if !funded && isAmountLessThanMin(sendArg.Amount, minAmountCreateAccountXLM) {
   652  		return res, fmt.Errorf("you must send at least %s XLM to fund the account for %s", minAmountCreateAccountXLM, sendArg.To)
   653  	}
   654  
   655  	sp, unlock := NewSeqnoProvider(mctx, walletState)
   656  	defer unlock()
   657  
   658  	tb, err := getTimeboundsForSending(mctx, walletState)
   659  	if err != nil {
   660  		return res, err
   661  	}
   662  
   663  	if recipient.HasMemo() {
   664  		if sendArg.PublicMemo != nil {
   665  			return res, fmt.Errorf("federation recipient included its own memo, but send called with a memo")
   666  		}
   667  		sendArg.PublicMemo, err = recipient.Memo()
   668  		if err != nil {
   669  			return res, err
   670  		}
   671  	}
   672  
   673  	var txID string
   674  	var seqno uint64
   675  	if !funded {
   676  		// if no balance, create_account operation
   677  		sig, err := stellarnet.CreateAccountXLMTransactionWithMemo(senderSeed2, *recipient.AccountID, sendArg.Amount, sendArg.PublicMemo, sp, tb, baseFee)
   678  		if err != nil {
   679  			return res, err
   680  		}
   681  		post.SignedTransaction = sig.Signed
   682  		txID = sig.TxHash
   683  		seqno = sig.Seqno
   684  	} else {
   685  		// if balance, payment operation
   686  		sig, err := stellarnet.PaymentXLMTransactionWithMemo(senderSeed2, *recipient.AccountID, sendArg.Amount, sendArg.PublicMemo, sp, tb, baseFee)
   687  		if err != nil {
   688  			return res, err
   689  		}
   690  		post.SignedTransaction = sig.Signed
   691  		txID = sig.TxHash
   692  		seqno = sig.Seqno
   693  	}
   694  
   695  	if err := walletState.AddPendingTx(mctx.Ctx(), senderAccountID, stellar1.TransactionID(txID), seqno); err != nil {
   696  		mctx.Debug("error calling AddPendingTx: %s", err)
   697  	}
   698  
   699  	if len(sendArg.SecretNote) > 0 {
   700  		noteClear := stellar1.NoteContents{
   701  			Note:      sendArg.SecretNote,
   702  			StellarID: stellar1.TransactionID(txID),
   703  		}
   704  		var recipientUv *keybase1.UserVersion
   705  		if recipient.User != nil {
   706  			recipientUv = &recipient.User.UV
   707  		}
   708  		post.NoteB64, err = NoteEncryptB64(mctx, noteClear, recipientUv)
   709  		if err != nil {
   710  			return res, fmt.Errorf("error encrypting note: %v", err)
   711  		}
   712  	}
   713  
   714  	// submit the transaction
   715  	rres, err := walletState.SubmitPayment(mctx.Ctx(), post)
   716  	if err != nil {
   717  		mctx.Debug("SEQNO SubmitPayment error seqno: %d txID: %s, err: %s", seqno, rres.StellarID, err)
   718  		if rerr := walletState.RemovePendingTx(mctx.Ctx(), senderAccountID, stellar1.TransactionID(txID)); rerr != nil {
   719  			mctx.Debug("error calling RemovePendingTx: %s", rerr)
   720  		}
   721  		return res, err
   722  	}
   723  	mctx.Debug("sent payment (direct) kbTxID:%v txID:%v pending:%v", seqno, rres.KeybaseID, rres.StellarID, rres.Pending)
   724  	mctx.Debug("SEQNO SubmitPayment success seqno: %d txID: %s", seqno, rres.StellarID)
   725  	if !rres.Pending {
   726  		mctx.Debug("SubmitPayment result wasn't pending, removing from wallet state: %s/%s", senderAccountID, txID)
   727  		err = walletState.RemovePendingTx(mctx.Ctx(), senderAccountID, stellar1.TransactionID(txID))
   728  		if err != nil {
   729  			mctx.Debug("SubmitPayment ws.RemovePendingTx error: %s", err)
   730  		}
   731  	}
   732  
   733  	err = walletState.Refresh(mctx, senderEntry.AccountID, "SubmitPayment")
   734  	if err != nil {
   735  		mctx.Debug("SubmitPayment ws.Refresh error: %s", err)
   736  	}
   737  
   738  	var chatRecipient string
   739  	if senderEntry.IsPrimary {
   740  		chatRecipient = chatRecipientStr(mctx, recipient)
   741  		sendChat := func(mctx libkb.MetaContext) {
   742  			chatSendPaymentMessageSoft(mctx, chatRecipient, rres.StellarID, "SendPayment")
   743  		}
   744  		if sendArg.QuickReturn {
   745  			go sendChat(mctx.WithCtx(context.Background()))
   746  		} else {
   747  			sendChat(mctx)
   748  		}
   749  	} else {
   750  		mctx.Debug("not sending chat message: sending from non-primary account")
   751  	}
   752  
   753  	return SendPaymentResult{
   754  		KbTxID:     rres.KeybaseID,
   755  		TxID:       rres.StellarID,
   756  		Pending:    rres.Pending,
   757  		JumpToChat: chatRecipient,
   758  	}, nil
   759  }
   760  
   761  type SendPathPaymentArg struct {
   762  	From        stellar1.AccountID
   763  	To          stellarcommon.RecipientInput
   764  	Path        stellar1.PaymentPath
   765  	SecretNote  string
   766  	PublicMemo  *stellarnet.Memo
   767  	QuickReturn bool
   768  }
   769  
   770  // SendPathPaymentCLI sends a path payment from CLI.
   771  func SendPathPaymentCLI(mctx libkb.MetaContext, walletState *WalletState, sendArg SendPathPaymentArg) (res SendPaymentResult, err error) {
   772  	return sendPathPayment(mctx, walletState, sendArg)
   773  }
   774  
   775  // SendPathPaymentGUI sends a path payment from GUI.
   776  func SendPathPaymentGUI(mctx libkb.MetaContext, walletState *WalletState, sendArg SendPathPaymentArg) (res SendPaymentResult, err error) {
   777  	return sendPathPayment(mctx, walletState, sendArg)
   778  }
   779  
   780  // PathPaymentTx reutrns a signed path payment tx.
   781  func PathPaymentTx(mctx libkb.MetaContext, walletState *WalletState, sendArg SendPathPaymentArg) (*stellarnet.SignResult, *stellar1.BundleEntry, *stellarcommon.Recipient, error) {
   782  	senderEntry, senderAccountBundle, err := LookupSender(mctx, sendArg.From)
   783  	if err != nil {
   784  		return nil, nil, nil, err
   785  	}
   786  	senderSeed, err := stellarnet.NewSeedStr(senderAccountBundle.Signers[0].SecureNoLogString())
   787  	if err != nil {
   788  		return nil, nil, nil, err
   789  	}
   790  
   791  	recipient, err := LookupRecipient(mctx, sendArg.To, false)
   792  	if err != nil {
   793  		return nil, nil, nil, err
   794  	}
   795  	if recipient.AccountID == nil {
   796  		return nil, nil, nil, errors.New("cannot send a path payment to a user without a stellar account")
   797  	}
   798  
   799  	if recipient.HasMemo() {
   800  		if sendArg.PublicMemo != nil {
   801  			return nil, nil, nil, fmt.Errorf("federation recipient included its own memo, but send called with a memo")
   802  		}
   803  		sendArg.PublicMemo, err = recipient.Memo()
   804  		if err != nil {
   805  			return nil, nil, nil, err
   806  		}
   807  	}
   808  
   809  	baseFee := walletState.BaseFee(mctx)
   810  
   811  	to, err := stellarnet.NewAddressStr(recipient.AccountID.String())
   812  	if err != nil {
   813  		return nil, nil, nil, err
   814  	}
   815  
   816  	sp, unlock := NewSeqnoProvider(mctx, walletState)
   817  	defer unlock()
   818  
   819  	sig, err := stellarnet.PathPaymentTransactionWithMemo(senderSeed, to, sendArg.Path.SourceAsset, sendArg.Path.SourceAmountMax, sendArg.Path.DestinationAsset, sendArg.Path.DestinationAmount, AssetSliceToAssetBase(sendArg.Path.Path), sendArg.PublicMemo, sp, nil, baseFee)
   820  	if err != nil {
   821  		return nil, nil, nil, err
   822  	}
   823  
   824  	return &sig, &senderEntry, &recipient, nil
   825  }
   826  
   827  func sendPathPayment(mctx libkb.MetaContext, walletState *WalletState, sendArg SendPathPaymentArg) (res SendPaymentResult, err error) {
   828  	sig, senderEntry, recipient, err := PathPaymentTx(mctx, walletState, sendArg)
   829  	if err != nil {
   830  		return res, err
   831  	}
   832  	senderAccountID := senderEntry.AccountID
   833  
   834  	post := stellar1.PathPaymentPost{
   835  		FromDeviceID:      mctx.G().ActiveDevice.DeviceID(),
   836  		QuickReturn:       sendArg.QuickReturn,
   837  		SignedTransaction: sig.Signed,
   838  	}
   839  
   840  	if recipient.User != nil {
   841  		post.To = &recipient.User.UV
   842  	}
   843  
   844  	if err := walletState.AddPendingTx(mctx.Ctx(), senderEntry.AccountID, stellar1.TransactionID(sig.TxHash), sig.Seqno); err != nil {
   845  		mctx.Debug("error calling AddPendingTx: %s", err)
   846  	}
   847  
   848  	if len(sendArg.SecretNote) > 0 {
   849  		noteClear := stellar1.NoteContents{
   850  			Note:      sendArg.SecretNote,
   851  			StellarID: stellar1.TransactionID(sig.TxHash),
   852  		}
   853  		var recipientUv *keybase1.UserVersion
   854  		if recipient.User != nil {
   855  			recipientUv = &recipient.User.UV
   856  		}
   857  		post.NoteB64, err = NoteEncryptB64(mctx, noteClear, recipientUv)
   858  		if err != nil {
   859  			return res, fmt.Errorf("error encrypting note: %v", err)
   860  		}
   861  	}
   862  
   863  	rres, err := walletState.SubmitPathPayment(mctx, post)
   864  	if err != nil {
   865  		mctx.Debug("SEQNO SubmitPathPayment error seqno: %d txID: %s, err: %s", sig.Seqno, rres.StellarID, err)
   866  		if rerr := walletState.RemovePendingTx(mctx.Ctx(), senderEntry.AccountID, stellar1.TransactionID(sig.TxHash)); rerr != nil {
   867  			mctx.Debug("error calling RemovePendingTx: %s", rerr)
   868  		}
   869  		return res, err
   870  	}
   871  	mctx.Debug("sent path payment (direct) kbTxID:%v txID:%v pending:%v", sig.Seqno, rres.KeybaseID, rres.StellarID, rres.Pending)
   872  	mctx.Debug("SEQNO SubmitPathPayment success seqno: %d txID: %s", sig.Seqno, rres.StellarID)
   873  	if !rres.Pending {
   874  		mctx.Debug("SubmitPathPayment result wasn't pending, removing from wallet state: %s/%s", senderAccountID, sig.TxHash)
   875  		err = walletState.RemovePendingTx(mctx.Ctx(), senderEntry.AccountID, stellar1.TransactionID(sig.TxHash))
   876  		if err != nil {
   877  			mctx.Debug("SubmitPathPayment ws.RemovePendingTx error: %s", err)
   878  		}
   879  	}
   880  
   881  	err = walletState.Refresh(mctx, senderEntry.AccountID, "SubmitPathPayment")
   882  	if err != nil {
   883  		mctx.Debug("SubmitPathPayment ws.Refresh error: %s", err)
   884  	}
   885  
   886  	var chatRecipient string
   887  	if senderEntry.IsPrimary {
   888  		chatRecipient = chatRecipientStr(mctx, *recipient)
   889  		sendChat := func(mctx libkb.MetaContext) {
   890  			chatSendPaymentMessageSoft(mctx, chatRecipient, rres.StellarID, "SendPathPayment")
   891  		}
   892  		if sendArg.QuickReturn {
   893  			go sendChat(mctx.WithCtx(context.Background()))
   894  		} else {
   895  			sendChat(mctx)
   896  		}
   897  	} else {
   898  		mctx.Debug("not sending chat message: sending from non-primary account")
   899  	}
   900  
   901  	return SendPaymentResult{
   902  		KbTxID:     rres.KeybaseID,
   903  		TxID:       rres.StellarID,
   904  		Pending:    rres.Pending,
   905  		JumpToChat: chatRecipient,
   906  	}, nil
   907  }
   908  
   909  type indexedSpec struct {
   910  	spec             libkb.MiniChatPaymentSpec
   911  	index            int
   912  	xlmAmountNumeric int64
   913  }
   914  
   915  // SpecMiniChatPayments returns a summary of the payment amounts for each recipient
   916  // and a total.
   917  func SpecMiniChatPayments(mctx libkb.MetaContext, walletState *WalletState, payments []libkb.MiniChatPayment) (*libkb.MiniChatPaymentSummary, error) {
   918  	// look up sender account
   919  	_, senderAccountBundle, err := LookupSenderPrimary(mctx)
   920  	if err != nil {
   921  		return nil, err
   922  	}
   923  	senderAccountID := senderAccountBundle.AccountID
   924  	senderCurrency, err := GetCurrencySetting(mctx, senderAccountID)
   925  	if err != nil {
   926  		return nil, err
   927  	}
   928  
   929  	senderRate, err := walletState.ExchangeRate(mctx.Ctx(), string(senderCurrency.Code))
   930  	if err != nil {
   931  		return nil, err
   932  	}
   933  
   934  	var summary libkb.MiniChatPaymentSummary
   935  
   936  	var xlmTotal int64
   937  	if len(payments) > 0 {
   938  		ch := make(chan indexedSpec)
   939  		for i, payment := range payments {
   940  			go func(payment libkb.MiniChatPayment, index int) {
   941  				spec, xlmAmountNumeric := specMiniChatPayment(mctx, walletState, payment)
   942  				ch <- indexedSpec{spec: spec, index: index, xlmAmountNumeric: xlmAmountNumeric}
   943  			}(payment, i)
   944  		}
   945  
   946  		summary.Specs = make([]libkb.MiniChatPaymentSpec, len(payments))
   947  		for i := 0; i < len(payments); i++ {
   948  			ispec := <-ch
   949  			summary.Specs[ispec.index] = ispec.spec
   950  			xlmTotal += ispec.xlmAmountNumeric
   951  		}
   952  	}
   953  
   954  	summary.XLMTotal = stellarnet.StringFromStellarAmount(xlmTotal)
   955  	if senderRate.Currency != "" && senderRate.Currency != "XLM" {
   956  		outsideAmount, err := stellarnet.ConvertXLMToOutside(summary.XLMTotal, senderRate.Rate)
   957  		if err != nil {
   958  			return nil, err
   959  		}
   960  		summary.DisplayTotal, err = FormatCurrencyWithCodeSuffix(mctx, outsideAmount, senderRate.Currency, stellarnet.Round)
   961  		if err != nil {
   962  			return nil, err
   963  		}
   964  	}
   965  
   966  	summary.XLMTotal, err = FormatAmountDescriptionXLM(mctx, summary.XLMTotal)
   967  	if err != nil {
   968  		return nil, err
   969  	}
   970  
   971  	return &summary, nil
   972  }
   973  
   974  func specMiniChatPayment(mctx libkb.MetaContext, walletState *WalletState, payment libkb.MiniChatPayment) (libkb.MiniChatPaymentSpec, int64) {
   975  	spec := libkb.MiniChatPaymentSpec{Username: payment.Username}
   976  	xlmAmount := payment.Amount
   977  	if payment.Currency != "" && payment.Currency != "XLM" {
   978  		exchangeRate, err := walletState.ExchangeRate(mctx.Ctx(), payment.Currency)
   979  		if err != nil {
   980  			spec.Error = err
   981  			return spec, 0
   982  		}
   983  		spec.DisplayAmount, err = FormatCurrencyWithCodeSuffix(mctx, payment.Amount, exchangeRate.Currency, stellarnet.Round)
   984  		if err != nil {
   985  			spec.Error = err
   986  			return spec, 0
   987  		}
   988  
   989  		xlmAmount, err = stellarnet.ConvertOutsideToXLM(payment.Amount, exchangeRate.Rate)
   990  		if err != nil {
   991  			spec.Error = err
   992  			return spec, 0
   993  		}
   994  	}
   995  
   996  	xlmAmountNumeric, err := stellarnet.ParseStellarAmount(xlmAmount)
   997  	if err != nil {
   998  		spec.Error = err
   999  		return spec, 0
  1000  	}
  1001  
  1002  	spec.XLMAmount, err = FormatAmountDescriptionXLM(mctx, xlmAmount)
  1003  	if err != nil {
  1004  		spec.Error = err
  1005  		return spec, 0
  1006  	}
  1007  
  1008  	return spec, xlmAmountNumeric
  1009  }
  1010  
  1011  // SendMiniChatPayments sends multiple payments from one sender to multiple
  1012  // different recipients as fast as it can.  These come from chat messages
  1013  // like "+1XLM@alice +2XLM@charlie".
  1014  func SendMiniChatPayments(m libkb.MetaContext, walletState *WalletState, convID chat1.ConversationID, payments []libkb.MiniChatPayment) (res []libkb.MiniChatPaymentResult, err error) {
  1015  	defer m.Trace("Stellar.SendMiniChatPayments", &err)()
  1016  
  1017  	// look up sender account
  1018  	senderAccountID, senderSeed, err := LookupSenderSeed(m)
  1019  	if err != nil {
  1020  		return nil, err
  1021  	}
  1022  
  1023  	prepared, unlock, err := PrepareMiniChatPayments(m, walletState, senderSeed, convID, payments)
  1024  	defer unlock()
  1025  	if err != nil {
  1026  		return nil, err
  1027  	}
  1028  
  1029  	resultList := make([]libkb.MiniChatPaymentResult, len(payments))
  1030  
  1031  	// need to submit tx one at a time, in order
  1032  	for i := 0; i < len(prepared); i++ {
  1033  		if prepared[i] == nil {
  1034  			// this should never happen
  1035  			return nil, errors.New("mini chat prepare failed")
  1036  		}
  1037  		mcpResult := libkb.MiniChatPaymentResult{Username: prepared[i].Username}
  1038  		if prepared[i].Error != nil {
  1039  			mcpResult.Error = prepared[i].Error
  1040  		} else {
  1041  			// submit the transaction
  1042  			m.Debug("SEQNO ics %d submitting payment seqno %d (txid %s)", i, prepared[i].Seqno, prepared[i].TxID)
  1043  
  1044  			if err := walletState.AddPendingTx(m.Ctx(), senderAccountID, prepared[i].TxID, prepared[i].Seqno); err != nil {
  1045  				m.Debug("SEQNO ics %d error calling AddPendingTx: %s", i, err)
  1046  			}
  1047  
  1048  			var submitRes stellar1.PaymentResult
  1049  			switch {
  1050  			case prepared[i].Direct != nil:
  1051  				submitRes, err = walletState.SubmitPayment(m.Ctx(), *prepared[i].Direct)
  1052  			case prepared[i].Relay != nil:
  1053  				submitRes, err = walletState.SubmitRelayPayment(m.Ctx(), *prepared[i].Relay)
  1054  			default:
  1055  				mcpResult.Error = errors.New("no direct or relay payment")
  1056  			}
  1057  
  1058  			if err != nil {
  1059  				mcpResult.Error = err
  1060  				m.Debug("SEQNO ics %d submit error for txid %s, seqno %d: %s", i, prepared[i].TxID, prepared[i].Seqno, err)
  1061  				if rerr := walletState.RemovePendingTx(m.Ctx(), senderAccountID, prepared[i].TxID); rerr != nil {
  1062  					m.Debug("SEQNO ics %d error calling RemovePendingTx: %s", i, rerr)
  1063  				}
  1064  			} else {
  1065  				mcpResult.PaymentID = stellar1.NewPaymentID(submitRes.StellarID)
  1066  				m.Debug("SEQNO ics %d submit success txid %s, seqno %d", i, prepared[i].TxID, prepared[i].Seqno)
  1067  			}
  1068  		}
  1069  		resultList[i] = mcpResult
  1070  	}
  1071  
  1072  	return resultList, nil
  1073  }
  1074  
  1075  type MiniPrepared struct {
  1076  	Username libkb.NormalizedUsername
  1077  	Direct   *stellar1.PaymentDirectPost
  1078  	Relay    *stellar1.PaymentRelayPost
  1079  	TxID     stellar1.TransactionID
  1080  	Seqno    uint64
  1081  	Error    error
  1082  }
  1083  
  1084  func PrepareMiniChatPayments(m libkb.MetaContext, walletState *WalletState, senderSeed stellarnet.SeedStr, convID chat1.ConversationID, payments []libkb.MiniChatPayment) ([]*MiniPrepared, func(), error) {
  1085  	prepared := make(chan *MiniPrepared)
  1086  
  1087  	baseFee := walletState.BaseFee(m)
  1088  	sp, unlock := NewSeqnoProvider(m, walletState)
  1089  	tb, err := getTimeboundsForSending(m, walletState)
  1090  	if err != nil {
  1091  		return nil, unlock, err
  1092  	}
  1093  
  1094  	for _, payment := range payments {
  1095  		go func(p libkb.MiniChatPayment) {
  1096  			prepared <- prepareMiniChatPayment(m, walletState, sp, tb, senderSeed, convID, p, baseFee)
  1097  		}(payment)
  1098  	}
  1099  
  1100  	// prepared chan could be out of order, so sort by seqno
  1101  	preparedList := make([]*MiniPrepared, len(payments))
  1102  	for i := 0; i < len(payments); i++ {
  1103  		preparedList[i] = <-prepared
  1104  	}
  1105  	sort.Slice(preparedList, func(a, b int) bool { return preparedList[a].Seqno < preparedList[b].Seqno })
  1106  
  1107  	return preparedList, unlock, nil
  1108  }
  1109  
  1110  func prepareMiniChatPayment(m libkb.MetaContext, remoter remote.Remoter, sp build.SequenceProvider, tb *build.Timebounds, senderSeed stellarnet.SeedStr, convID chat1.ConversationID, payment libkb.MiniChatPayment, baseFee uint64) *MiniPrepared {
  1111  	result := &MiniPrepared{Username: payment.Username}
  1112  	recipient, err := LookupRecipient(m, stellarcommon.RecipientInput(payment.Username.String()), false)
  1113  	if err != nil {
  1114  		m.Debug("LookupRecipient error: %s", err)
  1115  		result.Error = errors.New("error looking up recipient")
  1116  		return result
  1117  	}
  1118  
  1119  	if recipient.AccountID == nil {
  1120  		return prepareMiniChatPaymentRelay(m, remoter, sp, tb, senderSeed, convID, payment, recipient, baseFee)
  1121  	}
  1122  	return prepareMiniChatPaymentDirect(m, remoter, sp, tb, senderSeed, convID, payment, recipient, baseFee)
  1123  }
  1124  
  1125  func prepareMiniChatPaymentDirect(m libkb.MetaContext, remoter remote.Remoter, sp build.SequenceProvider, tb *build.Timebounds, senderSeed stellarnet.SeedStr, convID chat1.ConversationID, payment libkb.MiniChatPayment, recipient stellarcommon.Recipient, baseFee uint64) *MiniPrepared {
  1126  	result := &MiniPrepared{Username: payment.Username}
  1127  	funded, err := isAccountFunded(m.Ctx(), remoter, stellar1.AccountID(recipient.AccountID.String()))
  1128  	if err != nil {
  1129  		result.Error = err
  1130  		return result
  1131  	}
  1132  
  1133  	result.Direct = &stellar1.PaymentDirectPost{
  1134  		FromDeviceID: m.G().ActiveDevice.DeviceID(),
  1135  		To:           &recipient.User.UV,
  1136  		QuickReturn:  true,
  1137  	}
  1138  	if convID != nil {
  1139  		result.Direct.ChatConversationID = stellar1.NewChatConversationID(convID)
  1140  	}
  1141  
  1142  	xlmAmount := payment.Amount
  1143  	if payment.Currency != "" && payment.Currency != "XLM" {
  1144  		result.Direct.DisplayAmount = payment.Amount
  1145  		result.Direct.DisplayCurrency = payment.Currency
  1146  		exchangeRate, err := remoter.ExchangeRate(m.Ctx(), payment.Currency)
  1147  		if err != nil {
  1148  			result.Error = err
  1149  			return result
  1150  		}
  1151  
  1152  		xlmAmount, err = stellarnet.ConvertOutsideToXLM(payment.Amount, exchangeRate.Rate)
  1153  		if err != nil {
  1154  			result.Error = err
  1155  			return result
  1156  		}
  1157  	}
  1158  
  1159  	var signResult stellarnet.SignResult
  1160  	if funded {
  1161  		signResult, err = stellarnet.PaymentXLMTransactionWithMemo(senderSeed, *recipient.AccountID, xlmAmount, stellarnet.NewMemoNone(), sp, tb, baseFee)
  1162  	} else {
  1163  		if isAmountLessThanMin(xlmAmount, minAmountCreateAccountXLM) {
  1164  			result.Error = fmt.Errorf("you must send at least %s XLM to fund the account", minAmountCreateAccountXLM)
  1165  			return result
  1166  		}
  1167  		signResult, err = stellarnet.CreateAccountXLMTransactionWithMemo(senderSeed, *recipient.AccountID, xlmAmount, stellarnet.NewMemoNone(), sp, tb, baseFee)
  1168  	}
  1169  	if err != nil {
  1170  		result.Error = err
  1171  		return result
  1172  	}
  1173  	result.Direct.SignedTransaction = signResult.Signed
  1174  	result.Seqno = signResult.Seqno
  1175  	result.TxID = stellar1.TransactionID(signResult.TxHash)
  1176  
  1177  	return result
  1178  }
  1179  
  1180  func prepareMiniChatPaymentRelay(mctx libkb.MetaContext, remoter remote.Remoter, sp build.SequenceProvider, tb *build.Timebounds, senderSeed stellarnet.SeedStr, convID chat1.ConversationID, payment libkb.MiniChatPayment, recipient stellarcommon.Recipient, baseFee uint64) *MiniPrepared {
  1181  	result := &MiniPrepared{Username: payment.Username}
  1182  
  1183  	appKey, teamID, err := relays.GetKey(mctx, recipient)
  1184  	if err != nil {
  1185  		result.Error = err
  1186  		return result
  1187  	}
  1188  
  1189  	xlmAmount := payment.Amount
  1190  	var displayAmount, displayCurrency string
  1191  	if payment.Currency != "" && payment.Currency != "XLM" {
  1192  		displayAmount = payment.Amount
  1193  		displayCurrency = payment.Currency
  1194  		exchangeRate, err := remoter.ExchangeRate(mctx.Ctx(), payment.Currency)
  1195  		if err != nil {
  1196  			result.Error = err
  1197  			return result
  1198  		}
  1199  
  1200  		xlmAmount, err = stellarnet.ConvertOutsideToXLM(payment.Amount, exchangeRate.Rate)
  1201  		if err != nil {
  1202  			result.Error = err
  1203  			return result
  1204  		}
  1205  	}
  1206  
  1207  	if isAmountLessThanMin(xlmAmount, minAmountRelayXLM) {
  1208  		result.Error = fmt.Errorf("you must send at least %s XLM to fund the account", minAmountRelayXLM)
  1209  		return result
  1210  	}
  1211  
  1212  	relay, err := relays.Create(relays.Input{
  1213  		From:          stellar1.SecretKey(senderSeed),
  1214  		AmountXLM:     xlmAmount,
  1215  		EncryptFor:    appKey,
  1216  		SeqnoProvider: sp,
  1217  		Timebounds:    tb,
  1218  		BaseFee:       baseFee,
  1219  	})
  1220  	if err != nil {
  1221  		result.Error = err
  1222  		return result
  1223  	}
  1224  
  1225  	post := stellar1.PaymentRelayPost{
  1226  		FromDeviceID:      mctx.ActiveDevice().DeviceID(),
  1227  		ToAssertion:       string(recipient.Input),
  1228  		RelayAccount:      relay.RelayAccountID,
  1229  		TeamID:            teamID,
  1230  		BoxB64:            relay.EncryptedB64,
  1231  		SignedTransaction: relay.FundTx.Signed,
  1232  		DisplayAmount:     displayAmount,
  1233  		DisplayCurrency:   displayCurrency,
  1234  		QuickReturn:       true,
  1235  	}
  1236  	if recipient.User != nil {
  1237  		post.To = &recipient.User.UV
  1238  	}
  1239  
  1240  	result.Relay = &post
  1241  	result.Seqno = relay.FundTx.Seqno
  1242  	result.TxID = stellar1.TransactionID(relay.FundTx.TxHash)
  1243  
  1244  	if convID != nil {
  1245  		result.Relay.ChatConversationID = stellar1.NewChatConversationID(convID)
  1246  	}
  1247  
  1248  	return result
  1249  }
  1250  
  1251  // sendRelayPayment sends XLM through a relay account.
  1252  // The balance of the relay account can be claimed by either party.
  1253  func sendRelayPayment(mctx libkb.MetaContext, walletState *WalletState,
  1254  	from stellar1.SecretKey, recipient stellarcommon.Recipient, amount string, displayBalance DisplayBalance,
  1255  	secretNote string, publicMemo *stellarnet.Memo, quickReturn bool, senderEntryPrimary bool, baseFee uint64) (res SendPaymentResult, err error) {
  1256  	defer mctx.Trace("Stellar.sendRelayPayment", &err)()
  1257  	appKey, teamID, err := relays.GetKey(mctx, recipient)
  1258  	if err != nil {
  1259  		return res, err
  1260  	}
  1261  
  1262  	if isAmountLessThanMin(amount, minAmountRelayXLM) {
  1263  		return res, fmt.Errorf("you must send at least %s XLM to fund the account for %s", minAmountRelayXLM, recipient.Input)
  1264  	}
  1265  
  1266  	sp, unlock := NewSeqnoProvider(mctx, walletState)
  1267  	defer unlock()
  1268  	tb, err := getTimeboundsForSending(mctx, walletState)
  1269  	if err != nil {
  1270  		return res, err
  1271  	}
  1272  	relay, err := relays.Create(relays.Input{
  1273  		From:          from,
  1274  		AmountXLM:     amount,
  1275  		Note:          secretNote,
  1276  		PublicMemo:    publicMemo,
  1277  		EncryptFor:    appKey,
  1278  		SeqnoProvider: sp,
  1279  		Timebounds:    tb,
  1280  		BaseFee:       baseFee,
  1281  	})
  1282  	if err != nil {
  1283  		return res, err
  1284  	}
  1285  
  1286  	_, accountID, _, err := libkb.ParseStellarSecretKey(string(from))
  1287  	if err != nil {
  1288  		return res, err
  1289  	}
  1290  	if err := walletState.AddPendingTx(mctx.Ctx(), accountID, stellar1.TransactionID(relay.FundTx.TxHash), relay.FundTx.Seqno); err != nil {
  1291  		mctx.Debug("error calling AddPendingTx: %s", err)
  1292  	}
  1293  
  1294  	post := stellar1.PaymentRelayPost{
  1295  		FromDeviceID:      mctx.ActiveDevice().DeviceID(),
  1296  		ToAssertion:       string(recipient.Input),
  1297  		RelayAccount:      relay.RelayAccountID,
  1298  		TeamID:            teamID,
  1299  		BoxB64:            relay.EncryptedB64,
  1300  		SignedTransaction: relay.FundTx.Signed,
  1301  		DisplayAmount:     displayBalance.Amount,
  1302  		DisplayCurrency:   displayBalance.Currency,
  1303  		QuickReturn:       quickReturn,
  1304  	}
  1305  	if recipient.User != nil {
  1306  		post.To = &recipient.User.UV
  1307  	}
  1308  	rres, err := walletState.SubmitRelayPayment(mctx.Ctx(), post)
  1309  	if err != nil {
  1310  		if rerr := walletState.RemovePendingTx(mctx.Ctx(), accountID, stellar1.TransactionID(relay.FundTx.TxHash)); rerr != nil {
  1311  			mctx.Debug("error calling RemovePendingTx: %s", rerr)
  1312  		}
  1313  		return res, err
  1314  	}
  1315  	mctx.Debug("sent payment (relay) kbTxID:%v txID:%v pending:%v", rres.KeybaseID, rres.StellarID, rres.Pending)
  1316  
  1317  	if !rres.Pending {
  1318  		if err := walletState.RemovePendingTx(mctx.Ctx(), accountID, stellar1.TransactionID(relay.FundTx.TxHash)); err != nil {
  1319  			mctx.Debug("error calling RemovePendingTx: %s", err)
  1320  		}
  1321  	}
  1322  
  1323  	var chatRecipient string
  1324  	if senderEntryPrimary {
  1325  		chatRecipient = chatRecipientStr(mctx, recipient)
  1326  		sendChat := func(mctx libkb.MetaContext) {
  1327  			chatSendPaymentMessageSoft(mctx, chatRecipient, rres.StellarID, "SendRelayPayment")
  1328  		}
  1329  		if post.QuickReturn {
  1330  			go sendChat(mctx.WithCtx(context.Background()))
  1331  		} else {
  1332  			sendChat(mctx)
  1333  		}
  1334  	} else {
  1335  		mctx.Debug("not sending chat message (relay): sending from non-primary account")
  1336  	}
  1337  
  1338  	return SendPaymentResult{
  1339  		KbTxID:      rres.KeybaseID,
  1340  		TxID:        rres.StellarID,
  1341  		Pending:     rres.Pending,
  1342  		RelayTeamID: &teamID,
  1343  		JumpToChat:  chatRecipient,
  1344  	}, nil
  1345  }
  1346  
  1347  // Claim claims a waiting relay.
  1348  // If `dir` is nil the direction is inferred.
  1349  func Claim(mctx libkb.MetaContext, walletState *WalletState,
  1350  	txID string, into stellar1.AccountID, dir *stellar1.RelayDirection,
  1351  	autoClaimToken *string) (res stellar1.RelayClaimResult, err error) {
  1352  	defer mctx.Trace("Stellar.Claim", &err)()
  1353  	mctx.Debug("Stellar.Claim(txID:%v, into:%v, dir:%v, autoClaimToken:%v)", txID, into, dir, autoClaimToken)
  1354  	details, err := walletState.PaymentDetailsGeneric(mctx.Ctx(), txID)
  1355  	if err != nil {
  1356  		return res, err
  1357  	}
  1358  	p := details.Summary
  1359  	typ, err := p.Typ()
  1360  	if err != nil {
  1361  		return res, fmt.Errorf("error getting payment details: %v", err)
  1362  	}
  1363  	switch typ {
  1364  	case stellar1.PaymentSummaryType_STELLAR:
  1365  		return res, fmt.Errorf("Payment cannot be claimed. It was found on the Stellar network but not in Keybase.")
  1366  	case stellar1.PaymentSummaryType_DIRECT:
  1367  		p := p.Direct()
  1368  		switch p.TxStatus {
  1369  		case stellar1.TransactionStatus_SUCCESS:
  1370  			return res, fmt.Errorf("Payment cannot be claimed. The direct transfer already happened.")
  1371  		case stellar1.TransactionStatus_PENDING:
  1372  			return res, fmt.Errorf("Payment cannot be claimed. It is currently pending.")
  1373  		default:
  1374  			return res, fmt.Errorf("Payment cannot be claimed. The payment failed anyway.")
  1375  		}
  1376  	case stellar1.PaymentSummaryType_RELAY:
  1377  		return claimPaymentWithDetail(mctx, walletState, p.Relay(), into, dir)
  1378  	default:
  1379  		return res, fmt.Errorf("unrecognized payment type: %v", typ)
  1380  	}
  1381  }
  1382  
  1383  // If `dir` is nil the direction is inferred.
  1384  func claimPaymentWithDetail(mctx libkb.MetaContext, walletState *WalletState,
  1385  	p stellar1.PaymentSummaryRelay, into stellar1.AccountID, dir *stellar1.RelayDirection) (res stellar1.RelayClaimResult, err error) {
  1386  	if p.Claim != nil && p.Claim.TxStatus == stellar1.TransactionStatus_SUCCESS {
  1387  		recipient, _, err := mctx.G().GetUPAKLoader().Load(libkb.NewLoadUserByUIDArg(mctx.Ctx(), mctx.G(), p.Claim.To.Uid))
  1388  		if err != nil || recipient == nil {
  1389  			return res, fmt.Errorf("Payment already claimed")
  1390  		}
  1391  		return res, fmt.Errorf("Payment already claimed by %v", recipient.GetName())
  1392  	}
  1393  	rsec, err := relays.DecryptB64(mctx, p.TeamID, p.BoxB64)
  1394  	if err != nil {
  1395  		return res, fmt.Errorf("error opening secret to claim: %v", err)
  1396  	}
  1397  	skey, _, _, err := libkb.ParseStellarSecretKey(rsec.Sk.SecureNoLogString())
  1398  	if err != nil {
  1399  		return res, fmt.Errorf("error using shared secret key: %v", err)
  1400  	}
  1401  	destinationFunded, err := isAccountFunded(mctx.Ctx(), walletState, into)
  1402  	if err != nil {
  1403  		return res, err
  1404  	}
  1405  	useDir := stellar1.RelayDirection_CLAIM
  1406  	if dir == nil {
  1407  		// Infer direction
  1408  		if p.From.Uid.Equal(mctx.ActiveDevice().UID()) {
  1409  			useDir = stellar1.RelayDirection_YANK
  1410  		}
  1411  	} else {
  1412  		// Direction from caller
  1413  		useDir = *dir
  1414  	}
  1415  
  1416  	baseFee := walletState.BaseFee(mctx)
  1417  	sp, unlock := NewClaimSeqnoProvider(mctx, walletState)
  1418  	defer unlock()
  1419  	tb, err := getTimeboundsForSending(mctx, walletState)
  1420  	if err != nil {
  1421  		return res, err
  1422  	}
  1423  	sig, err := stellarnet.RelocateTransaction(stellarnet.SeedStr(skey.SecureNoLogString()),
  1424  		stellarnet.AddressStr(into.String()), destinationFunded, nil, sp, tb, baseFee)
  1425  	if err != nil {
  1426  		return res, fmt.Errorf("error building claim transaction: %v", err)
  1427  	}
  1428  	return walletState.SubmitRelayClaim(mctx.Ctx(), stellar1.RelayClaimPost{
  1429  		KeybaseID:         p.KbTxID,
  1430  		Dir:               useDir,
  1431  		SignedTransaction: sig.Signed,
  1432  	})
  1433  }
  1434  
  1435  func isAccountFunded(ctx context.Context, remoter remote.Remoter, accountID stellar1.AccountID) (funded bool, err error) {
  1436  	balances, err := remoter.Balances(ctx, accountID)
  1437  	if err != nil {
  1438  		return false, err
  1439  	}
  1440  	return hasPositiveLumenBalance(balances)
  1441  }
  1442  
  1443  func hasPositiveLumenBalance(balances []stellar1.Balance) (res bool, err error) {
  1444  	for _, b := range balances {
  1445  		if b.Asset.IsNativeXLM() {
  1446  			a, err := stellarnet.ParseStellarAmount(b.Amount)
  1447  			if err != nil {
  1448  				return false, err
  1449  			}
  1450  			if a > 0 {
  1451  				return true, nil
  1452  			}
  1453  		}
  1454  	}
  1455  	return false, nil
  1456  }
  1457  
  1458  func GetOwnPrimaryAccountID(mctx libkb.MetaContext) (res stellar1.AccountID, err error) {
  1459  	activeBundle, err := remote.FetchSecretlessBundle(mctx)
  1460  	if err != nil {
  1461  		return res, err
  1462  	}
  1463  	primary, err := activeBundle.PrimaryAccount()
  1464  	if err != nil {
  1465  		return res, err
  1466  	}
  1467  	return primary.AccountID, nil
  1468  }
  1469  
  1470  func RecentPaymentsCLILocal(mctx libkb.MetaContext, remoter remote.Remoter, accountID stellar1.AccountID) (res []stellar1.PaymentOrErrorCLILocal, err error) {
  1471  	defer mctx.Trace("Stellar.RecentPaymentsCLILocal", &err)()
  1472  	arg := remote.RecentPaymentsArg{
  1473  		AccountID:       accountID,
  1474  		IncludeAdvanced: true,
  1475  	}
  1476  	page, err := remoter.RecentPayments(mctx.Ctx(), arg)
  1477  	if err != nil {
  1478  		return nil, err
  1479  	}
  1480  	for _, p := range page.Payments {
  1481  		lp, err := localizePayment(mctx, p)
  1482  		if err == nil {
  1483  			res = append(res, stellar1.PaymentOrErrorCLILocal{
  1484  				Payment: &lp,
  1485  			})
  1486  		} else {
  1487  			errStr := err.Error()
  1488  			res = append(res, stellar1.PaymentOrErrorCLILocal{
  1489  				Err: &errStr,
  1490  			})
  1491  		}
  1492  	}
  1493  	return res, nil
  1494  }
  1495  
  1496  func PaymentDetailCLILocal(ctx context.Context, g *libkb.GlobalContext, remoter remote.Remoter, txID string) (res stellar1.PaymentCLILocal, err error) {
  1497  	defer g.CTrace(ctx, "Stellar.PaymentDetailCLILocal", &err)()
  1498  	payment, err := remoter.PaymentDetailsGeneric(ctx, txID)
  1499  	if err != nil {
  1500  		return res, err
  1501  	}
  1502  	mctx := libkb.NewMetaContext(ctx, g)
  1503  	p, err := localizePayment(mctx, payment.Summary)
  1504  	if err != nil {
  1505  		return res, err
  1506  	}
  1507  
  1508  	p.PublicNote = payment.Memo
  1509  	p.PublicNoteType = payment.MemoType
  1510  	if payment.FeeCharged != "" {
  1511  		p.FeeChargedDescription, err = FormatAmountDescriptionXLM(mctx, payment.FeeCharged)
  1512  		if err != nil {
  1513  			return res, err
  1514  		}
  1515  	}
  1516  
  1517  	return p, nil
  1518  }
  1519  
  1520  // When isCLI : Identifies the recipient checking track breaks and all.
  1521  // When not isCLI: Does a verified lookup of the assertion.
  1522  // Returns an error if a resolution was found but failed.
  1523  // Returns ("", nil) if no resolution was found.
  1524  func lookupRecipientAssertion(m libkb.MetaContext, assertion string, isCLI bool) (maybeUsername string, err error) {
  1525  	defer m.Trace(fmt.Sprintf("Stellar.lookupRecipientAssertion(isCLI:%v, %v)", isCLI, assertion), &err)()
  1526  	reason := fmt.Sprintf("Find transaction recipient for %s", assertion)
  1527  
  1528  	// GUI is a verified lookup modeled after func ResolveAndCheck.
  1529  	arg := keybase1.Identify2Arg{
  1530  		UserAssertion:         assertion,
  1531  		CanSuppressUI:         true,
  1532  		ActLoggedOut:          true,
  1533  		NoErrorOnTrackFailure: true,
  1534  		Reason:                keybase1.IdentifyReason{Reason: reason},
  1535  		IdentifyBehavior:      keybase1.TLFIdentifyBehavior_RESOLVE_AND_CHECK,
  1536  	}
  1537  	if isCLI {
  1538  		// CLI is a real identify
  1539  		arg = keybase1.Identify2Arg{
  1540  			UserAssertion:    assertion,
  1541  			UseDelegateUI:    true,
  1542  			Reason:           keybase1.IdentifyReason{Reason: reason},
  1543  			IdentifyBehavior: keybase1.TLFIdentifyBehavior_CLI,
  1544  		}
  1545  	}
  1546  
  1547  	eng := engine.NewResolveThenIdentify2(m.G(), &arg)
  1548  	err = engine.RunEngine2(m, eng)
  1549  	if err != nil {
  1550  		// These errors mean no resolution was found.
  1551  		if _, ok := err.(libkb.NotFoundError); ok {
  1552  			m.Debug("identifyRecipient: not found %s: %s", assertion, err)
  1553  			return "", nil
  1554  		}
  1555  		if libkb.IsResolutionNotFoundError(err) {
  1556  			m.Debug("identifyRecipient: resolution not found error %s: %s", assertion, err)
  1557  			return "", nil
  1558  		}
  1559  		return "", err
  1560  	}
  1561  
  1562  	idRes, err := eng.Result(m)
  1563  	if err != nil {
  1564  		return "", err
  1565  	}
  1566  	if idRes == nil {
  1567  		return "", fmt.Errorf("missing identify result")
  1568  	}
  1569  	m.Debug("lookupRecipientAssertion: uv: %v", idRes.Upk.Current.ToUserVersion())
  1570  	username := idRes.Upk.GetName()
  1571  	if username == "" {
  1572  		return "", fmt.Errorf("empty identify result username")
  1573  	}
  1574  	if isCLI && idRes.TrackBreaks != nil {
  1575  		m.Debug("lookupRecipientAssertion: TrackBreaks = %+v", idRes.TrackBreaks)
  1576  		return "", libkb.TrackingBrokeError{}
  1577  	}
  1578  	return username, nil
  1579  }
  1580  
  1581  // ChangeAccountName changes the name of an account.
  1582  // Make sure to keep this in sync with ValidateAccountNameLocal.
  1583  // An empty name is not allowed.
  1584  // Renaming an account to an already used name is blocked.
  1585  // Maximum length of AccountNameMaxRunes runes.
  1586  func ChangeAccountName(m libkb.MetaContext, walletState *WalletState, accountID stellar1.AccountID, newName string) (err error) {
  1587  	if newName == "" {
  1588  		return fmt.Errorf("name required")
  1589  	}
  1590  	runes := utf8.RuneCountInString(newName)
  1591  	if runes > AccountNameMaxRunes {
  1592  		return fmt.Errorf("account name can be %v characters at the longest but was %v", AccountNameMaxRunes, runes)
  1593  	}
  1594  	b, err := remote.FetchSecretlessBundle(m)
  1595  	if err != nil {
  1596  		return err
  1597  	}
  1598  	var found bool
  1599  	for i, acc := range b.Accounts {
  1600  		if acc.AccountID.Eq(accountID) {
  1601  			// Change Name in place to modify Account struct.
  1602  			b.Accounts[i].Name = newName
  1603  			found = true
  1604  		} else if acc.Name == newName {
  1605  			return fmt.Errorf("you already have an account with that name")
  1606  		}
  1607  	}
  1608  	if !found {
  1609  		return fmt.Errorf("account not found: %v", accountID)
  1610  	}
  1611  	nextBundle := bundle.AdvanceBundle(*b)
  1612  	if err := remote.Post(m, nextBundle); err != nil {
  1613  		return err
  1614  	}
  1615  
  1616  	return walletState.UpdateAccountEntriesWithBundle(m, "change account name", &nextBundle)
  1617  }
  1618  
  1619  func SetAccountAsPrimary(m libkb.MetaContext, walletState *WalletState, accountID stellar1.AccountID) (err error) {
  1620  	if accountID.IsNil() {
  1621  		return errors.New("passed empty AccountID")
  1622  	}
  1623  	b, err := remote.FetchAccountBundle(m, accountID)
  1624  	if err != nil {
  1625  		return err
  1626  	}
  1627  	var foundAccID, foundPrimary bool
  1628  	for i, acc := range b.Accounts {
  1629  		if acc.AccountID.Eq(accountID) {
  1630  			if acc.IsPrimary {
  1631  				// Nothing to do.
  1632  				return nil
  1633  			}
  1634  			b.Accounts[i].IsPrimary = true
  1635  			foundAccID = true
  1636  		} else if acc.IsPrimary {
  1637  			b.Accounts[i].IsPrimary = false
  1638  			foundPrimary = true
  1639  		}
  1640  
  1641  		if foundAccID && foundPrimary {
  1642  			break
  1643  		}
  1644  	}
  1645  	if !foundAccID {
  1646  		return fmt.Errorf("account not found: %v", accountID)
  1647  	}
  1648  	nextBundle := bundle.AdvanceAccounts(*b, []stellar1.AccountID{accountID})
  1649  	if err = remote.PostWithChainlink(m, nextBundle); err != nil {
  1650  		return err
  1651  	}
  1652  
  1653  	return walletState.UpdateAccountEntriesWithBundle(m, "set account as primary", &nextBundle)
  1654  }
  1655  
  1656  func DeleteAccount(m libkb.MetaContext, accountID stellar1.AccountID) error {
  1657  	if accountID.IsNil() {
  1658  		return errors.New("passed empty AccountID")
  1659  	}
  1660  	prevBundle, err := remote.FetchAccountBundle(m, accountID)
  1661  	if err != nil {
  1662  		return err
  1663  	}
  1664  
  1665  	nextBundle := bundle.AdvanceBundle(*prevBundle)
  1666  	var found bool
  1667  	for i, acc := range nextBundle.Accounts {
  1668  		if acc.AccountID.Eq(accountID) {
  1669  			if acc.IsPrimary {
  1670  				return fmt.Errorf("cannot delete primary account %v", accountID)
  1671  			}
  1672  
  1673  			nextBundle.Accounts = append(nextBundle.Accounts[:i], nextBundle.Accounts[i+1:]...)
  1674  			delete(nextBundle.AccountBundles, accountID)
  1675  			found = true
  1676  			break
  1677  		}
  1678  	}
  1679  	if !found {
  1680  		return fmt.Errorf("account not found: %v", accountID)
  1681  	}
  1682  	return remote.Post(m, nextBundle)
  1683  }
  1684  
  1685  const DefaultCurrencySetting = "USD"
  1686  
  1687  // GetAccountDisplayCurrency gets currency setting from the server, and it
  1688  // returned currency is empty (NULL in database), then default "USD" is used.
  1689  // When creating a wallet, client always sets default currency setting. Also
  1690  // when a new account in existing wallet is created, it will inherit currency
  1691  // setting from primary account (this happens serverside). Empty currency
  1692  // settings should only happen in very old accounts or when wallet generation
  1693  // was interrupted in precise moment.
  1694  func GetAccountDisplayCurrency(mctx libkb.MetaContext, accountID stellar1.AccountID) (res string, err error) {
  1695  	codeStr, err := remote.GetAccountDisplayCurrency(mctx.Ctx(), mctx.G(), accountID)
  1696  	if err != nil {
  1697  		if err != remote.ErrAccountIDMissing {
  1698  			return res, err
  1699  		}
  1700  		codeStr = "" // to be safe so it uses default below
  1701  	}
  1702  	if codeStr == "" {
  1703  		codeStr = DefaultCurrencySetting
  1704  		mctx.Debug("Using default display currency %s for account %s", codeStr, accountID)
  1705  	}
  1706  	return codeStr, nil
  1707  }
  1708  
  1709  func GetCurrencySetting(mctx libkb.MetaContext, accountID stellar1.AccountID) (res stellar1.CurrencyLocal, err error) {
  1710  	codeStr, err := GetAccountDisplayCurrency(mctx, accountID)
  1711  	if err != nil {
  1712  		return res, err
  1713  	}
  1714  	conf, err := mctx.G().GetStellar().GetServerDefinitions(mctx.Ctx())
  1715  	if err != nil {
  1716  		return res, err
  1717  	}
  1718  	currency, ok := conf.GetCurrencyLocal(stellar1.OutsideCurrencyCode(codeStr))
  1719  	if !ok {
  1720  		return res, fmt.Errorf("Got unrecognized currency code %q", codeStr)
  1721  	}
  1722  	return currency, nil
  1723  }
  1724  
  1725  func CreateNewAccount(mctx libkb.MetaContext, accountName string) (ret stellar1.AccountID, err error) {
  1726  	prevBundle, err := remote.FetchSecretlessBundle(mctx)
  1727  	if err != nil {
  1728  		return ret, err
  1729  	}
  1730  	nextBundle := bundle.AdvanceBundle(*prevBundle)
  1731  	ret, err = bundle.CreateNewAccount(&nextBundle, accountName, false /* makePrimary */)
  1732  	if err != nil {
  1733  		return ret, err
  1734  	}
  1735  	return ret, remote.Post(mctx, nextBundle)
  1736  }
  1737  
  1738  func chatRecipientStr(mctx libkb.MetaContext, recipient stellarcommon.Recipient) string {
  1739  	if recipient.User != nil {
  1740  		if recipient.User.UV.Uid.Equal(mctx.ActiveDevice().UID()) {
  1741  			// Don't send chat to self.
  1742  			return ""
  1743  		}
  1744  		return recipient.User.Username.String()
  1745  	} else if recipient.Assertion != nil {
  1746  		return recipient.Assertion.String()
  1747  	}
  1748  	return ""
  1749  }
  1750  
  1751  func chatSendPaymentMessageSoft(mctx libkb.MetaContext, to string, txID stellar1.TransactionID, logLabel string) {
  1752  	if to == "" {
  1753  		return
  1754  	}
  1755  	err := chatSendPaymentMessage(mctx, to, txID, false)
  1756  	if err != nil {
  1757  		// if the chat message fails to send, just log the error
  1758  		mctx.Debug("failed to send chat %v mesage: %s", logLabel, err)
  1759  	}
  1760  }
  1761  
  1762  func chatSendPaymentMessage(m libkb.MetaContext, to string, txID stellar1.TransactionID, blocking bool) error {
  1763  	m.G().StartStandaloneChat()
  1764  	if m.G().ChatHelper == nil {
  1765  		return errors.New("cannot send SendPayment message:  chat helper is nil")
  1766  	}
  1767  
  1768  	name := strings.Join([]string{m.CurrentUsername().String(), to}, ",")
  1769  
  1770  	msg := chat1.MessageSendPayment{
  1771  		PaymentID: stellar1.NewPaymentID(txID),
  1772  	}
  1773  
  1774  	body := chat1.NewMessageBodyWithSendpayment(msg)
  1775  
  1776  	// identify already performed, so skip here
  1777  	var err error
  1778  	if blocking {
  1779  		err = m.G().ChatHelper.SendMsgByName(m.Ctx(), name, nil,
  1780  			chat1.ConversationMembersType_IMPTEAMNATIVE, keybase1.TLFIdentifyBehavior_CHAT_SKIP, body,
  1781  			chat1.MessageType_SENDPAYMENT)
  1782  	} else {
  1783  		_, err = m.G().ChatHelper.SendMsgByNameNonblock(m.Ctx(), name, nil,
  1784  			chat1.ConversationMembersType_IMPTEAMNATIVE, keybase1.TLFIdentifyBehavior_CHAT_SKIP, body,
  1785  			chat1.MessageType_SENDPAYMENT, nil)
  1786  	}
  1787  	return err
  1788  }
  1789  
  1790  type MakeRequestArg struct {
  1791  	To       stellarcommon.RecipientInput
  1792  	Amount   string
  1793  	Asset    *stellar1.Asset
  1794  	Currency *stellar1.OutsideCurrencyCode
  1795  	Note     string
  1796  }
  1797  
  1798  func MakeRequestGUI(m libkb.MetaContext, remoter remote.Remoter, arg MakeRequestArg) (ret stellar1.KeybaseRequestID, err error) {
  1799  	return makeRequest(m, remoter, arg, false /* isCLI */)
  1800  }
  1801  
  1802  func MakeRequestCLI(m libkb.MetaContext, remoter remote.Remoter, arg MakeRequestArg) (ret stellar1.KeybaseRequestID, err error) {
  1803  	return makeRequest(m, remoter, arg, true /* isCLI */)
  1804  }
  1805  
  1806  func makeRequest(m libkb.MetaContext, remoter remote.Remoter, arg MakeRequestArg, isCLI bool) (ret stellar1.KeybaseRequestID, err error) {
  1807  	defer m.Trace("Stellar.MakeRequest", &err)()
  1808  
  1809  	if arg.Asset == nil && arg.Currency == nil {
  1810  		return ret, fmt.Errorf("expected either Asset or Currency, got none")
  1811  	} else if arg.Asset != nil && arg.Currency != nil {
  1812  		return ret, fmt.Errorf("expected either Asset or Currency, got both")
  1813  	}
  1814  
  1815  	if arg.Asset != nil && !arg.Asset.IsNativeXLM() {
  1816  		return ret, fmt.Errorf("requesting non-XLM assets is not supported")
  1817  	}
  1818  
  1819  	if arg.Asset != nil {
  1820  		a, err := stellarnet.ParseStellarAmount(arg.Amount)
  1821  		if err != nil {
  1822  			return ret, err
  1823  		}
  1824  		if a <= 0 {
  1825  			return ret, fmt.Errorf("must request positive amount of XLM")
  1826  		}
  1827  	}
  1828  
  1829  	if arg.Currency != nil {
  1830  		conf, err := m.G().GetStellar().GetServerDefinitions(m.Ctx())
  1831  		if err != nil {
  1832  			return ret, err
  1833  		}
  1834  		_, ok := conf.GetCurrencyLocal(*arg.Currency)
  1835  		if !ok {
  1836  			return ret, fmt.Errorf("unrecognized currency code %q", *arg.Currency)
  1837  		}
  1838  	}
  1839  
  1840  	// Make sure chat is functional. Chat message is the only way for
  1841  	// the recipient to learn about the request, so it's essential
  1842  	// that we are able to send REQUESTPAYMENT chat message.
  1843  	m.G().StartStandaloneChat()
  1844  	if m.G().ChatHelper == nil {
  1845  		return ret, errors.New("cannot send RequestPayment message: chat helper is nil")
  1846  	}
  1847  
  1848  	recipient, err := LookupRecipient(m, arg.To, isCLI)
  1849  	if err != nil {
  1850  		return ret, err
  1851  	}
  1852  
  1853  	post := stellar1.RequestPost{
  1854  		Amount:   arg.Amount,
  1855  		Asset:    arg.Asset,
  1856  		Currency: arg.Currency,
  1857  	}
  1858  
  1859  	switch {
  1860  	case recipient.User != nil:
  1861  		post.ToAssertion = recipient.User.Username.String()
  1862  		post.ToUser = &recipient.User.UV
  1863  	case recipient.Assertion != nil:
  1864  		post.ToAssertion = recipient.Assertion.String()
  1865  	default:
  1866  		return ret, fmt.Errorf("expected username or user assertion as recipient")
  1867  	}
  1868  
  1869  	requestID, err := remoter.SubmitRequest(m.Ctx(), post)
  1870  	if err != nil {
  1871  		return ret, err
  1872  	}
  1873  
  1874  	body := chat1.NewMessageBodyWithRequestpayment(chat1.MessageRequestPayment{
  1875  		RequestID: requestID,
  1876  		Note:      arg.Note,
  1877  	})
  1878  
  1879  	displayName := strings.Join([]string{m.CurrentUsername().String(), post.ToAssertion}, ",")
  1880  
  1881  	membersType := chat1.ConversationMembersType_IMPTEAMNATIVE
  1882  	err = m.G().ChatHelper.SendMsgByName(m.Ctx(), displayName, nil,
  1883  		membersType, keybase1.TLFIdentifyBehavior_CHAT_SKIP, body, chat1.MessageType_REQUESTPAYMENT)
  1884  	return requestID, err
  1885  }
  1886  
  1887  // Lookup a user who has the stellar account ID.
  1888  // Verifies the result against the user's sigchain.
  1889  // If there are no users, or multiple users, returns NotFoundError.
  1890  func LookupUserByAccountID(m libkb.MetaContext, accountID stellar1.AccountID) (uv keybase1.UserVersion, un libkb.NormalizedUsername, err error) {
  1891  	defer m.Trace(fmt.Sprintf("Stellar.LookupUserByAccount(%v)", accountID), &err)()
  1892  	usersUnverified, err := remote.LookupUnverified(m.Ctx(), m.G(), accountID)
  1893  	if err != nil {
  1894  		return uv, un, err
  1895  	}
  1896  	m.Debug("got %v unverified results", len(usersUnverified))
  1897  	for i, uv := range usersUnverified {
  1898  		m.Debug("usersUnverified[%v] = %v", i, uv)
  1899  	}
  1900  	if len(usersUnverified) == 0 {
  1901  		return uv, un, libkb.NotFoundError{Msg: fmt.Sprintf("No user found with account %v", accountID)}
  1902  	}
  1903  	if len(usersUnverified) > 1 {
  1904  		return uv, un, libkb.NotFoundError{Msg: fmt.Sprintf("Multiple users found with account: %v", accountID)}
  1905  	}
  1906  	uv = usersUnverified[0]
  1907  	// Verify that `uv` (from server) matches `accountID`.
  1908  	verify := func(forcePoll bool) (upak *keybase1.UserPlusKeysV2AllIncarnations, retry bool, err error) {
  1909  		defer m.Trace(fmt.Sprintf("verify(forcePoll:%v, accountID:%v, uv:%v)", forcePoll, accountID, uv), &err)()
  1910  		upak, _, err = m.G().GetUPAKLoader().LoadV2(
  1911  			libkb.NewLoadUserArgWithMetaContext(m).WithPublicKeyOptional().WithUID(uv.Uid).WithForcePoll(forcePoll))
  1912  		if err != nil {
  1913  			return nil, false, err
  1914  		}
  1915  		genericErr := errors.New("error verifying account lookup")
  1916  		if !upak.Current.EldestSeqno.Eq(uv.EldestSeqno) {
  1917  			m.Debug("user %v's eldest seqno did not match %v != %v", upak.Current.Username, upak.Current.EldestSeqno, uv.EldestSeqno)
  1918  			return nil, true, genericErr
  1919  		}
  1920  		if upak.Current.StellarAccountID == nil {
  1921  			m.Debug("user %v has no stellar account", upak.Current.Username)
  1922  			return nil, true, genericErr
  1923  		}
  1924  		unverifiedAccountID, err := libkb.ParseStellarAccountID(*upak.Current.StellarAccountID)
  1925  		if err != nil {
  1926  			m.Debug("user has invalid account ID '%v': %v", *upak.Current.StellarAccountID, err)
  1927  			return nil, false, genericErr
  1928  		}
  1929  		if !unverifiedAccountID.Eq(accountID) {
  1930  			m.Debug("user %v has different account %v != %v", upak.Current.Username, unverifiedAccountID, accountID)
  1931  			return nil, true, genericErr
  1932  		}
  1933  		return upak, false, nil
  1934  	}
  1935  	upak, retry, err := verify(false)
  1936  	if err == nil {
  1937  		return upak.Current.ToUserVersion(), libkb.NewNormalizedUsername(upak.Current.GetName()), err
  1938  	}
  1939  	if !retry {
  1940  		return keybase1.UserVersion{}, "", err
  1941  	}
  1942  	// Try again with ForcePoll in case the previous attempt lost a race.
  1943  	upak, _, err = verify(true)
  1944  	if err != nil {
  1945  		return keybase1.UserVersion{}, "", err
  1946  	}
  1947  	return upak.Current.ToUserVersion(), libkb.NewNormalizedUsername(upak.Current.GetName()), err
  1948  }
  1949  
  1950  // AccountExchangeRate returns the exchange rate for an account for the logged in user.
  1951  // Note that it is possible that multiple users can own the same account and have
  1952  // different display currency preferences.
  1953  func AccountExchangeRate(mctx libkb.MetaContext, remoter remote.Remoter, accountID stellar1.AccountID) (stellar1.OutsideExchangeRate, error) {
  1954  	currency, err := GetCurrencySetting(mctx, accountID)
  1955  	if err != nil {
  1956  		return stellar1.OutsideExchangeRate{}, err
  1957  	}
  1958  
  1959  	return remoter.ExchangeRate(mctx.Ctx(), string(currency.Code))
  1960  }
  1961  
  1962  func RefreshUnreadCount(g *libkb.GlobalContext, accountID stellar1.AccountID) {
  1963  	g.Log.Debug("RefreshUnreadCount for stellar account %s", accountID)
  1964  	s := getGlobal(g)
  1965  	ctx := context.Background()
  1966  	details, err := s.remoter.Details(ctx, accountID)
  1967  	if err != nil {
  1968  		return
  1969  	}
  1970  	g.Log.Debug("RefreshUnreadCount got details for stellar account %s", accountID)
  1971  
  1972  	err = s.UpdateUnreadCount(ctx, accountID, details.UnreadPayments)
  1973  	if err != nil {
  1974  		g.Log.Debug("RefreshUnreadCount UpdateUnreadCount error: %s", err)
  1975  	} else {
  1976  		g.Log.Debug("RefreshUnreadCount UpdateUnreadCount => %d for stellar account %s", details.UnreadPayments, accountID)
  1977  	}
  1978  }
  1979  
  1980  // Get a per-user key.
  1981  // Wait for attempt but only warn on error.
  1982  func perUserKeyUpgradeSoft(mctx libkb.MetaContext, reason string) {
  1983  	arg := &engine.PerUserKeyUpgradeArgs{}
  1984  	eng := engine.NewPerUserKeyUpgrade(mctx.G(), arg)
  1985  	err := engine.RunEngine2(mctx, eng)
  1986  	if err != nil {
  1987  		mctx.Debug("PerUserKeyUpgrade failed (%s): %v", reason, err)
  1988  	}
  1989  }
  1990  
  1991  func HasAcceptedDisclaimer(ctx context.Context, g *libkb.GlobalContext) (bool, error) {
  1992  	return getGlobal(g).hasAcceptedDisclaimer(ctx)
  1993  }
  1994  
  1995  func InformAcceptedDisclaimer(ctx context.Context, g *libkb.GlobalContext) {
  1996  	getGlobal(g).informAcceptedDisclaimer(ctx)
  1997  }
  1998  
  1999  func RandomBuildPaymentID() (stellar1.BuildPaymentID, error) {
  2000  	randBytes, err := libkb.RandBytes(15)
  2001  	if err != nil {
  2002  		return "", err
  2003  	}
  2004  	return stellar1.BuildPaymentID("bb" + hex.EncodeToString(randBytes)), nil
  2005  }
  2006  
  2007  func AllWalletAccounts(mctx libkb.MetaContext, remoter remote.Remoter) ([]stellar1.WalletAccountLocal, error) {
  2008  	bundle, err := remote.FetchSecretlessBundle(mctx)
  2009  	if err != nil {
  2010  		return nil, err
  2011  	}
  2012  
  2013  	dumpBundle := false
  2014  	var accts []stellar1.WalletAccountLocal
  2015  	for _, entry := range bundle.Accounts {
  2016  		acct, err := accountLocal(mctx, remoter, entry)
  2017  		if err != nil {
  2018  			if err != remote.ErrAccountIDMissing {
  2019  				return nil, err
  2020  			}
  2021  			mctx.Debug("bundle entry has empty account id: %+v", entry)
  2022  			dumpBundle = true // log the full bundle later
  2023  
  2024  			// skip this entry
  2025  			continue
  2026  		}
  2027  
  2028  		if acct.AccountID.IsNil() {
  2029  			mctx.Debug("accountLocal for entry %+v returned nil account id", entry)
  2030  		}
  2031  
  2032  		accts = append(accts, acct)
  2033  	}
  2034  
  2035  	if dumpBundle {
  2036  		mctx.Debug("Full bundle: %+v", bundle)
  2037  	}
  2038  
  2039  	// Put the primary account first, then sort by name, then by account ID
  2040  	sort.SliceStable(accts, func(i, j int) bool {
  2041  		if accts[i].IsDefault {
  2042  			return true
  2043  		}
  2044  		if accts[j].IsDefault {
  2045  			return false
  2046  		}
  2047  		if accts[i].Name == accts[j].Name {
  2048  			return accts[i].AccountID < accts[j].AccountID
  2049  		}
  2050  		return accts[i].Name < accts[j].Name
  2051  	})
  2052  
  2053  	// debugging empty account id
  2054  	mctx.Debug("AllWalletAccounts returning %d accounts:", len(accts))
  2055  	for i, a := range accts {
  2056  		mctx.Debug("%d: %q (default: %v)", i, a.AccountID, a.IsDefault)
  2057  		if a.AccountID.IsNil() {
  2058  			mctx.Debug("%d: account id is empty (%+v) !!!!!!", a)
  2059  		}
  2060  	}
  2061  
  2062  	return accts, nil
  2063  }
  2064  
  2065  // WalletAccount returns stellar1.WalletAccountLocal for accountID.
  2066  func WalletAccount(mctx libkb.MetaContext, remoter remote.Remoter, accountID stellar1.AccountID) (stellar1.WalletAccountLocal, error) {
  2067  	bundle, err := remote.FetchSecretlessBundle(mctx)
  2068  	if err != nil {
  2069  		return stellar1.WalletAccountLocal{}, err
  2070  	}
  2071  	entry, err := bundle.Lookup(accountID)
  2072  	if err != nil {
  2073  		return stellar1.WalletAccountLocal{}, err
  2074  	}
  2075  
  2076  	return accountLocal(mctx, remoter, entry)
  2077  }
  2078  
  2079  func accountLocal(mctx libkb.MetaContext, remoter remote.Remoter, entry stellar1.BundleEntry) (stellar1.WalletAccountLocal, error) {
  2080  	var empty stellar1.WalletAccountLocal
  2081  	details, err := AccountDetails(mctx, remoter, entry.AccountID)
  2082  	if err != nil {
  2083  		mctx.Debug("remote.Details failed for %q: %s", entry.AccountID, err)
  2084  		return empty, err
  2085  	}
  2086  
  2087  	if details.AccountID.IsNil() {
  2088  		mctx.Debug("AccountDetails for entry.AccountID %q returned empty account id (full details: %+v)", entry.AccountID, details)
  2089  	}
  2090  
  2091  	return AccountDetailsToWalletAccountLocal(mctx, entry.AccountID, details, entry.IsPrimary, entry.Name, entry.Mode)
  2092  }
  2093  
  2094  // AccountDetails gets stellar1.AccountDetails for accountID.
  2095  //
  2096  // It has the side effect of updating the badge state with the
  2097  // stellar payment unread count for accountID.
  2098  func AccountDetails(mctx libkb.MetaContext, remoter remote.Remoter, accountID stellar1.AccountID) (stellar1.AccountDetails, error) {
  2099  	details, err := remoter.Details(mctx.Ctx(), accountID)
  2100  	details.SetDefaultDisplayCurrency()
  2101  	if err != nil {
  2102  		return details, err
  2103  	}
  2104  
  2105  	err = mctx.G().GetStellar().UpdateUnreadCount(mctx.Ctx(), accountID, details.UnreadPayments)
  2106  	if err != nil {
  2107  		mctx.Debug("AccountDetails UpdateUnreadCount error: %s", err)
  2108  	}
  2109  
  2110  	return details, nil
  2111  }
  2112  
  2113  func AirdropStatus(mctx libkb.MetaContext) (stellar1.AirdropStatus, error) {
  2114  	apiStatus, err := remote.AirdropStatus(mctx)
  2115  	if err != nil {
  2116  		return stellar1.AirdropStatus{}, err
  2117  	}
  2118  	return TransformToAirdropStatus(apiStatus), nil
  2119  }
  2120  
  2121  func FindPaymentPath(mctx libkb.MetaContext, remoter remote.Remoter, source stellar1.AccountID, to string, sourceAsset, destinationAsset stellar1.Asset, amount string) (stellar1.PaymentPath, error) {
  2122  	recipient, err := LookupRecipient(mctx, stellarcommon.RecipientInput(to), false)
  2123  	if err != nil {
  2124  		return stellar1.PaymentPath{}, err
  2125  	}
  2126  	if recipient.AccountID == nil {
  2127  		return stellar1.PaymentPath{}, errors.New("cannot send a path payment to a user without a stellar account")
  2128  	}
  2129  
  2130  	sourceEntry, _, err := LookupSender(mctx, source)
  2131  	if err != nil {
  2132  		return stellar1.PaymentPath{}, err
  2133  	}
  2134  
  2135  	query := stellar1.PaymentPathQuery{
  2136  		Source:           sourceEntry.AccountID,
  2137  		Destination:      stellar1.AccountID(recipient.AccountID.String()),
  2138  		SourceAsset:      sourceAsset,
  2139  		DestinationAsset: destinationAsset,
  2140  		Amount:           amount,
  2141  	}
  2142  	return remoter.FindPaymentPath(mctx, query)
  2143  }
  2144  
  2145  func FuzzyAssetSearch(mctx libkb.MetaContext, remoter remote.Remoter, arg stellar1.FuzzyAssetSearchArg) ([]stellar1.Asset, error) {
  2146  	return remoter.FuzzyAssetSearch(mctx, arg)
  2147  }
  2148  
  2149  func ListPopularAssets(mctx libkb.MetaContext, remoter remote.Remoter, arg stellar1.ListPopularAssetsArg) (stellar1.AssetListResult, error) {
  2150  	return remoter.ListPopularAssets(mctx, arg)
  2151  }