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

     1  package stellarsvc
     2  
     3  import (
     4  	"context"
     5  	"encoding/base64"
     6  	"encoding/json"
     7  	"errors"
     8  	"fmt"
     9  	"math"
    10  	"net/http"
    11  	"net/url"
    12  	"sort"
    13  
    14  	"github.com/keybase/client/go/libkb"
    15  	"github.com/keybase/client/go/protocol/stellar1"
    16  	"github.com/keybase/client/go/stellar"
    17  	"github.com/keybase/client/go/stellar/remote"
    18  	"github.com/keybase/client/go/stellar/stellarcommon"
    19  	"github.com/keybase/stellarnet"
    20  	"github.com/stellar/go/xdr"
    21  )
    22  
    23  type UISource interface {
    24  	SecretUI(g *libkb.GlobalContext, sessionID int) libkb.SecretUI
    25  	IdentifyUI(g *libkb.GlobalContext, sessionID int) libkb.IdentifyUI
    26  	StellarUI() stellar1.UiInterface
    27  }
    28  
    29  type Server struct {
    30  	libkb.Contextified
    31  	uiSource    UISource
    32  	remoter     remote.Remoter
    33  	walletState *stellar.WalletState
    34  }
    35  
    36  func New(g *libkb.GlobalContext, uiSource UISource, walletState *stellar.WalletState) *Server {
    37  	return &Server{
    38  		Contextified: libkb.NewContextified(g),
    39  		uiSource:     uiSource,
    40  		remoter:      walletState,
    41  		walletState:  walletState,
    42  	}
    43  }
    44  
    45  func (s *Server) assertLoggedIn(mctx libkb.MetaContext) error {
    46  	loggedIn := mctx.ActiveDevice().Valid()
    47  	if !loggedIn {
    48  		return libkb.LoginRequiredError{}
    49  	}
    50  	return nil
    51  }
    52  
    53  func (s *Server) logTag(ctx context.Context) context.Context {
    54  	return libkb.WithLogTag(ctx, "WA")
    55  }
    56  
    57  type preambleArg struct {
    58  	RPCName string
    59  	// Pointer to the RPC's error return value.
    60  	// Can be nil for RPCs that do not err.
    61  	Err            *error
    62  	RequireWallet  bool
    63  	AllowLoggedOut bool
    64  }
    65  
    66  // Preamble
    67  // Example usage:
    68  //
    69  //	ctx, err, fin := c.Preamble(...)
    70  //	defer fin()
    71  //	if err != nil { return err }
    72  func (s *Server) Preamble(inCtx context.Context, opts preambleArg) (mctx libkb.MetaContext, fin func(), err error) {
    73  	mctx = libkb.NewMetaContext(s.logTag(inCtx), s.G())
    74  	fin = mctx.Trace("LRPC "+opts.RPCName, opts.Err)
    75  	if !opts.AllowLoggedOut {
    76  		if err = s.assertLoggedIn(mctx); err != nil {
    77  			return mctx, fin, err
    78  		}
    79  	}
    80  	if opts.RequireWallet {
    81  		cwg, err := stellar.CreateWalletGated(mctx)
    82  		if err != nil {
    83  			return mctx, fin, err
    84  		}
    85  		if !cwg.HasWallet {
    86  			if !cwg.AcceptedDisclaimer {
    87  				// Synthesize an AppStatusError so the CLI and GUI can match on these errors.
    88  				err = libkb.NewAppStatusError(&libkb.AppStatus{
    89  					Code: libkb.SCStellarNeedDisclaimer,
    90  					Name: "STELLAR_NEED_DISCLAIMER",
    91  					Desc: "user hasn't yet accepted the Stellar disclaimer",
    92  				})
    93  				return mctx, fin, err
    94  			}
    95  			return mctx, fin, errors.New("logged-in user does not have a wallet")
    96  		}
    97  	}
    98  	return mctx, fin, nil
    99  }
   100  
   101  func (s *Server) BalancesLocal(ctx context.Context, accountID stellar1.AccountID) (ret []stellar1.Balance, err error) {
   102  	mctx, fin, err := s.Preamble(ctx, preambleArg{
   103  		RPCName: "BalancesLocal",
   104  		Err:     &err,
   105  	})
   106  	defer fin()
   107  	if err != nil {
   108  		return ret, err
   109  	}
   110  
   111  	return s.remoter.Balances(mctx.Ctx(), accountID)
   112  }
   113  
   114  func (s *Server) ImportSecretKeyLocal(ctx context.Context, arg stellar1.ImportSecretKeyLocalArg) (err error) {
   115  	mctx, fin, err := s.Preamble(ctx, preambleArg{
   116  		RPCName:       "ImportSecretKeyLocal",
   117  		Err:           &err,
   118  		RequireWallet: true,
   119  	})
   120  	defer fin()
   121  	if err != nil {
   122  		return err
   123  	}
   124  
   125  	return stellar.ImportSecretKey(mctx, arg.SecretKey, arg.MakePrimary, arg.Name)
   126  }
   127  
   128  func (s *Server) ExportSecretKeyLocal(ctx context.Context, accountID stellar1.AccountID) (res stellar1.SecretKey, err error) {
   129  	mctx, fin, err := s.Preamble(ctx, preambleArg{
   130  		RPCName:       "ExportSecretKeyLocal",
   131  		Err:           &err,
   132  		RequireWallet: true,
   133  	})
   134  	defer fin()
   135  	if err != nil {
   136  		return res, err
   137  	}
   138  
   139  	// Prompt for passphrase
   140  	username := s.G().GetEnv().GetUsername().String()
   141  	arg := libkb.DefaultPassphrasePromptArg(mctx, username)
   142  	arg.Prompt += " to export Stellar secret keys"
   143  	secretUI := s.uiSource.SecretUI(s.G(), 0)
   144  	ppRes, err := secretUI.GetPassphrase(arg, nil)
   145  	if err != nil {
   146  		return res, err
   147  	}
   148  	_, err = libkb.VerifyPassphraseForLoggedInUser(mctx, ppRes.Passphrase)
   149  	if err != nil {
   150  		return res, err
   151  	}
   152  	return stellar.ExportSecretKey(mctx, accountID)
   153  }
   154  
   155  func (s *Server) OwnAccountLocal(ctx context.Context, accountID stellar1.AccountID) (isOwn bool, err error) {
   156  	mctx, fin, err := s.Preamble(ctx, preambleArg{
   157  		RPCName:       "ExportSecretKeyLocal",
   158  		Err:           &err,
   159  		RequireWallet: true,
   160  	})
   161  	defer fin()
   162  	if err != nil {
   163  		return isOwn, err
   164  	}
   165  	isOwn, _, err = stellar.OwnAccount(mctx, accountID)
   166  	return isOwn, err
   167  }
   168  
   169  func (s *Server) SendCLILocal(ctx context.Context, arg stellar1.SendCLILocalArg) (res stellar1.SendResultCLILocal, err error) {
   170  	mctx, fin, err := s.Preamble(ctx, preambleArg{
   171  		RPCName:       "SendCLILocal",
   172  		Err:           &err,
   173  		RequireWallet: true,
   174  	})
   175  	defer fin()
   176  	if err != nil {
   177  		return res, err
   178  	}
   179  
   180  	if !arg.Asset.IsNativeXLM() {
   181  		return res, fmt.Errorf("sending non-XLM assets is not supported")
   182  	}
   183  
   184  	// make sure that the xlm amount is close to the display amount the
   185  	// user thinks they are sending.
   186  	if err = s.checkDisplayAmount(mctx.Ctx(), arg); err != nil {
   187  		return res, err
   188  	}
   189  
   190  	displayBalance := stellar.DisplayBalance{
   191  		Amount:   arg.DisplayAmount,
   192  		Currency: arg.DisplayCurrency,
   193  	}
   194  	uis := libkb.UIs{
   195  		IdentifyUI: s.uiSource.IdentifyUI(s.G(), 0),
   196  	}
   197  	mctx = mctx.WithUIs(uis)
   198  
   199  	memo, err := stellarnet.NewMemoFromStrings(arg.PublicNote, arg.PublicNoteType.String())
   200  	if err != nil {
   201  		return res, err
   202  	}
   203  
   204  	sendRes, err := stellar.SendPaymentCLI(mctx, s.walletState, stellar.SendPaymentArg{
   205  		From:           arg.FromAccountID,
   206  		To:             stellarcommon.RecipientInput(arg.Recipient),
   207  		Amount:         arg.Amount,
   208  		DisplayBalance: displayBalance,
   209  		SecretNote:     arg.Note,
   210  		ForceRelay:     arg.ForceRelay,
   211  		QuickReturn:    false,
   212  		PublicMemo:     memo,
   213  	})
   214  	if err != nil {
   215  		return res, err
   216  	}
   217  	return stellar1.SendResultCLILocal{
   218  		KbTxID: sendRes.KbTxID,
   219  		TxID:   sendRes.TxID,
   220  	}, nil
   221  }
   222  
   223  func (s *Server) AccountMergeCLILocal(ctx context.Context, arg stellar1.AccountMergeCLILocalArg) (res stellar1.TransactionID, err error) {
   224  	mctx, fin, err := s.Preamble(ctx, preambleArg{
   225  		RPCName:       "AccountMergeCLILocal",
   226  		Err:           &err,
   227  		RequireWallet: true,
   228  	})
   229  	defer fin()
   230  	if err != nil {
   231  		return res, err
   232  	}
   233  	uis := libkb.UIs{
   234  		IdentifyUI: s.uiSource.IdentifyUI(s.G(), 0),
   235  	}
   236  	mctx = mctx.WithUIs(uis)
   237  
   238  	primary, err := stellar.GetOwnPrimaryAccountID(mctx)
   239  	if err != nil {
   240  		return res, err
   241  	}
   242  	if arg.FromAccountID == primary {
   243  		return res, fmt.Errorf("cannot merge away your primary account")
   244  	}
   245  	if arg.To == "" {
   246  		// if unspecified, default the target account to the user's primary
   247  		arg.To = primary.String()
   248  	}
   249  
   250  	signRes, err := stellar.AccountMerge(mctx, s.walletState, arg)
   251  	if err != nil {
   252  		mctx.Debug("error building account-merge transaction for %s into %s: %v", arg.FromAccountID, arg.To, err)
   253  		return res, err
   254  	}
   255  	err = s.remoter.PostAnyTransaction(mctx, signRes.Signed)
   256  	if err != nil {
   257  		mctx.Debug("error posting account-merge transaction for %s into %s: %v", arg.FromAccountID, arg.To, err)
   258  		return res, err
   259  	}
   260  	mctx.Debug("posted account merge transaction for %s into %s", arg.FromAccountID, arg.To)
   261  	err = s.walletState.RefreshAll(mctx, "account merge")
   262  	if err != nil {
   263  		mctx.Debug("error refreshing accounts after successfully processing a merge")
   264  	}
   265  	return stellar1.TransactionID(signRes.TxHash), nil
   266  }
   267  
   268  func (s *Server) SendPathCLILocal(ctx context.Context, arg stellar1.SendPathCLILocalArg) (res stellar1.SendResultCLILocal, err error) {
   269  	mctx, fin, err := s.Preamble(ctx, preambleArg{
   270  		RPCName:       "SendPathCLILocal",
   271  		Err:           &err,
   272  		RequireWallet: true,
   273  	})
   274  	defer fin()
   275  	if err != nil {
   276  		return res, err
   277  	}
   278  
   279  	uis := libkb.UIs{
   280  		IdentifyUI: s.uiSource.IdentifyUI(s.G(), 0),
   281  	}
   282  	mctx = mctx.WithUIs(uis)
   283  
   284  	memo, err := stellarnet.NewMemoFromStrings(arg.PublicNote, arg.PublicNoteType.String())
   285  	if err != nil {
   286  		return res, err
   287  	}
   288  
   289  	sendRes, err := stellar.SendPathPaymentCLI(mctx, s.walletState, stellar.SendPathPaymentArg{
   290  		From:        arg.Source,
   291  		To:          stellarcommon.RecipientInput(arg.Recipient),
   292  		Path:        arg.Path,
   293  		SecretNote:  arg.Note,
   294  		PublicMemo:  memo,
   295  		QuickReturn: false,
   296  	})
   297  	if err != nil {
   298  		return res, err
   299  	}
   300  	return stellar1.SendResultCLILocal{
   301  		KbTxID: sendRes.KbTxID,
   302  		TxID:   sendRes.TxID,
   303  	}, nil
   304  }
   305  
   306  func (s *Server) ClaimCLILocal(ctx context.Context, arg stellar1.ClaimCLILocalArg) (res stellar1.RelayClaimResult, err error) {
   307  	mctx, fin, err := s.Preamble(ctx, preambleArg{
   308  		RPCName:       "ClaimCLILocal",
   309  		Err:           &err,
   310  		RequireWallet: true,
   311  	})
   312  	defer fin()
   313  	if err != nil {
   314  		return res, err
   315  	}
   316  
   317  	var into stellar1.AccountID
   318  	if arg.Into != nil {
   319  		into = *arg.Into
   320  	} else {
   321  		// Default to claiming into the user's primary wallet.
   322  		into, err = stellar.GetOwnPrimaryAccountID(mctx)
   323  		if err != nil {
   324  			return res, err
   325  		}
   326  	}
   327  	return stellar.Claim(mctx, s.walletState, arg.TxID, into, nil, nil)
   328  }
   329  
   330  func (s *Server) RecentPaymentsCLILocal(ctx context.Context, accountID *stellar1.AccountID) (res []stellar1.PaymentOrErrorCLILocal, err error) {
   331  	mctx, fin, err := s.Preamble(ctx, preambleArg{
   332  		RPCName:       "RecentPaymentsCLILocal",
   333  		Err:           &err,
   334  		RequireWallet: true,
   335  	})
   336  	defer fin()
   337  	if err != nil {
   338  		return nil, err
   339  	}
   340  
   341  	var selectAccountID stellar1.AccountID
   342  	if accountID == nil {
   343  		selectAccountID, err = stellar.GetOwnPrimaryAccountID(mctx)
   344  		if err != nil {
   345  			return nil, err
   346  		}
   347  	} else {
   348  		selectAccountID = *accountID
   349  	}
   350  	return stellar.RecentPaymentsCLILocal(mctx, s.remoter, selectAccountID)
   351  }
   352  
   353  func (s *Server) PaymentDetailCLILocal(ctx context.Context, txID string) (res stellar1.PaymentCLILocal, err error) {
   354  	mctx, fin, err := s.Preamble(ctx, preambleArg{
   355  		RPCName: "PaymentDetailCLILocal",
   356  		Err:     &err,
   357  	})
   358  	defer fin()
   359  	if err != nil {
   360  		return res, err
   361  	}
   362  
   363  	return stellar.PaymentDetailCLILocal(mctx.Ctx(), s.G(), s.remoter, txID)
   364  }
   365  
   366  // WalletInitLocal creates and posts an initial stellar bundle for a user.
   367  // Only succeeds if they do not already have one.
   368  // Safe to call even if the user has a bundle already.
   369  func (s *Server) WalletInitLocal(ctx context.Context) (err error) {
   370  	mctx, fin, err := s.Preamble(ctx, preambleArg{
   371  		RPCName: "WalletInitLocal",
   372  		Err:     &err,
   373  	})
   374  	defer fin()
   375  	if err != nil {
   376  		return err
   377  	}
   378  
   379  	_, err = stellar.CreateWallet(mctx)
   380  	return err
   381  }
   382  
   383  func (s *Server) SetDisplayCurrency(ctx context.Context, arg stellar1.SetDisplayCurrencyArg) (err error) {
   384  	mctx, fin, err := s.Preamble(ctx, preambleArg{
   385  		RPCName:       fmt.Sprintf("SetDisplayCurrency(%s, %s)", arg.AccountID, arg.Currency),
   386  		Err:           &err,
   387  		RequireWallet: true,
   388  	})
   389  	defer fin()
   390  	if err != nil {
   391  		return err
   392  	}
   393  
   394  	return remote.SetAccountDefaultCurrency(mctx.Ctx(), s.G(), arg.AccountID, arg.Currency)
   395  }
   396  
   397  type exchangeRateMap map[string]stellar1.OutsideExchangeRate
   398  
   399  // getLocalCurrencyAndExchangeRate gets display currency setting
   400  // for accountID and fetches exchange rate is set.
   401  //
   402  // Arguments `account` and `exchangeRates` may end up mutated.
   403  func getLocalCurrencyAndExchangeRate(mctx libkb.MetaContext, remoter remote.Remoter, account *stellar1.OwnAccountCLILocal, exchangeRates exchangeRateMap) error {
   404  	displayCurrency, err := stellar.GetAccountDisplayCurrency(mctx, account.AccountID)
   405  	if err != nil {
   406  		return err
   407  	}
   408  	rate, ok := exchangeRates[displayCurrency]
   409  	if !ok {
   410  		var err error
   411  		rate, err = remoter.ExchangeRate(mctx.Ctx(), displayCurrency)
   412  		if err != nil {
   413  			return err
   414  		}
   415  		exchangeRates[displayCurrency] = rate
   416  	}
   417  	account.ExchangeRate = &rate
   418  	return nil
   419  }
   420  
   421  func (s *Server) WalletGetAccountsCLILocal(ctx context.Context) (ret []stellar1.OwnAccountCLILocal, err error) {
   422  	mctx, fin, err := s.Preamble(ctx, preambleArg{
   423  		RPCName:       "WalletGetAccountsCLILocal",
   424  		Err:           &err,
   425  		RequireWallet: true,
   426  	})
   427  	defer fin()
   428  	if err != nil {
   429  		return ret, err
   430  	}
   431  
   432  	currentBundle, err := remote.FetchSecretlessBundle(mctx)
   433  	if err != nil {
   434  		return nil, err
   435  	}
   436  
   437  	var accountError error
   438  	exchangeRates := make(exchangeRateMap)
   439  	for _, account := range currentBundle.Accounts {
   440  		accID := account.AccountID
   441  		acc := stellar1.OwnAccountCLILocal{
   442  			AccountID:   accID,
   443  			IsPrimary:   account.IsPrimary,
   444  			Name:        account.Name,
   445  			AccountMode: account.Mode,
   446  		}
   447  
   448  		balances, err := s.remoter.Balances(ctx, accID)
   449  		if err != nil {
   450  			accountError = err
   451  			s.G().Log.Warning("Could not load balance for %q", accID)
   452  			continue
   453  		}
   454  
   455  		acc.Balance = balances
   456  
   457  		if err := getLocalCurrencyAndExchangeRate(mctx, s.remoter, &acc, exchangeRates); err != nil {
   458  			s.G().Log.Warning("Could not load local currency exchange rate for %q", accID)
   459  		}
   460  
   461  		ret = append(ret, acc)
   462  	}
   463  
   464  	// Put the primary account first, then sort by name, then by account ID
   465  	sort.SliceStable(ret, func(i, j int) bool {
   466  		if ret[i].IsPrimary {
   467  			return true
   468  		}
   469  		if ret[j].IsPrimary {
   470  			return false
   471  		}
   472  		if ret[i].Name == ret[j].Name {
   473  			return ret[i].AccountID < ret[j].AccountID
   474  		}
   475  		return ret[i].Name < ret[j].Name
   476  	})
   477  
   478  	return ret, accountError
   479  }
   480  
   481  func (s *Server) ExchangeRateLocal(ctx context.Context, currency stellar1.OutsideCurrencyCode) (res stellar1.OutsideExchangeRate, err error) {
   482  	mctx, fin, err := s.Preamble(ctx, preambleArg{
   483  		RPCName:        fmt.Sprintf("ExchangeRateLocal(%s)", string(currency)),
   484  		Err:            &err,
   485  		AllowLoggedOut: true,
   486  	})
   487  	defer fin()
   488  	if err != nil {
   489  		return res, err
   490  	}
   491  
   492  	return s.remoter.ExchangeRate(mctx.Ctx(), string(currency))
   493  }
   494  
   495  func (s *Server) GetAvailableLocalCurrencies(ctx context.Context) (ret map[stellar1.OutsideCurrencyCode]stellar1.OutsideCurrencyDefinition, err error) {
   496  	mctx, fin, err := s.Preamble(ctx, preambleArg{
   497  		RPCName:        "GetAvailableLocalCurrencies",
   498  		Err:            &err,
   499  		AllowLoggedOut: true,
   500  	})
   501  	defer fin()
   502  	if err != nil {
   503  		return ret, err
   504  	}
   505  
   506  	conf, err := s.G().GetStellar().GetServerDefinitions(mctx.Ctx())
   507  	if err != nil {
   508  		return ret, err
   509  	}
   510  	return conf.Currencies, nil
   511  }
   512  
   513  func (s *Server) FormatLocalCurrencyString(ctx context.Context, arg stellar1.FormatLocalCurrencyStringArg) (res string, err error) {
   514  	mctx, fin, err := s.Preamble(ctx, preambleArg{
   515  		RPCName:        "FormatLocalCurrencyString",
   516  		Err:            &err,
   517  		AllowLoggedOut: true,
   518  	})
   519  	defer fin()
   520  	if err != nil {
   521  		return res, err
   522  	}
   523  
   524  	return stellar.FormatCurrency(mctx, arg.Amount, arg.Code, stellarnet.Round)
   525  }
   526  
   527  // check that the display amount is within 1% of current exchange rates
   528  func (s *Server) checkDisplayAmount(ctx context.Context, arg stellar1.SendCLILocalArg) error {
   529  	if arg.DisplayAmount == "" {
   530  		return nil
   531  	}
   532  
   533  	exchangeRate, err := s.remoter.ExchangeRate(ctx, arg.DisplayCurrency)
   534  	if err != nil {
   535  		return err
   536  	}
   537  
   538  	xlmAmount, err := stellarnet.ConvertOutsideToXLM(arg.DisplayAmount, exchangeRate.Rate)
   539  	if err != nil {
   540  		return err
   541  	}
   542  
   543  	currentAmt, err := stellarnet.ParseStellarAmount(xlmAmount)
   544  	if err != nil {
   545  		return err
   546  	}
   547  
   548  	argAmt, err := stellarnet.ParseStellarAmount(arg.Amount)
   549  	if err != nil {
   550  		return err
   551  	}
   552  
   553  	if percentageAmountChange(currentAmt, argAmt) > 1.0 {
   554  		s.G().Log.CDebugf(ctx, "large exchange rate delta: argAmt: %d, currentAmt: %d", argAmt, currentAmt)
   555  		return errors.New("current exchange rates have changed more than 1%")
   556  	}
   557  
   558  	return nil
   559  }
   560  
   561  func (s *Server) MakeRequestCLILocal(ctx context.Context, arg stellar1.MakeRequestCLILocalArg) (res stellar1.KeybaseRequestID, err error) {
   562  	mctx, fin, err := s.Preamble(ctx, preambleArg{
   563  		RPCName:       "MakeRequestCLILocal",
   564  		Err:           &err,
   565  		RequireWallet: true,
   566  	})
   567  	defer fin()
   568  	if err != nil {
   569  		return "", err
   570  	}
   571  
   572  	uis := libkb.UIs{
   573  		IdentifyUI: s.uiSource.IdentifyUI(s.G(), 0),
   574  	}
   575  	mctx = mctx.WithUIs(uis)
   576  
   577  	return stellar.MakeRequestCLI(mctx, s.remoter, stellar.MakeRequestArg{
   578  		To:       stellarcommon.RecipientInput(arg.Recipient),
   579  		Amount:   arg.Amount,
   580  		Asset:    arg.Asset,
   581  		Currency: arg.Currency,
   582  		Note:     arg.Note,
   583  	})
   584  }
   585  
   586  func (s *Server) LookupCLILocal(ctx context.Context, arg string) (res stellar1.LookupResultCLILocal, err error) {
   587  	mctx, fin, err := s.Preamble(ctx, preambleArg{
   588  		RPCName:        "LookupCLILocal",
   589  		Err:            &err,
   590  		RequireWallet:  false,
   591  		AllowLoggedOut: true,
   592  	})
   593  	defer fin()
   594  	if err != nil {
   595  		return res, err
   596  	}
   597  
   598  	uis := libkb.UIs{
   599  		IdentifyUI: s.uiSource.IdentifyUI(s.G(), 0),
   600  	}
   601  	mctx = mctx.WithUIs(uis)
   602  
   603  	recipient, err := stellar.LookupRecipient(mctx, stellarcommon.RecipientInput(arg), true)
   604  	if err != nil {
   605  		return res, err
   606  	}
   607  	if recipient.AccountID != nil {
   608  		// Lookup Account ID -> User
   609  		uv, username, err := stellar.LookupUserByAccountID(mctx, stellar1.AccountID(recipient.AccountID.String()))
   610  		if err == nil {
   611  			recipient.User = &stellarcommon.User{
   612  				UV:       uv,
   613  				Username: username,
   614  			}
   615  		}
   616  	}
   617  	if recipient.AccountID == nil {
   618  		if recipient.User != nil {
   619  			return res, fmt.Errorf("Keybase user %q does not have a Stellar account", recipient.User.Username)
   620  		} else if recipient.Assertion != nil {
   621  			return res, fmt.Errorf("Could not resolve assertion %q", *recipient.Assertion)
   622  		}
   623  		return res, fmt.Errorf("Could not find a Stellar account for %q", recipient.Input)
   624  	}
   625  	res.AccountID = stellar1.AccountID(*recipient.AccountID)
   626  	if recipient.User != nil {
   627  		u := recipient.User.Username.String()
   628  		res.Username = &u
   629  	}
   630  	return res, nil
   631  }
   632  
   633  func (s *Server) BatchLocal(ctx context.Context, arg stellar1.BatchLocalArg) (res stellar1.BatchResultLocal, err error) {
   634  	mctx, fin, err := s.Preamble(ctx, preambleArg{
   635  		RPCName:        "BatchLocal",
   636  		Err:            &err,
   637  		RequireWallet:  true,
   638  		AllowLoggedOut: false,
   639  	})
   640  	defer fin()
   641  	if err != nil {
   642  		return res, err
   643  	}
   644  
   645  	if arg.UseMulti {
   646  		res, err = stellar.BatchMulti(mctx, s.walletState, arg)
   647  		if err == nil {
   648  			return res, nil
   649  		}
   650  
   651  		if err == stellar.ErrRelayinMultiBatch {
   652  			mctx.Debug("found relay recipient in BatchMulti, using standard Batch instead")
   653  			return stellar.Batch(mctx, s.walletState, arg)
   654  		}
   655  
   656  		return res, err
   657  	}
   658  
   659  	return stellar.Batch(mctx, s.walletState, arg)
   660  }
   661  
   662  func (s *Server) ValidateStellarURILocal(ctx context.Context, arg stellar1.ValidateStellarURILocalArg) (res stellar1.ValidateStellarURIResultLocal, err error) {
   663  	mctx, fin, err := s.Preamble(ctx, preambleArg{
   664  		RPCName: "ValidateStellarURILocal",
   665  		Err:     &err,
   666  	})
   667  	defer fin()
   668  	if err != nil {
   669  		return stellar1.ValidateStellarURIResultLocal{}, err
   670  	}
   671  
   672  	vp, _, err := s.validateStellarURI(mctx, arg.InputURI, http.DefaultClient)
   673  	if err != nil {
   674  		return stellar1.ValidateStellarURIResultLocal{}, err
   675  	}
   676  	return *vp, nil
   677  }
   678  
   679  const zeroSourceAccount = "00000000000000000000000000000000000000000000000000000000"
   680  
   681  func (s *Server) validateStellarURI(mctx libkb.MetaContext, uri string, getter stellarnet.HTTPGetter) (*stellar1.ValidateStellarURIResultLocal, *stellarnet.ValidatedStellarURI, error) {
   682  	validated, err := stellarnet.ValidateStellarURI(uri, getter)
   683  	if err != nil {
   684  		switch err.(type) {
   685  		case stellarnet.ErrNetworkWellKnownOrigin, stellarnet.ErrInvalidWellKnownOrigin:
   686  			// format these errors a little nicer for frontend to use directly
   687  			domain, xerr := stellarnet.UnvalidatedStellarURIOriginDomain(uri)
   688  			if xerr == nil {
   689  				return nil, nil, fmt.Errorf("This Stellar link claims to be signed by %s, but the Keybase app cannot currently verify the signature came from %s. Sorry, there's nothing you can do with this Stellar link.", domain, domain)
   690  			}
   691  		}
   692  		return nil, nil, err
   693  	}
   694  
   695  	if validated.UnknownReplaceFields {
   696  		return nil, nil, errors.New("This Stellar link is requesting replacements on fields in the transaction that Keybase does not handle. Sorry, there's nothing you can do with this Stellar link.")
   697  	}
   698  
   699  	local := stellar1.ValidateStellarURIResultLocal{
   700  		Operation:    validated.Operation,
   701  		OriginDomain: validated.OriginDomain,
   702  		Message:      validated.Message,
   703  		CallbackURL:  validated.CallbackURL,
   704  		Xdr:          validated.XDR,
   705  		Recipient:    validated.Recipient,
   706  		Amount:       validated.Amount,
   707  		AssetCode:    validated.AssetCode,
   708  		AssetIssuer:  validated.AssetIssuer,
   709  		Memo:         validated.Memo,
   710  		MemoType:     validated.MemoType,
   711  		Signed:       validated.Signed,
   712  	}
   713  
   714  	if validated.AssetCode == "" {
   715  		accountID, err := stellar.GetOwnPrimaryAccountID(mctx)
   716  		if err != nil {
   717  			return nil, nil, err
   718  		}
   719  		displayCurrency, err := stellar.GetAccountDisplayCurrency(mctx, accountID)
   720  		if err != nil {
   721  			return nil, nil, err
   722  		}
   723  		rate, err := s.remoter.ExchangeRate(mctx.Ctx(), displayCurrency)
   724  		if err != nil {
   725  			return nil, nil, err
   726  		}
   727  
   728  		if validated.Amount != "" {
   729  			// show how much validate.Amount XLM is in the user's display currency
   730  			outsideAmount, err := stellarnet.ConvertXLMToOutside(validated.Amount, rate.Rate)
   731  			if err != nil {
   732  				return nil, nil, err
   733  			}
   734  			fmtWorth, err := stellar.FormatCurrencyWithCodeSuffix(mctx, outsideAmount, rate.Currency, stellarnet.Round)
   735  			if err != nil {
   736  				return nil, nil, err
   737  			}
   738  			local.DisplayAmountFiat = fmtWorth
   739  		}
   740  
   741  		// include user's XLM available to send
   742  		details, err := s.remoter.Details(mctx.Ctx(), accountID)
   743  		if err != nil {
   744  			return nil, nil, err
   745  		}
   746  		availableXLM := details.Available
   747  		if availableXLM == "" {
   748  			availableXLM = "0"
   749  		}
   750  		fmtAvailableAmountXLM, err := stellar.FormatAmount(mctx, availableXLM, false, stellarnet.Round)
   751  		if err != nil {
   752  			return nil, nil, err
   753  		}
   754  		availableAmount, err := stellarnet.ConvertXLMToOutside(availableXLM, rate.Rate)
   755  		if err != nil {
   756  			return nil, nil, err
   757  		}
   758  		fmtAvailableWorth, err := stellar.FormatCurrencyWithCodeSuffix(mctx, availableAmount, rate.Currency, stellarnet.Round)
   759  		if err != nil {
   760  			return nil, nil, err
   761  		}
   762  		local.AvailableToSendNative = fmtAvailableAmountXLM + " XLM"
   763  		local.AvailableToSendFiat = fmtAvailableWorth
   764  	}
   765  
   766  	if validated.TxEnv != nil {
   767  		tx := validated.TxEnv.Tx
   768  		if !validated.ReplaceSourceAccount && tx.SourceAccount.Address() != "" && tx.SourceAccount.Address() != zeroSourceAccount {
   769  			local.Summary.Source = stellar1.AccountID(tx.SourceAccount.Address())
   770  		}
   771  		local.Summary.Fee = int(tx.Fee)
   772  		local.Summary.Memo, local.Summary.MemoType, err = memoStrings(tx.Memo)
   773  		if err != nil {
   774  			return nil, nil, err
   775  		}
   776  		local.Summary.Operations = make([]string, len(tx.Operations))
   777  		for i, op := range tx.Operations {
   778  			const pastTense = false
   779  			local.Summary.Operations[i] = stellarnet.OpSummary(op, pastTense)
   780  		}
   781  	}
   782  
   783  	return &local, validated, nil
   784  }
   785  
   786  func (s *Server) ApproveTxURILocal(ctx context.Context, arg stellar1.ApproveTxURILocalArg) (txID stellar1.TransactionID, err error) {
   787  	mctx, fin, err := s.Preamble(ctx, preambleArg{
   788  		RPCName: "ApproveTxURILocal",
   789  		Err:     &err,
   790  	})
   791  	defer fin()
   792  	if err != nil {
   793  		return "", err
   794  	}
   795  
   796  	// revalidate the URI
   797  	vp, validated, err := s.validateStellarURI(mctx, arg.InputURI, http.DefaultClient)
   798  	if err != nil {
   799  		return "", err
   800  	}
   801  
   802  	txEnv := validated.TxEnv
   803  	if txEnv == nil {
   804  		return "", errors.New("no tx envelope in URI")
   805  	}
   806  
   807  	if validated.ReplaceSourceAccount || vp.Summary.Source == "" {
   808  		// need to fill in SourceAccount
   809  		accountID, err := stellar.GetOwnPrimaryAccountID(mctx)
   810  		if err != nil {
   811  			return "", err
   812  		}
   813  		address, err := stellarnet.NewAddressStr(accountID.String())
   814  		if err != nil {
   815  			return "", err
   816  		}
   817  		txEnv.Tx.SourceAccount, err = address.AccountID()
   818  		if err != nil {
   819  			return "", err
   820  		}
   821  	}
   822  
   823  	if txEnv.Tx.SeqNum == 0 || validated.ReplaceSeqnum {
   824  		// need to fill in SeqNum
   825  		sp, unlock := stellar.NewSeqnoProvider(mctx, s.walletState)
   826  		defer unlock()
   827  
   828  		txEnv.Tx.SeqNum, err = sp.SequenceForAccount(txEnv.Tx.SourceAccount.Address())
   829  		if err != nil {
   830  			return "", err
   831  		}
   832  
   833  		// need to bump the seqno:
   834  		txEnv.Tx.SeqNum++
   835  	}
   836  
   837  	// sign it
   838  	_, seed, err := stellar.LookupSenderSeed(mctx)
   839  	if err != nil {
   840  		return "", err
   841  	}
   842  	sig, err := stellarnet.SignEnvelope(seed, *txEnv)
   843  	if err != nil {
   844  		return "", err
   845  	}
   846  
   847  	if vp.CallbackURL == "" {
   848  		_, err := stellarnet.Submit(sig.Signed)
   849  		if err != nil {
   850  			return "", err
   851  		}
   852  	} else if err := postXDRToCallback(sig.Signed, vp.CallbackURL); err != nil {
   853  		return "", err
   854  	}
   855  
   856  	return stellar1.TransactionID(sig.TxHash), nil
   857  }
   858  
   859  func (s *Server) ApprovePayURILocal(ctx context.Context, arg stellar1.ApprovePayURILocalArg) (txID stellar1.TransactionID, err error) {
   860  	mctx, fin, err := s.Preamble(ctx, preambleArg{
   861  		RPCName: "ApprovePayURILocal",
   862  		Err:     &err,
   863  	})
   864  	defer fin()
   865  	if err != nil {
   866  		return "", err
   867  	}
   868  
   869  	// revalidate the URI
   870  	vp, validated, err := s.validateStellarURI(mctx, arg.InputURI, http.DefaultClient)
   871  	if err != nil {
   872  		return "", err
   873  	}
   874  
   875  	if vp.AssetCode != "" || vp.AssetIssuer != "" {
   876  		return "", errors.New("URI is requesting a path payment, not an XLM pay operation")
   877  	}
   878  
   879  	if vp.Amount == "" {
   880  		vp.Amount = arg.Amount
   881  	}
   882  	memo, err := validated.MemoExport()
   883  	if err != nil {
   884  		return "", err
   885  	}
   886  
   887  	if vp.CallbackURL != "" {
   888  		recipient, err := stellar.LookupRecipient(mctx, stellarcommon.RecipientInput(vp.Recipient), arg.FromCLI)
   889  		if err != nil {
   890  			return "", err
   891  		}
   892  		if recipient.AccountID == nil {
   893  			return "", errors.New("recipient lookup failed to find an account")
   894  		}
   895  		recipientAddr, err := stellarnet.NewAddressStr(recipient.AccountID.String())
   896  		if err != nil {
   897  			return "", err
   898  		}
   899  
   900  		_, senderSeed, err := stellar.LookupSenderSeed(mctx)
   901  		if err != nil {
   902  			return "", err
   903  		}
   904  
   905  		sp, unlock := stellar.NewSeqnoProvider(mctx, s.walletState)
   906  		defer unlock()
   907  
   908  		baseFee := s.walletState.BaseFee(mctx)
   909  
   910  		sig, err := stellarnet.PaymentXLMTransactionWithMemo(senderSeed, recipientAddr, vp.Amount, memo, sp, nil, baseFee)
   911  		if err != nil {
   912  			return "", err
   913  		}
   914  		if err := postXDRToCallback(sig.Signed, vp.CallbackURL); err != nil {
   915  			return "", err
   916  		}
   917  		return stellar1.TransactionID(sig.TxHash), nil
   918  	}
   919  
   920  	sendArg := stellar.SendPaymentArg{
   921  		To:         stellarcommon.RecipientInput(vp.Recipient),
   922  		Amount:     vp.Amount,
   923  		PublicMemo: memo,
   924  	}
   925  
   926  	var res stellar.SendPaymentResult
   927  	if arg.FromCLI {
   928  		sendArg.QuickReturn = false
   929  		res, err = stellar.SendPaymentCLI(mctx, s.walletState, sendArg)
   930  	} else {
   931  		sendArg.QuickReturn = true
   932  		res, err = stellar.SendPaymentGUI(mctx, s.walletState, sendArg)
   933  	}
   934  	if err != nil {
   935  		return "", err
   936  	}
   937  
   938  	// TODO: handle callback path
   939  
   940  	return res.TxID, nil
   941  }
   942  
   943  func (s *Server) GetPartnerUrlsLocal(ctx context.Context, sessionID int) (res []stellar1.PartnerUrl, err error) {
   944  	mctx, fin, err := s.Preamble(ctx, preambleArg{
   945  		RPCName:        "GetPartnerUrlsLocal",
   946  		Err:            &err,
   947  		AllowLoggedOut: true,
   948  	})
   949  	defer fin()
   950  	if err != nil {
   951  		return nil, err
   952  	}
   953  	// Pull back all of the external_urls, but only look at the partner_urls.
   954  	// To ensure we have flexibility in the future, only type check the objects
   955  	// under the key we care about here.
   956  	entry, err := s.G().GetExternalURLStore().GetLatestEntry(mctx)
   957  	if err != nil {
   958  		return nil, err
   959  	}
   960  	var externalURLs map[string]map[string][]interface{}
   961  	if err := json.Unmarshal([]byte(entry.Entry), &externalURLs); err != nil {
   962  		return nil, err
   963  	}
   964  	externalURLGroups, ok := externalURLs[libkb.ExternalURLsBaseKey]
   965  	if !ok {
   966  		return nil, fmt.Errorf("no external URLs to parse")
   967  	}
   968  	userIsKeybaseAdmin := s.G().Env.GetFeatureFlags().Admin(s.G().GetMyUID())
   969  	for _, asInterface := range externalURLGroups[libkb.ExternalURLsStellarPartners] {
   970  		asData, err := json.Marshal(asInterface)
   971  		if err != nil {
   972  			return nil, err
   973  		}
   974  		var partnerURL stellar1.PartnerUrl
   975  		err = json.Unmarshal(asData, &partnerURL)
   976  		if err != nil {
   977  			return nil, err
   978  		}
   979  		if partnerURL.AdminOnly && !userIsKeybaseAdmin {
   980  			// this external url is intended only to be seen by admins for now
   981  			continue
   982  		}
   983  		res = append(res, partnerURL)
   984  	}
   985  	return res, nil
   986  }
   987  
   988  func (s *Server) ApprovePathURILocal(ctx context.Context, arg stellar1.ApprovePathURILocalArg) (txID stellar1.TransactionID, err error) {
   989  	mctx, fin, err := s.Preamble(ctx, preambleArg{
   990  		RPCName: "ApprovePathURILocal",
   991  		Err:     &err,
   992  	})
   993  	defer fin()
   994  	if err != nil {
   995  		return "", err
   996  	}
   997  
   998  	// revalidate the URI
   999  	vp, validated, err := s.validateStellarURI(mctx, arg.InputURI, http.DefaultClient)
  1000  	if err != nil {
  1001  		return "", err
  1002  	}
  1003  
  1004  	memo, err := validated.MemoExport()
  1005  	if err != nil {
  1006  		return "", err
  1007  	}
  1008  
  1009  	sendArg := stellar.SendPathPaymentArg{
  1010  		To:         stellarcommon.RecipientInput(vp.Recipient),
  1011  		Path:       arg.FullPath,
  1012  		PublicMemo: memo,
  1013  	}
  1014  
  1015  	if vp.CallbackURL != "" {
  1016  		sig, _, _, err := stellar.PathPaymentTx(mctx, s.walletState, sendArg)
  1017  		if err != nil {
  1018  			return "", err
  1019  		}
  1020  		if err := postXDRToCallback(sig.Signed, vp.CallbackURL); err != nil {
  1021  			return "", err
  1022  		}
  1023  		return stellar1.TransactionID(sig.TxHash), nil
  1024  	}
  1025  
  1026  	var res stellar.SendPaymentResult
  1027  	if arg.FromCLI {
  1028  		sendArg.QuickReturn = false
  1029  		res, err = stellar.SendPathPaymentCLI(mctx, s.walletState, sendArg)
  1030  	} else {
  1031  		sendArg.QuickReturn = true
  1032  		res, err = stellar.SendPathPaymentGUI(mctx, s.walletState, sendArg)
  1033  	}
  1034  	if err != nil {
  1035  		return "", err
  1036  	}
  1037  
  1038  	return res.TxID, nil
  1039  }
  1040  
  1041  func (s *Server) SignTransactionXdrLocal(ctx context.Context, arg stellar1.SignTransactionXdrLocalArg) (res stellar1.SignXdrResult, err error) {
  1042  	mctx, fin, err := s.Preamble(ctx, preambleArg{
  1043  		RPCName:       "SignTransactionXdrLocal",
  1044  		Err:           &err,
  1045  		RequireWallet: true,
  1046  	})
  1047  	defer fin()
  1048  	if err != nil {
  1049  		return res, err
  1050  	}
  1051  
  1052  	unpackedTx, txIDPrecalc, err := unpackTx(arg.EnvelopeXdr)
  1053  	if err != nil {
  1054  		return res, err
  1055  	}
  1056  
  1057  	var accountID stellar1.AccountID
  1058  	if arg.AccountID == nil {
  1059  		// Derive signer account id from transaction's sourceAccount.
  1060  		accountID = stellar1.AccountID(unpackedTx.Tx.SourceAccount.Address())
  1061  		mctx.Debug("Trying to sign with SourceAccount: %s", accountID.String())
  1062  	} else {
  1063  		// We were provided with specific AccountID we want to sign with.
  1064  		accountID = *arg.AccountID
  1065  		mctx.Debug("Trying to sign with (passed as argument): %s", accountID.String())
  1066  	}
  1067  
  1068  	_, acctBundle, err := stellar.LookupSender(mctx, accountID)
  1069  	if err != nil {
  1070  		return res, err
  1071  	}
  1072  
  1073  	senderSeed, err := stellarnet.NewSeedStr(acctBundle.Signers[0].SecureNoLogString())
  1074  	if err != nil {
  1075  		return res, err
  1076  	}
  1077  
  1078  	signRes, err := stellarnet.SignEnvelope(senderSeed, unpackedTx)
  1079  	if err != nil {
  1080  		return res, err
  1081  	}
  1082  
  1083  	res.SingedTx = signRes.Signed
  1084  	res.AccountID = accountID
  1085  
  1086  	if arg.Submit {
  1087  		submitErr := s.remoter.PostAnyTransaction(mctx, signRes.Signed)
  1088  		if submitErr != nil {
  1089  			errStr := submitErr.Error()
  1090  			mctx.Debug("Submit failed with: %s\n", errStr)
  1091  			res.SubmitErr = &errStr
  1092  		} else {
  1093  			txID := stellar1.TransactionID(txIDPrecalc)
  1094  			mctx.Debug("Submit successful. Tx ID is: %s", txID.String())
  1095  			res.SubmitTxID = &txID
  1096  		}
  1097  	}
  1098  
  1099  	return res, nil
  1100  }
  1101  
  1102  func postXDRToCallback(signed, callbackURL string) error {
  1103  	u, err := url.Parse(callbackURL)
  1104  	if err != nil {
  1105  		return err
  1106  	}
  1107  
  1108  	// take any values that are in the URL
  1109  	values := u.Query()
  1110  	// remove the RawQuery so we can POST them all as a form
  1111  	u.RawQuery = ""
  1112  
  1113  	// put the signed tx in the values
  1114  	values.Set("xdr", signed)
  1115  
  1116  	// POST it
  1117  	_, err = http.PostForm(callbackURL, values)
  1118  	return err
  1119  }
  1120  
  1121  func percentageAmountChange(a, b int64) float64 {
  1122  	if a == 0 && b == 0 {
  1123  		return 0.0
  1124  	}
  1125  	mid := 0.5 * float64(a+b)
  1126  	return math.Abs(100.0 * float64(a-b) / mid)
  1127  }
  1128  
  1129  func memoStrings(x xdr.Memo) (string, string, error) {
  1130  	switch x.Type {
  1131  	case xdr.MemoTypeMemoNone:
  1132  		return "", "MEMO_NONE", nil
  1133  	case xdr.MemoTypeMemoText:
  1134  		return x.MustText(), "MEMO_TEXT", nil
  1135  	case xdr.MemoTypeMemoId:
  1136  		return fmt.Sprintf("%d", x.MustId()), "MEMO_ID", nil
  1137  	case xdr.MemoTypeMemoHash:
  1138  		hash := x.MustHash()
  1139  		return base64.StdEncoding.EncodeToString(hash[:]), "MEMO_HASH", nil
  1140  	case xdr.MemoTypeMemoReturn:
  1141  		hash := x.MustRetHash()
  1142  		return base64.StdEncoding.EncodeToString(hash[:]), "MEMO_RETURN", nil
  1143  	default:
  1144  		return "", "", errors.New("invalid memo type")
  1145  	}
  1146  }
  1147  
  1148  func unpackTx(envelopeXdr string) (unpackedTx xdr.TransactionEnvelope, txIDPrecalc string, err error) {
  1149  	err = xdr.SafeUnmarshalBase64(envelopeXdr, &unpackedTx)
  1150  	if err != nil {
  1151  		return unpackedTx, txIDPrecalc, fmt.Errorf("decoding tx: %v", err)
  1152  	}
  1153  	txIDPrecalc, err = stellarnet.HashTx(unpackedTx.Tx)
  1154  	return unpackedTx, txIDPrecalc, err
  1155  }