github.com/keybase/client/go@v0.0.0-20241007131713-f10651d043c8/stellar/stellarsvc/remote_mock_test.go (about)

     1  package stellarsvc
     2  
     3  import (
     4  	"context"
     5  	"encoding/base64"
     6  	"encoding/hex"
     7  	"errors"
     8  	"fmt"
     9  	"strconv"
    10  	"sync"
    11  	"testing"
    12  	"time"
    13  
    14  	"github.com/keybase/client/go/libkb"
    15  	"github.com/keybase/client/go/protocol/keybase1"
    16  	"github.com/keybase/client/go/protocol/stellar1"
    17  	"github.com/keybase/client/go/stellar/remote"
    18  	"github.com/keybase/stellarnet"
    19  	"github.com/stellar/go/keypair"
    20  	"github.com/stellar/go/xdr"
    21  
    22  	"github.com/stretchr/testify/require"
    23  )
    24  
    25  type txlogger struct {
    26  	transactions []stellar1.PaymentDetails
    27  	sync.Mutex
    28  	T testing.TB
    29  }
    30  
    31  func newTxLogger(t testing.TB) *txlogger { return &txlogger{T: t} }
    32  
    33  func (t *txlogger) Add(tx stellar1.PaymentDetails) {
    34  	t.Lock()
    35  	defer t.Unlock()
    36  	t.transactions = append([]stellar1.PaymentDetails{tx}, t.transactions...)
    37  }
    38  
    39  func (t *txlogger) AddClaim(kbTxID stellar1.KeybaseTransactionID, c stellar1.ClaimSummary) {
    40  	t.Lock()
    41  	defer t.Unlock()
    42  	for i := range t.transactions {
    43  		p := &t.transactions[i]
    44  		typ, err := p.Summary.Typ()
    45  		require.NoError(t.T, err)
    46  		if typ != stellar1.PaymentSummaryType_RELAY {
    47  			continue
    48  		}
    49  		if !p.Summary.Relay().KbTxID.Eq(kbTxID) {
    50  			continue
    51  		}
    52  		p.Summary.Relay__.Claim = &c
    53  		return
    54  	}
    55  	require.Fail(t.T, "should find relay to attach claim to", "%v", kbTxID)
    56  }
    57  
    58  // Filter by accountID
    59  // But: Unclaimed relays not from the caller are effectively associated with the caller's primary account.
    60  func (t *txlogger) Filter(ctx context.Context, tc *TestContext, accountID stellar1.AccountID, limit int, skipPending bool) []stellar1.PaymentSummary {
    61  	t.Lock()
    62  	defer t.Unlock()
    63  
    64  	// load the caller to get their primary account
    65  	callerAccountID := stellar1.AccountID("")
    66  	meUID := tc.G.ActiveDevice.UID()
    67  	require.False(t.T, meUID.IsNil())
    68  	loadMeArg := libkb.NewLoadUserArgWithContext(ctx, tc.G).
    69  		WithUID(meUID).
    70  		WithSelf(true).
    71  		WithNetContext(ctx)
    72  	user, err := libkb.LoadUser(loadMeArg)
    73  	require.NoError(t.T, err)
    74  	myAccountID := user.StellarAccountID()
    75  	if myAccountID != nil {
    76  		callerAccountID = *myAccountID
    77  	}
    78  	caller := user.ToUserVersion()
    79  
    80  	var res []stellar1.PaymentSummary
    81  collection:
    82  	for _, tx := range t.transactions {
    83  		if limit > 0 && len(res) == limit {
    84  			break
    85  		}
    86  
    87  		typ, err := tx.Summary.Typ()
    88  		require.NoError(t.T, err)
    89  		switch typ {
    90  		case stellar1.PaymentSummaryType_STELLAR:
    91  			p := tx.Summary.Stellar()
    92  			for _, acc := range []stellar1.AccountID{p.From, p.To} {
    93  				if acc.Eq(accountID) {
    94  					res = append(res, tx.Summary)
    95  					continue collection
    96  				}
    97  			}
    98  		case stellar1.PaymentSummaryType_DIRECT:
    99  			p := tx.Summary.Direct()
   100  			for _, acc := range []stellar1.AccountID{p.FromStellar, p.ToStellar} {
   101  				if acc.Eq(accountID) {
   102  					res = append(res, tx.Summary)
   103  					continue collection
   104  				}
   105  			}
   106  		case stellar1.PaymentSummaryType_RELAY:
   107  			p := tx.Summary.Relay()
   108  
   109  			// Caller must be a member of the impteam.
   110  			if !t.isCallerInImplicitTeam(tc, p.TeamID) {
   111  				t.T.Logf("filtered out relay (team membership): %v", p.KbTxID)
   112  				continue collection
   113  			}
   114  
   115  			filterByAccount := func(r *stellar1.PaymentSummaryRelay, accountID stellar1.AccountID) bool {
   116  				if accountID.IsNil() {
   117  					return true
   118  				}
   119  				if r.FromStellar.Eq(accountID) {
   120  					return true
   121  				}
   122  				var successfullyClaimed bool
   123  				if r.Claim != nil {
   124  					if r.Claim.ToStellar.Eq(accountID) {
   125  						return true
   126  					}
   127  					if r.Claim.TxStatus == stellar1.TransactionStatus_SUCCESS {
   128  						successfullyClaimed = true
   129  					}
   130  				}
   131  				// Unclaimed relays not from the caller are effectively associated with the caller's primary account.
   132  				if !successfullyClaimed && !r.From.Eq(caller) && !callerAccountID.IsNil() && accountID.Eq(callerAccountID) {
   133  					return true
   134  				}
   135  				return false
   136  			}
   137  
   138  			if !filterByAccount(&p, accountID) {
   139  				t.T.Logf("filtered out relay (account filter): %v queryAccountID:%v callerAccountID:%v",
   140  					p.KbTxID, accountID, callerAccountID)
   141  				continue collection
   142  			}
   143  
   144  			if skipPending {
   145  				pending := true
   146  				if p.TxStatus != stellar1.TransactionStatus_SUCCESS && p.TxStatus != stellar1.TransactionStatus_PENDING {
   147  					pending = false
   148  				}
   149  				if p.Claim != nil && p.Claim.TxStatus == stellar1.TransactionStatus_SUCCESS {
   150  					pending = false
   151  				}
   152  				if pending {
   153  					continue collection
   154  				}
   155  			}
   156  
   157  			res = append(res, tx.Summary)
   158  		default:
   159  			require.Fail(t.T, "unrecognized variant", "%v", typ)
   160  		}
   161  	}
   162  	return res
   163  }
   164  
   165  // Pending by accountID
   166  func (t *txlogger) Pending(ctx context.Context, tc *TestContext, accountID stellar1.AccountID, limit int) []stellar1.PaymentSummary {
   167  	t.Lock()
   168  	defer t.Unlock()
   169  
   170  	// load the caller to get their primary account
   171  	callerAccountID := stellar1.AccountID("")
   172  	meUID := tc.G.ActiveDevice.UID()
   173  	require.False(t.T, meUID.IsNil())
   174  	loadMeArg := libkb.NewLoadUserArgWithContext(ctx, tc.G).
   175  		WithUID(meUID).
   176  		WithSelf(true).
   177  		WithNetContext(ctx)
   178  	user, err := libkb.LoadUser(loadMeArg)
   179  	require.NoError(t.T, err)
   180  	myAccountID := user.StellarAccountID()
   181  	if myAccountID != nil {
   182  		callerAccountID = *myAccountID
   183  	}
   184  	caller := user.ToUserVersion()
   185  
   186  	var res []stellar1.PaymentSummary
   187  	for _, tx := range t.transactions {
   188  		if limit > 0 && len(res) == limit {
   189  			break
   190  		}
   191  
   192  		typ, err := tx.Summary.Typ()
   193  		require.NoError(t.T, err)
   194  		switch typ {
   195  		case stellar1.PaymentSummaryType_STELLAR:
   196  			continue
   197  		case stellar1.PaymentSummaryType_DIRECT:
   198  			continue
   199  		case stellar1.PaymentSummaryType_RELAY:
   200  			p := tx.Summary.Relay()
   201  
   202  			// Caller must be a member of the impteam.
   203  			if !t.isCallerInImplicitTeam(tc, p.TeamID) {
   204  				t.T.Logf("filtered out relay (team membership): %v", p.KbTxID)
   205  				continue
   206  			}
   207  
   208  			filterByAccount := func(r *stellar1.PaymentSummaryRelay, accountID stellar1.AccountID) bool {
   209  				if accountID.IsNil() {
   210  					return true
   211  				}
   212  				if r.FromStellar.Eq(accountID) {
   213  					return true
   214  				}
   215  				var successfullyClaimed bool
   216  				if r.Claim != nil {
   217  					if r.Claim.ToStellar.Eq(accountID) {
   218  						return true
   219  					}
   220  					if r.Claim.TxStatus == stellar1.TransactionStatus_SUCCESS {
   221  						successfullyClaimed = true
   222  					}
   223  				}
   224  				// Unclaimed relays not from the caller are effectively associated with the caller's primary account.
   225  				if !successfullyClaimed && !r.From.Eq(caller) && !callerAccountID.IsNil() && accountID.Eq(callerAccountID) {
   226  					return true
   227  				}
   228  				return false
   229  			}
   230  
   231  			if !filterByAccount(&p, accountID) {
   232  				t.T.Logf("filtered out relay (account filter): %v queryAccountID:%v callerAccountID:%v",
   233  					p.KbTxID, accountID, callerAccountID)
   234  				continue
   235  			}
   236  
   237  			if p.TxStatus != stellar1.TransactionStatus_SUCCESS && p.TxStatus != stellar1.TransactionStatus_PENDING {
   238  				continue
   239  			}
   240  			if p.Claim != nil && p.Claim.TxStatus == stellar1.TransactionStatus_SUCCESS {
   241  				continue
   242  			}
   243  
   244  			res = append(res, tx.Summary)
   245  		default:
   246  			require.Fail(t.T, "unrecognized variant", "%v", typ)
   247  		}
   248  	}
   249  	return res
   250  }
   251  
   252  // Check whether the caller is in the implicit team.
   253  // By loading the team.
   254  func (t *txlogger) isCallerInImplicitTeam(tc *TestContext, teamID keybase1.TeamID) bool {
   255  	team, _, err := tc.G.GetTeamLoader().Load(context.Background(), keybase1.LoadTeamArg{
   256  		ID:      teamID,
   257  		StaleOK: true,
   258  	})
   259  	if err != nil && err.Error() == "You are not a member of this team (error 2623)" {
   260  		t.T.Logf("caller %v not in team %v", tc.Fu.User.ToUserVersion(), teamID)
   261  		return false
   262  	}
   263  	require.NoError(t.T, err, "Could not load team. And error not recognized as non-membership, assumed to be malfunction.")
   264  	return team.Chain.Implicit
   265  }
   266  
   267  func (t *txlogger) Find(txID string) *stellar1.PaymentDetails {
   268  	t.Lock()
   269  	defer t.Unlock()
   270  	for _, tx := range t.transactions {
   271  		typ, err := tx.Summary.Typ()
   272  		require.NoError(t.T, err)
   273  		switch typ {
   274  		case stellar1.PaymentSummaryType_STELLAR:
   275  			if tx.Summary.Stellar().TxID.String() == txID {
   276  				return &tx
   277  			}
   278  		case stellar1.PaymentSummaryType_DIRECT:
   279  			p := tx.Summary.Direct()
   280  			if p.TxID.String() == txID || p.KbTxID.String() == txID {
   281  				return &tx
   282  			}
   283  		case stellar1.PaymentSummaryType_RELAY:
   284  			if tx.Summary.Relay().TxID.String() == txID || tx.Summary.Relay().KbTxID.String() == txID {
   285  				return &tx
   286  			}
   287  		default:
   288  			require.Fail(t.T, "unrecognized variant", "%v", typ)
   289  		}
   290  	}
   291  	return nil
   292  }
   293  
   294  func (t *txlogger) FindFirstUnclaimedFor(uv keybase1.UserVersion) (*stellar1.PaymentDetails, error) {
   295  	t.Lock()
   296  	defer t.Unlock()
   297  	for _, tx := range t.transactions {
   298  		typ, err := tx.Summary.Typ()
   299  		if err != nil {
   300  			return nil, err
   301  		}
   302  		if typ != stellar1.PaymentSummaryType_RELAY {
   303  			continue
   304  		}
   305  		relay := tx.Summary.Relay()
   306  		if relay.Claim != nil {
   307  			continue
   308  		}
   309  		if relay.To.Eq(uv) {
   310  			return &tx, nil
   311  		}
   312  	}
   313  	return nil, nil
   314  }
   315  
   316  type FakeAccount struct {
   317  	T             testing.TB
   318  	accountID     stellar1.AccountID
   319  	secretKey     stellar1.SecretKey // can be missing for relay accounts
   320  	balance       stellar1.Balance   // XLM
   321  	otherBalances []stellar1.Balance // other assets
   322  	subentries    int
   323  	inflationDest stellar1.AccountID
   324  }
   325  
   326  func (a *FakeAccount) AddBalance(amt string) {
   327  	n, err := stellarnet.ParseStellarAmount(amt)
   328  	require.NoError(a.T, err)
   329  	a.AdjustBalance(n)
   330  }
   331  
   332  func (a *FakeAccount) SubtractBalance(amt string) {
   333  	n, err := stellarnet.ParseStellarAmount(amt)
   334  	require.NoError(a.T, err)
   335  	a.AdjustBalance(-n)
   336  }
   337  
   338  func (a *FakeAccount) ZeroBalance() int64 {
   339  	res, err := stellarnet.ParseStellarAmount(a.balance.Amount)
   340  	require.NoError(a.T, err)
   341  	a.balance.Amount = "0"
   342  	return res
   343  }
   344  
   345  func (a *FakeAccount) AdjustBalance(amt int64) {
   346  	b, err := stellarnet.ParseStellarAmount(a.balance.Amount)
   347  	require.NoError(a.T, err)
   348  	b += amt
   349  	a.balance.Amount = stellarnet.StringFromStellarAmount(b)
   350  	if b < 0 {
   351  		require.Fail(a.T, "account balance went negative", "%v %v", a.accountID, a.balance.Amount)
   352  	}
   353  	a.Check()
   354  }
   355  
   356  func (a *FakeAccount) IsFunded() bool {
   357  	return a.Check()
   358  }
   359  
   360  // Check that the account balance makes sense.
   361  // Returns whether the account is funded.
   362  func (a *FakeAccount) Check() bool {
   363  	b, err := stellarnet.ParseStellarAmount(a.balance.Amount)
   364  	require.NoError(a.T, err)
   365  	minimumReserve, err := stellarnet.ParseStellarAmount("1.0")
   366  	require.NoError(a.T, err)
   367  	switch {
   368  	case b == 0:
   369  		return false
   370  	case b < 0:
   371  		require.Fail(a.T, "account has negative balance", "%v", a.accountID)
   372  	case b < minimumReserve:
   373  		require.Fail(a.T, "account has less than the minimum balance", "%v < %v %v",
   374  			stellarnet.StringFromStellarAmount(b), stellarnet.StringFromStellarAmount(minimumReserve), a.accountID)
   375  	default:
   376  		return true
   377  	}
   378  
   379  	return b != 0
   380  }
   381  
   382  func (a *FakeAccount) availableBalance() string {
   383  	b, err := stellarnet.AvailableBalance(a.balance.Amount, a.subentries)
   384  	if err != nil {
   385  		a.T.Fatalf("AvailableBalance error: %s", err)
   386  	}
   387  	return b
   388  }
   389  
   390  func (a *FakeAccount) AdjustAssetBalance(amount int64, asset stellar1.Asset) {
   391  	for i, v := range a.otherBalances {
   392  		if v.Asset.SameAsset(asset) {
   393  			b, err := stellarnet.ParseStellarAmount(v.Amount)
   394  			require.NoError(a.T, err)
   395  			b += amount
   396  			v.Amount = stellarnet.StringFromStellarAmount(b)
   397  			a.otherBalances[i] = v
   398  			return
   399  		}
   400  	}
   401  
   402  	balance := stellar1.Balance{
   403  		Amount:       stellarnet.StringFromStellarAmount(amount),
   404  		Asset:        asset,
   405  		IsAuthorized: true,
   406  	}
   407  	a.otherBalances = append(a.otherBalances, balance)
   408  }
   409  
   410  // RemoteClientMock is a Remoter that calls into a BackendMock.
   411  // It basically proxies all calls but passes the caller's TC so the backend knows who's calling.
   412  // Threadsafe.
   413  type RemoteClientMock struct {
   414  	libkb.Contextified
   415  	Tc      *TestContext
   416  	Backend *BackendMock
   417  }
   418  
   419  func NewRemoteClientMock(tc *TestContext, bem *BackendMock) *RemoteClientMock {
   420  	return &RemoteClientMock{
   421  		Contextified: libkb.NewContextified(tc.G),
   422  		Tc:           tc,
   423  		Backend:      bem,
   424  	}
   425  }
   426  
   427  func (r *RemoteClientMock) AccountSeqno(ctx context.Context, accountID stellar1.AccountID) (uint64, error) {
   428  	return r.Backend.AccountSeqno(ctx, accountID)
   429  }
   430  
   431  func (r *RemoteClientMock) Balances(ctx context.Context, accountID stellar1.AccountID) ([]stellar1.Balance, error) {
   432  	return r.Backend.Balances(ctx, accountID)
   433  }
   434  
   435  func (r *RemoteClientMock) SubmitPayment(ctx context.Context, post stellar1.PaymentDirectPost) (stellar1.PaymentResult, error) {
   436  	return r.Backend.SubmitPayment(ctx, r.Tc, post)
   437  }
   438  
   439  func (r *RemoteClientMock) SubmitRelayPayment(ctx context.Context, post stellar1.PaymentRelayPost) (stellar1.PaymentResult, error) {
   440  	return r.Backend.SubmitRelayPayment(ctx, r.Tc, post)
   441  }
   442  
   443  func (r *RemoteClientMock) SubmitMultiPayment(ctx context.Context, post stellar1.PaymentMultiPost) (stellar1.SubmitMultiRes, error) {
   444  	return r.Backend.SubmitMultiPayment(ctx, r.Tc, post)
   445  }
   446  
   447  func (r *RemoteClientMock) SubmitRelayClaim(ctx context.Context, post stellar1.RelayClaimPost) (stellar1.RelayClaimResult, error) {
   448  	return r.Backend.SubmitRelayClaim(ctx, r.Tc, post)
   449  }
   450  
   451  func (r *RemoteClientMock) AcquireAutoClaimLock(ctx context.Context) (string, error) {
   452  	return r.Backend.AcquireAutoClaimLock(ctx, r.Tc)
   453  }
   454  
   455  func (r *RemoteClientMock) ReleaseAutoClaimLock(ctx context.Context, token string) error {
   456  	return r.Backend.ReleaseAutoClaimLock(ctx, r.Tc, token)
   457  }
   458  
   459  func (r *RemoteClientMock) NextAutoClaim(ctx context.Context) (*stellar1.AutoClaim, error) {
   460  	return r.Backend.NextAutoClaim(ctx, r.Tc)
   461  }
   462  
   463  func (r *RemoteClientMock) RecentPayments(ctx context.Context, arg remote.RecentPaymentsArg) (stellar1.PaymentsPage, error) {
   464  	return r.Backend.RecentPayments(ctx, r.Tc, arg.AccountID, arg.Cursor, arg.Limit, arg.SkipPending)
   465  }
   466  
   467  func (r *RemoteClientMock) PendingPayments(ctx context.Context, accountID stellar1.AccountID, limit int) ([]stellar1.PaymentSummary, error) {
   468  	return r.Backend.PendingPayments(ctx, r.Tc, accountID, limit)
   469  }
   470  
   471  func (r *RemoteClientMock) PaymentDetails(ctx context.Context, accountID stellar1.AccountID, txID string) (res stellar1.PaymentDetails, err error) {
   472  	return r.Backend.PaymentDetails(ctx, r.Tc, accountID, txID)
   473  }
   474  
   475  func (r *RemoteClientMock) PaymentDetailsGeneric(ctx context.Context, txID string) (res stellar1.PaymentDetails, err error) {
   476  	return r.Backend.PaymentDetailsGeneric(ctx, r.Tc, txID)
   477  }
   478  
   479  func (r *RemoteClientMock) Details(ctx context.Context, accountID stellar1.AccountID) (stellar1.AccountDetails, error) {
   480  	return r.Backend.Details(ctx, r.Tc, accountID)
   481  }
   482  
   483  func (r *RemoteClientMock) GetAccountDisplayCurrency(ctx context.Context, accountID stellar1.AccountID) (string, error) {
   484  	return r.Backend.GetAccountDisplayCurrency(ctx, r.Tc, accountID)
   485  }
   486  
   487  func (r *RemoteClientMock) ExchangeRate(ctx context.Context, currency string) (stellar1.OutsideExchangeRate, error) {
   488  	return r.Backend.ExchangeRate(ctx, r.Tc, currency)
   489  }
   490  
   491  func (r *RemoteClientMock) SubmitRequest(ctx context.Context, post stellar1.RequestPost) (stellar1.KeybaseRequestID, error) {
   492  	return r.Backend.SubmitRequest(ctx, r.Tc, post)
   493  }
   494  
   495  func (r *RemoteClientMock) RequestDetails(ctx context.Context, requestID stellar1.KeybaseRequestID) (stellar1.RequestDetails, error) {
   496  	return r.Backend.RequestDetails(ctx, r.Tc, requestID)
   497  }
   498  
   499  func (r *RemoteClientMock) CancelRequest(ctx context.Context, requestID stellar1.KeybaseRequestID) error {
   500  	return r.Backend.CancelRequest(ctx, r.Tc, requestID)
   501  }
   502  
   503  func (r *RemoteClientMock) MarkAsRead(ctx context.Context, acctID stellar1.AccountID, mostRecentID stellar1.TransactionID) error {
   504  	return r.Backend.MarkAsRead(ctx, r.Tc, acctID, mostRecentID)
   505  }
   506  
   507  func (r *RemoteClientMock) SetAccountMobileOnly(ctx context.Context, acctID stellar1.AccountID) error {
   508  	return r.Backend.SetAccountMobileOnly(ctx, r.Tc, acctID)
   509  }
   510  
   511  func (r *RemoteClientMock) MakeAccountAllDevices(ctx context.Context, acctID stellar1.AccountID) error {
   512  	return r.Backend.MakeAccountAllDevices(ctx, r.Tc, acctID)
   513  }
   514  
   515  func (r *RemoteClientMock) IsAccountMobileOnly(ctx context.Context, acctID stellar1.AccountID) (bool, error) {
   516  	return r.Backend.IsAccountMobileOnly(ctx, r.Tc, acctID)
   517  }
   518  
   519  func (r *RemoteClientMock) ServerTimeboundsRecommendation(ctx context.Context) (stellar1.TimeboundsRecommendation, error) {
   520  	return r.Backend.ServerTimeboundsRecommendation(ctx, r.Tc)
   521  }
   522  
   523  func (r *RemoteClientMock) SetInflationDestination(ctx context.Context, signedTx string) error {
   524  	return r.Backend.SetInflationDestination(ctx, r.Tc, signedTx)
   525  }
   526  
   527  func (r *RemoteClientMock) GetInflationDestinations(ctx context.Context) (ret []stellar1.PredefinedInflationDestination, err error) {
   528  	return r.Backend.GetInflationDestinations(ctx, r.Tc)
   529  }
   530  
   531  func (r *RemoteClientMock) NetworkOptions(ctx context.Context) (stellar1.NetworkOptions, error) {
   532  	return stellar1.NetworkOptions{BaseFee: 100}, nil
   533  }
   534  
   535  func (r *RemoteClientMock) DetailsPlusPayments(ctx context.Context, accountID stellar1.AccountID) (stellar1.DetailsPlusPayments, error) {
   536  	details, err := r.Backend.Details(ctx, r.Tc, accountID)
   537  	if err != nil {
   538  		return stellar1.DetailsPlusPayments{}, err
   539  	}
   540  
   541  	recent, err := r.Backend.RecentPayments(ctx, r.Tc, accountID, nil, 50, true)
   542  	if err != nil {
   543  		return stellar1.DetailsPlusPayments{}, err
   544  	}
   545  
   546  	pending, err := r.Backend.PendingPayments(ctx, r.Tc, accountID, 25)
   547  	if err != nil {
   548  		return stellar1.DetailsPlusPayments{}, err
   549  	}
   550  
   551  	return stellar1.DetailsPlusPayments{
   552  		Details:         details,
   553  		RecentPayments:  recent,
   554  		PendingPayments: pending,
   555  	}, nil
   556  }
   557  
   558  func (r *RemoteClientMock) AllDetailsPlusPayments(mctx libkb.MetaContext) ([]stellar1.DetailsPlusPayments, error) {
   559  	r.Tc.T.Log("AllDetailsPlusPayments for %s", r.Tc.Fu.GetUID())
   560  	ids := r.Backend.AllAccountIDs(r.Tc.Fu.GetUID())
   561  	var all []stellar1.DetailsPlusPayments
   562  	for _, id := range ids {
   563  		dpp, err := r.DetailsPlusPayments(mctx.Ctx(), id)
   564  		if err == nil {
   565  			r.Tc.T.Log("AllDetailsPlusPayments dpp for %s/%s: %+v", r.Tc.Fu.GetUID(), id, dpp)
   566  			all = append(all, dpp)
   567  		}
   568  	}
   569  	return all, nil
   570  }
   571  
   572  func (r *RemoteClientMock) ChangeTrustline(ctx context.Context, signedTx string) error {
   573  	return r.Backend.ChangeTrustline(ctx, r.Tc, signedTx)
   574  }
   575  
   576  func (r *RemoteClientMock) FindPaymentPath(_ libkb.MetaContext, _ stellar1.PaymentPathQuery) (stellar1.PaymentPath, error) {
   577  	return stellar1.PaymentPath{}, errors.New("not mocked")
   578  }
   579  
   580  func (r *RemoteClientMock) SubmitPathPayment(_ libkb.MetaContext, _ stellar1.PathPaymentPost) (stellar1.PaymentResult, error) {
   581  	return stellar1.PaymentResult{}, errors.New("not mocked")
   582  }
   583  
   584  func (r *RemoteClientMock) FuzzyAssetSearch(_ libkb.MetaContext, _ stellar1.FuzzyAssetSearchArg) ([]stellar1.Asset, error) {
   585  	return nil, errors.New("not mocked")
   586  }
   587  
   588  func (r *RemoteClientMock) ListPopularAssets(_ libkb.MetaContext, _ stellar1.ListPopularAssetsArg) (stellar1.AssetListResult, error) {
   589  	return stellar1.AssetListResult{}, errors.New("not mocked")
   590  }
   591  
   592  func (r *RemoteClientMock) PostAnyTransaction(_ libkb.MetaContext, _ string) error {
   593  	return errors.New("post any transaction is not mocked")
   594  }
   595  
   596  var _ remote.Remoter = (*RemoteClientMock)(nil)
   597  
   598  const (
   599  	defaultExchangeRate   = "0.318328"
   600  	alternateExchangeRate = "0.212133"
   601  )
   602  
   603  // BackendMock is a mock of stellard.
   604  // Stores the data and services RemoteClientMock's calls.
   605  // Threadsafe.
   606  type BackendMock struct {
   607  	sync.Mutex
   608  	T        testing.TB
   609  	seqnos   map[stellar1.AccountID]uint64
   610  	accounts map[stellar1.AccountID]*FakeAccount
   611  	requests map[stellar1.KeybaseRequestID]*stellar1.RequestDetails
   612  	txLog    *txlogger
   613  	exchRate string
   614  	currency string
   615  
   616  	autoclaimEnabled map[keybase1.UID]bool
   617  	autoclaimLocks   map[keybase1.UID]bool
   618  
   619  	userAccounts map[keybase1.UID][]stellar1.AccountID
   620  }
   621  
   622  func NewBackendMock(t testing.TB) *BackendMock {
   623  	return &BackendMock{
   624  		T:        t,
   625  		seqnos:   make(map[stellar1.AccountID]uint64),
   626  		accounts: make(map[stellar1.AccountID]*FakeAccount),
   627  		requests: make(map[stellar1.KeybaseRequestID]*stellar1.RequestDetails),
   628  		txLog:    newTxLogger(t),
   629  		exchRate: defaultExchangeRate,
   630  		currency: "USD",
   631  
   632  		autoclaimEnabled: make(map[keybase1.UID]bool),
   633  		autoclaimLocks:   make(map[keybase1.UID]bool),
   634  
   635  		userAccounts: make(map[keybase1.UID][]stellar1.AccountID),
   636  	}
   637  }
   638  
   639  func (r *BackendMock) trace(err *error, name string, format string, args ...interface{}) func() {
   640  	r.T.Logf("+ %s %s", name, fmt.Sprintf(format, args...))
   641  	return func() {
   642  		errStr := "?"
   643  		if err != nil {
   644  			if *err == nil {
   645  				errStr = "ok"
   646  			} else {
   647  				errStr = "ERROR: " + (*err).Error()
   648  			}
   649  		}
   650  		r.T.Logf("- %s => %s", name, errStr)
   651  	}
   652  }
   653  
   654  func (r *BackendMock) addPayment(accountID stellar1.AccountID, payment stellar1.PaymentDetails) {
   655  	defer r.trace(nil, "BackendMock.addPayment", "")()
   656  	r.txLog.Add(payment)
   657  
   658  	r.seqnos[accountID]++
   659  }
   660  
   661  func (r *BackendMock) addClaim(accountID stellar1.AccountID, kbTxID stellar1.KeybaseTransactionID, summary stellar1.ClaimSummary) {
   662  	defer r.trace(nil, "BackendMock.addClaim", "")()
   663  	r.txLog.AddClaim(kbTxID, summary)
   664  
   665  	r.seqnos[accountID]++
   666  }
   667  
   668  func (r *BackendMock) AccountSeqno(ctx context.Context, accountID stellar1.AccountID) (res uint64, err error) {
   669  	defer r.trace(&err, "BackendMock.AccountSeqno", "%v", accountID)()
   670  	r.Lock()
   671  	defer r.Unlock()
   672  	_, ok := r.seqnos[accountID]
   673  	if !ok {
   674  		r.seqnos[accountID] = uint64(time.Now().UnixNano())
   675  	}
   676  
   677  	return r.seqnos[accountID], nil
   678  }
   679  
   680  func (r *BackendMock) Balances(ctx context.Context, accountID stellar1.AccountID) (res []stellar1.Balance, err error) {
   681  	defer r.trace(&err, "BackendMock.Balances", "%v", accountID)()
   682  	r.Lock()
   683  	defer r.Unlock()
   684  	a, ok := r.accounts[accountID]
   685  	if !ok {
   686  		// If an account does not exist on the network, return empty balance list.
   687  		return nil, nil
   688  	}
   689  	res = append(res, a.balance)
   690  	res = append(res, a.otherBalances...)
   691  	return res, nil
   692  }
   693  
   694  func (r *BackendMock) SubmitPayment(ctx context.Context, tc *TestContext, post stellar1.PaymentDirectPost) (res stellar1.PaymentResult, err error) {
   695  	defer tc.G.CTrace(ctx, "BackendMock.SubmitPayment", &err)()
   696  	r.Lock()
   697  	defer r.Unlock()
   698  	kbTxID := randomKeybaseTransactionID(r.T)
   699  
   700  	if post.QuickReturn {
   701  		msg := "SubmitPayment with QuickReturn not implemented on BackendMock"
   702  		r.T.Fatalf(msg)
   703  		return res, errors.New(msg)
   704  	}
   705  
   706  	// Unpack signed transaction and checks if Payment matches transaction.
   707  	unpackedTx, txIDPrecalc, err := unpackTx(post.SignedTransaction)
   708  
   709  	if err != nil {
   710  		return res, err
   711  	}
   712  	extract, err := extractPaymentTx(unpackedTx.Tx)
   713  	if err != nil {
   714  		return res, err
   715  	}
   716  	if extract.AmountXdr < 0 {
   717  		return res, fmt.Errorf("payment amount %v must be greater than zero", extract.Amount)
   718  	}
   719  
   720  	a, ok := r.accounts[extract.From]
   721  	if !ok {
   722  		return stellar1.PaymentResult{}, libkb.NotFoundError{Msg: fmt.Sprintf("source account not found: '%v'", extract.From)}
   723  	}
   724  
   725  	if !extract.Asset.IsNativeXLM() {
   726  		return stellar1.PaymentResult{}, errors.New("can only handle native")
   727  	}
   728  
   729  	require.NotNil(tc.T, extract.TimeBounds, "We are expecting TimeBounds in all txs")
   730  	if extract.TimeBounds != nil {
   731  		require.NotZero(tc.T, extract.TimeBounds.MaxTime, "We are expecting non-zero TimeBounds.MaxTime in all txs")
   732  		require.True(tc.T, time.Now().Before(time.Unix(int64(extract.TimeBounds.MaxTime), 0)))
   733  		// We always send MinTime=0 but this assertion should still hold.
   734  		require.True(tc.T, time.Now().After(time.Unix(int64(extract.TimeBounds.MinTime), 0)))
   735  	}
   736  
   737  	caller, err := tc.G.GetMeUV(ctx)
   738  	if err != nil {
   739  		return stellar1.PaymentResult{}, fmt.Errorf("could not get self UV: %v", err)
   740  	}
   741  
   742  	toIsFunded := false
   743  	b, toExists := r.accounts[extract.To]
   744  
   745  	if !toIsFunded {
   746  		if extract.AmountXdr < 10000000 {
   747  			return stellar1.PaymentResult{}, errors.New("op minimum reserve get outta here")
   748  		}
   749  	}
   750  	if !toExists {
   751  		b = r.addAccountByID(caller.Uid, extract.To, false)
   752  	}
   753  	a.SubtractBalance(extract.Amount)
   754  	a.AdjustBalance(-(int64(unpackedTx.Tx.Fee)))
   755  	b.AddBalance(extract.Amount)
   756  
   757  	summary := stellar1.NewPaymentSummaryWithDirect(stellar1.PaymentSummaryDirect{
   758  		KbTxID:              kbTxID,
   759  		TxID:                stellar1.TransactionID(txIDPrecalc),
   760  		TxStatus:            stellar1.TransactionStatus_SUCCESS,
   761  		FromStellar:         extract.From,
   762  		From:                caller,
   763  		FromDeviceID:        post.FromDeviceID,
   764  		FromDisplayAmount:   "123.23",
   765  		FromDisplayCurrency: "USD",
   766  		ToDisplayAmount:     "18.50",
   767  		ToDisplayCurrency:   "JPY",
   768  		ToStellar:           extract.To,
   769  		To:                  post.To,
   770  		Amount:              extract.Amount,
   771  		Asset:               extract.Asset,
   772  		DisplayAmount:       &post.DisplayAmount,
   773  		DisplayCurrency:     &post.DisplayCurrency,
   774  		NoteB64:             post.NoteB64,
   775  		Ctime:               stellar1.ToTimeMs(time.Now()),
   776  		Rtime:               stellar1.ToTimeMs(time.Now()),
   777  	})
   778  
   779  	memo, memoType := extractMemo(unpackedTx.Tx)
   780  
   781  	r.addPayment(extract.From, stellar1.PaymentDetails{
   782  		Summary:       summary,
   783  		Memo:          memo,
   784  		MemoType:      memoType,
   785  		ExternalTxURL: fmt.Sprintf("https://stellar.expert/explorer/public/tx/%s", txIDPrecalc),
   786  	})
   787  
   788  	return stellar1.PaymentResult{
   789  		StellarID: stellar1.TransactionID(txIDPrecalc),
   790  		KeybaseID: kbTxID,
   791  	}, nil
   792  }
   793  
   794  func (r *BackendMock) SubmitRelayPayment(ctx context.Context, tc *TestContext, post stellar1.PaymentRelayPost) (res stellar1.PaymentResult, err error) {
   795  	defer tc.G.CTrace(ctx, "BackendMock.SubmitRelayPayment", &err)()
   796  	r.Lock()
   797  	defer r.Unlock()
   798  	kbTxID := randomKeybaseTransactionID(r.T)
   799  
   800  	if post.QuickReturn {
   801  		msg := "SubmitRelayPayment with QuickReturn not implemented on BackendMock"
   802  		r.T.Fatalf(msg)
   803  		return res, errors.New(msg)
   804  	}
   805  
   806  	unpackedTx, txIDPrecalc, err := unpackTx(post.SignedTransaction)
   807  	if err != nil {
   808  		return res, err
   809  	}
   810  	extract, err := extractPaymentTx(unpackedTx.Tx)
   811  	if err != nil {
   812  		return res, err
   813  	}
   814  	if extract.OpType != xdr.OperationTypeCreateAccount {
   815  		return res, fmt.Errorf("relay funding transaction must be CreateAccount but got %v", extract.OpType)
   816  	}
   817  	if !extract.To.Eq(post.RelayAccount) {
   818  		return res, fmt.Errorf("relay destination does not match funding tx: %v != %v", extract.To, post.RelayAccount)
   819  	}
   820  	if !extract.Asset.IsNativeXLM() {
   821  		return res, fmt.Errorf("relay transaction can only transport XLM asset")
   822  	}
   823  	const relayPaymentMinimumBalance = xdr.Int64(20100000) // 2.01 XLM
   824  	if extract.AmountXdr < relayPaymentMinimumBalance {
   825  		return res, fmt.Errorf("must send at least %v", stellarnet.StringFromStellarXdrAmount(relayPaymentMinimumBalance))
   826  	}
   827  
   828  	caller, err := tc.G.GetMeUV(ctx)
   829  	if err != nil {
   830  		return stellar1.PaymentResult{}, fmt.Errorf("could not get self UV: %v", err)
   831  	}
   832  
   833  	a, ok := r.accounts[extract.From]
   834  	if !ok {
   835  		return stellar1.PaymentResult{}, libkb.NotFoundError{Msg: fmt.Sprintf("source account not found: '%v'", extract.From)}
   836  	}
   837  	b := r.addAccountByID(caller.Uid, extract.To, false)
   838  	a.SubtractBalance(extract.Amount)
   839  	a.AdjustBalance(-(int64(unpackedTx.Tx.Fee)))
   840  	b.AddBalance(extract.Amount)
   841  
   842  	summary := stellar1.NewPaymentSummaryWithRelay(stellar1.PaymentSummaryRelay{
   843  		KbTxID:          kbTxID,
   844  		TxID:            stellar1.TransactionID(txIDPrecalc),
   845  		TxStatus:        stellar1.TransactionStatus_SUCCESS,
   846  		FromStellar:     extract.From,
   847  		From:            caller,
   848  		FromDeviceID:    post.FromDeviceID,
   849  		To:              post.To,
   850  		ToAssertion:     post.ToAssertion,
   851  		RelayAccount:    extract.To,
   852  		Amount:          extract.Amount,
   853  		DisplayAmount:   &post.DisplayAmount,
   854  		DisplayCurrency: &post.DisplayCurrency,
   855  		Ctime:           stellar1.ToTimeMs(time.Now()),
   856  		Rtime:           stellar1.ToTimeMs(time.Now()),
   857  		BoxB64:          post.BoxB64,
   858  		TeamID:          post.TeamID,
   859  	})
   860  	r.addPayment(extract.From, stellar1.PaymentDetails{Summary: summary})
   861  
   862  	return stellar1.PaymentResult{
   863  		StellarID: stellar1.TransactionID(txIDPrecalc),
   864  		KeybaseID: kbTxID,
   865  	}, nil
   866  }
   867  
   868  func (r *BackendMock) SubmitRelayClaim(ctx context.Context, tc *TestContext, post stellar1.RelayClaimPost) (res stellar1.RelayClaimResult, err error) {
   869  	defer tc.G.CTrace(ctx, "BackendMock.SubmitRelayClaim", &err)()
   870  	r.Lock()
   871  	defer r.Unlock()
   872  
   873  	unpackedTx, txIDPrecalc, err := unpackTx(post.SignedTransaction)
   874  	if err != nil {
   875  		return res, err
   876  	}
   877  	extract, err := extractRelocateTx(unpackedTx.Tx)
   878  	if err != nil {
   879  		return res, err
   880  	}
   881  
   882  	a, ok := r.accounts[extract.From]
   883  	if !ok {
   884  		return res, libkb.NotFoundError{Msg: fmt.Sprintf("claim source account not found: '%v'", extract.From)}
   885  	}
   886  	b, ok := r.accounts[extract.To]
   887  	if !ok {
   888  		return res, libkb.NotFoundError{Msg: fmt.Sprintf("claim target account not found: '%v'", extract.From)}
   889  	}
   890  	if amt, _ := stellarnet.ParseStellarAmount(a.balance.Amount); amt == 0 {
   891  		return res, fmt.Errorf("claim source account has zero balance: %v", a.accountID)
   892  	}
   893  	a.AdjustBalance(-(int64(unpackedTx.Tx.Fee)))
   894  	b.AdjustBalance(a.ZeroBalance())
   895  
   896  	caller, err := tc.G.GetMeUV(ctx)
   897  	if err != nil {
   898  		return stellar1.RelayClaimResult{}, fmt.Errorf("could not get self UV: %v", err)
   899  	}
   900  	r.addClaim(extract.From, post.KeybaseID, stellar1.ClaimSummary{
   901  		TxID:      stellar1.TransactionID(txIDPrecalc),
   902  		TxStatus:  stellar1.TransactionStatus_SUCCESS,
   903  		Dir:       post.Dir,
   904  		ToStellar: extract.To,
   905  		To:        caller,
   906  	})
   907  
   908  	return stellar1.RelayClaimResult{
   909  		ClaimStellarID: stellar1.TransactionID(txIDPrecalc),
   910  	}, nil
   911  }
   912  
   913  func (r *BackendMock) EnableAutoclaimMock(tc *TestContext) {
   914  	r.autoclaimEnabled[tc.Fu.GetUID()] = true
   915  	r.autoclaimLocks[tc.Fu.GetUID()] = false
   916  }
   917  
   918  func (r *BackendMock) AcquireAutoClaimLock(ctx context.Context, tc *TestContext) (string, error) {
   919  	uid := tc.Fu.GetUID()
   920  	if !r.autoclaimEnabled[uid] {
   921  		return "", fmt.Errorf("Autoclaims are not enabled for %q", tc.Fu.Username)
   922  	}
   923  	require.False(tc.T, r.autoclaimLocks[uid], "Lock already acquired")
   924  	r.autoclaimLocks[uid] = true
   925  	return "autoclaim_test_token", nil
   926  }
   927  
   928  func (r *BackendMock) ReleaseAutoClaimLock(ctx context.Context, tc *TestContext, token string) error {
   929  	uid := tc.Fu.GetUID()
   930  	require.True(tc.T, r.autoclaimEnabled[uid], "autoclaims have to be enabled for uid")
   931  	require.True(tc.T, r.autoclaimLocks[uid], "Lock has to be called first before Release")
   932  	r.autoclaimLocks[uid] = false
   933  	return nil
   934  }
   935  
   936  func (r *BackendMock) NextAutoClaim(ctx context.Context, tc *TestContext) (*stellar1.AutoClaim, error) {
   937  	caller, err := tc.G.GetMeUV(ctx)
   938  	if err != nil {
   939  		return nil, fmt.Errorf("could not get self UV: %v", err)
   940  	}
   941  	uid := caller.Uid
   942  	require.True(tc.T, r.autoclaimEnabled[uid], "autoclaims have to be enabled for uid")
   943  	require.True(tc.T, r.autoclaimLocks[uid], "Lock has to be called first before NextAutoClaim")
   944  
   945  	payment, err := r.txLog.FindFirstUnclaimedFor(caller)
   946  	require.NoError(tc.T, err)
   947  	if payment != nil {
   948  		return &stellar1.AutoClaim{
   949  			KbTxID: payment.Summary.Relay().KbTxID,
   950  		}, nil
   951  	}
   952  	return nil, nil
   953  }
   954  
   955  func (r *BackendMock) SubmitMultiPayment(ctx context.Context, tc *TestContext, post stellar1.PaymentMultiPost) (stellar1.SubmitMultiRes, error) {
   956  	r.Lock()
   957  	defer r.Unlock()
   958  
   959  	// doing as little as possible here (i.e. just returning the
   960  	// transaction id and not storing any of these operations in the mock)
   961  
   962  	_, txID, err := unpackTx(post.SignedTransaction)
   963  	if err != nil {
   964  		return stellar1.SubmitMultiRes{}, err
   965  	}
   966  
   967  	return stellar1.SubmitMultiRes{
   968  		TxID: stellar1.TransactionID(txID),
   969  	}, nil
   970  }
   971  
   972  func (r *BackendMock) RecentPayments(ctx context.Context, tc *TestContext, accountID stellar1.AccountID, cursor *stellar1.PageCursor, limit int, skipPending bool) (res stellar1.PaymentsPage, err error) {
   973  	defer tc.G.CTrace(ctx, "BackendMock.RecentPayments", &err)()
   974  	r.Lock()
   975  	defer r.Unlock()
   976  	if cursor != nil {
   977  		return res, errors.New("cursor not mocked")
   978  	}
   979  	res.Payments = r.txLog.Filter(ctx, tc, accountID, limit, skipPending)
   980  	return res, nil
   981  }
   982  
   983  func (r *BackendMock) PendingPayments(ctx context.Context, tc *TestContext, accountID stellar1.AccountID, limit int) (res []stellar1.PaymentSummary, err error) {
   984  	defer tc.G.CTrace(ctx, "BackendMock.PendingPayments", &err)()
   985  	r.Lock()
   986  	defer r.Unlock()
   987  	res = r.txLog.Pending(ctx, tc, accountID, limit)
   988  	return res, nil
   989  }
   990  
   991  func (r *BackendMock) PaymentDetails(ctx context.Context, tc *TestContext, accountID stellar1.AccountID, txID string) (res stellar1.PaymentDetails, err error) {
   992  	defer tc.G.CTrace(ctx, "BackendMock.PaymentDetails", &err)()
   993  	if accountID.IsNil() {
   994  		return res, errors.New("PaymentDetails requires AccountID")
   995  	}
   996  	r.Lock()
   997  	defer r.Unlock()
   998  	p := r.txLog.Find(txID)
   999  	if p == nil {
  1000  		return res, fmt.Errorf("BackendMock: tx not found: '%v'", txID)
  1001  	}
  1002  	return *p, nil
  1003  }
  1004  
  1005  func (r *BackendMock) PaymentDetailsGeneric(ctx context.Context, tc *TestContext, txID string) (res stellar1.PaymentDetails, err error) {
  1006  	defer tc.G.CTrace(ctx, "BackendMock.PaymentDetailsGeneric", &err)()
  1007  	r.Lock()
  1008  	defer r.Unlock()
  1009  	p := r.txLog.Find(txID)
  1010  	if p == nil {
  1011  		return res, fmt.Errorf("BackendMock: tx not found: '%v'", txID)
  1012  	}
  1013  	return *p, nil
  1014  }
  1015  
  1016  type accountCurrencyResult struct {
  1017  	libkb.AppStatusEmbed
  1018  	CurrencyDisplayPreference string `json:"currency_display_preference"`
  1019  }
  1020  
  1021  func (r *BackendMock) Details(ctx context.Context, tc *TestContext, accountID stellar1.AccountID) (res stellar1.AccountDetails, err error) {
  1022  	defer tc.G.CTrace(ctx, "RemoteMock.Details", &err)()
  1023  	r.Lock()
  1024  	defer r.Unlock()
  1025  
  1026  	_, err = stellarnet.NewAddressStr(string(accountID))
  1027  	if err != nil {
  1028  		return res, err
  1029  	}
  1030  
  1031  	// Fetch the currency display preference for this account first,
  1032  	// users are allowed to have currency preferences even for accounts
  1033  	// that do not exist on the network yet.
  1034  	var displayCurrency string
  1035  	mctx := libkb.NewMetaContext(ctx, tc.G)
  1036  	apiArg := libkb.APIArg{
  1037  		Endpoint:    "stellar/accountcurrency",
  1038  		SessionType: libkb.APISessionTypeREQUIRED,
  1039  		Args: libkb.HTTPArgs{
  1040  			"account_id": libkb.S{Val: string(accountID)},
  1041  		},
  1042  	}
  1043  	var apiRes accountCurrencyResult
  1044  	err = tc.G.API.GetDecode(mctx, apiArg, &apiRes)
  1045  	if err == nil {
  1046  		displayCurrency = apiRes.CurrencyDisplayPreference
  1047  	}
  1048  
  1049  	a, ok := r.accounts[accountID]
  1050  	if !ok {
  1051  		// If an account does not exist on the network, return something reasonable.
  1052  		return stellar1.AccountDetails{
  1053  			AccountID:       accountID,
  1054  			Seqno:           "0",
  1055  			Balances:        nil,
  1056  			SubentryCount:   0,
  1057  			Available:       "0",
  1058  			DisplayCurrency: displayCurrency,
  1059  		}, nil
  1060  	}
  1061  	var balances []stellar1.Balance
  1062  	// this is different than how BackendMock.Balances works:
  1063  	if a.balance.Amount != "" {
  1064  		balances = []stellar1.Balance{a.balance}
  1065  	}
  1066  	balances = append(balances, a.otherBalances...)
  1067  
  1068  	var inflationDest *stellar1.AccountID
  1069  	if a.inflationDest != "" {
  1070  		inflationDest = &a.inflationDest
  1071  	}
  1072  
  1073  	return stellar1.AccountDetails{
  1074  		AccountID:            accountID,
  1075  		Seqno:                strconv.FormatUint(r.seqnos[accountID], 10),
  1076  		Balances:             balances,
  1077  		SubentryCount:        a.subentries,
  1078  		Available:            a.availableBalance(),
  1079  		DisplayCurrency:      displayCurrency,
  1080  		InflationDestination: inflationDest,
  1081  	}, nil
  1082  }
  1083  
  1084  func (r *BackendMock) AddAccount(uid keybase1.UID) stellar1.AccountID {
  1085  	defer r.trace(nil, "BackendMock.AddAccount", "")()
  1086  	r.Lock()
  1087  	defer r.Unlock()
  1088  	return r.addAccountRandom(uid, true)
  1089  }
  1090  
  1091  func (r *BackendMock) AddAccountEmpty(t *testing.T, uid keybase1.UID) stellar1.AccountID {
  1092  	return r.addAccountRandom(uid, false)
  1093  }
  1094  
  1095  func (r *BackendMock) addAccountRandom(uid keybase1.UID, funded bool) stellar1.AccountID {
  1096  	full, err := keypair.Random()
  1097  	require.NoError(r.T, err)
  1098  	amount := "0"
  1099  	if funded {
  1100  		amount = "10000"
  1101  	}
  1102  	a := &FakeAccount{
  1103  		T:         r.T,
  1104  		accountID: stellar1.AccountID(full.Address()),
  1105  		secretKey: stellar1.SecretKey(full.Seed()),
  1106  		balance: stellar1.Balance{
  1107  			Asset:        stellar1.Asset{Type: "native"},
  1108  			Amount:       amount,
  1109  			IsAuthorized: true,
  1110  		},
  1111  	}
  1112  
  1113  	require.Nil(r.T, r.accounts[a.accountID], "attempt to re-add account %v", a.accountID)
  1114  	r.accounts[a.accountID] = a
  1115  	r.seqnos[a.accountID] = uint64(time.Now().UnixNano())
  1116  	r.userAccounts[uid] = append(r.userAccounts[uid], a.accountID)
  1117  	return a.accountID
  1118  }
  1119  
  1120  func (r *BackendMock) addAccountByID(uid keybase1.UID, accountID stellar1.AccountID, funded bool) *FakeAccount {
  1121  	amount := "0"
  1122  	if funded {
  1123  		amount = "10000"
  1124  	}
  1125  	a := &FakeAccount{
  1126  		T:         r.T,
  1127  		accountID: accountID,
  1128  		balance: stellar1.Balance{
  1129  			Asset:        stellar1.AssetNative(),
  1130  			Amount:       amount,
  1131  			IsAuthorized: true,
  1132  		},
  1133  	}
  1134  	require.Nil(r.T, r.accounts[a.accountID], "attempt to re-add account %v", a.accountID)
  1135  	r.accounts[a.accountID] = a
  1136  	r.seqnos[a.accountID] = uint64(time.Now().UnixNano())
  1137  	r.userAccounts[uid] = append(r.userAccounts[uid], a.accountID)
  1138  	return a
  1139  }
  1140  
  1141  func (r *BackendMock) ImportAccountsForUser(tc *TestContext) (res []*FakeAccount) {
  1142  	mctx := tc.MetaContext()
  1143  	defer mctx.Trace("BackendMock.ImportAccountsForUser", nil)()
  1144  	r.Lock()
  1145  	bundle, err := fetchWholeBundleForTesting(mctx)
  1146  	require.NoError(r.T, err)
  1147  	for _, account := range bundle.Accounts {
  1148  		if _, found := r.accounts[account.AccountID]; found {
  1149  			continue
  1150  		}
  1151  		acc := r.addAccountByID(tc.Fu.GetUID(), account.AccountID, false /* funded */)
  1152  		acc.secretKey = bundle.AccountBundles[account.AccountID].Signers[0]
  1153  		res = append(res, acc)
  1154  	}
  1155  	r.Unlock()
  1156  
  1157  	err = tc.Srv.walletState.RefreshAll(mctx, "test")
  1158  	require.NoError(r.T, err)
  1159  
  1160  	return res
  1161  }
  1162  
  1163  func (r *BackendMock) SecretKey(accountID stellar1.AccountID) stellar1.SecretKey {
  1164  	defer r.trace(nil, "BackendMock.SecretKey", "")()
  1165  	r.Lock()
  1166  	defer r.Unlock()
  1167  	a := r.accounts[accountID]
  1168  	require.NotNil(r.T, a, "SecretKey: account id not in remote mock: %v", accountID)
  1169  	require.True(r.T, len(a.secretKey) > 0, "secret key missing in mock for: %v", accountID)
  1170  	return a.secretKey
  1171  }
  1172  
  1173  func (r *BackendMock) AssertBalance(accountID stellar1.AccountID, amount string) {
  1174  	r.Lock()
  1175  	defer r.Unlock()
  1176  	require.NotNil(r.T, r.accounts[accountID], "account should exist in mock to assert balance")
  1177  	require.Equal(r.T, amount, r.accounts[accountID].balance.Amount, "account balance")
  1178  }
  1179  
  1180  func (r *BackendMock) GetAccountDisplayCurrency(ctx context.Context, tc *TestContext, accountID stellar1.AccountID) (string, error) {
  1181  	r.Lock()
  1182  	defer r.Unlock()
  1183  	return r.currency, nil
  1184  }
  1185  
  1186  func (r *BackendMock) SetDisplayCurrency(currency string) {
  1187  	r.Lock()
  1188  	defer r.Unlock()
  1189  	r.currency = currency
  1190  }
  1191  
  1192  func (r *BackendMock) ExchangeRate(ctx context.Context, tc *TestContext, currency string) (stellar1.OutsideExchangeRate, error) {
  1193  	r.Lock()
  1194  	defer r.Unlock()
  1195  	return stellar1.OutsideExchangeRate{
  1196  		Currency: stellar1.OutsideCurrencyCode(currency),
  1197  		Rate:     r.exchRate,
  1198  	}, nil
  1199  }
  1200  
  1201  func (r *BackendMock) UseDefaultExchangeRate() {
  1202  	r.exchRate = defaultExchangeRate
  1203  }
  1204  
  1205  func (r *BackendMock) UseAlternateExchangeRate() {
  1206  	r.exchRate = alternateExchangeRate
  1207  }
  1208  
  1209  func (r *BackendMock) SetExchangeRate(rate string) {
  1210  	r.exchRate = rate
  1211  }
  1212  
  1213  func (r *BackendMock) SubmitRequest(ctx context.Context, tc *TestContext, post stellar1.RequestPost) (res stellar1.KeybaseRequestID, err error) {
  1214  	b, err := libkb.RandBytesWithSuffix(stellar1.KeybaseRequestIDLen, stellar1.KeybaseRequestIDSuffix)
  1215  	if err != nil {
  1216  		return "", err
  1217  	}
  1218  
  1219  	reqID, err := stellar1.KeybaseRequestIDFromString(hex.EncodeToString(b))
  1220  	if err != nil {
  1221  		return "", err
  1222  	}
  1223  
  1224  	caller, err := tc.G.GetMeUV(ctx)
  1225  	if err != nil {
  1226  		return "", fmt.Errorf("could not get self UV: %v", err)
  1227  	}
  1228  
  1229  	r.requests[reqID] = &stellar1.RequestDetails{
  1230  		Id:          reqID,
  1231  		FromUser:    caller,
  1232  		ToUser:      post.ToUser,
  1233  		ToAssertion: post.ToAssertion,
  1234  		Amount:      post.Amount,
  1235  		Asset:       post.Asset,
  1236  		Currency:    post.Currency,
  1237  	}
  1238  	return reqID, nil
  1239  }
  1240  
  1241  func (r *BackendMock) RequestDetails(ctx context.Context, tc *TestContext, requestID stellar1.KeybaseRequestID) (res stellar1.RequestDetails, err error) {
  1242  	details, ok := r.requests[requestID]
  1243  	if !ok {
  1244  		return res, fmt.Errorf("request %v not found", requestID)
  1245  	}
  1246  
  1247  	return *details, nil
  1248  }
  1249  
  1250  func (r *BackendMock) CancelRequest(ctx context.Context, tc *TestContext, requestID stellar1.KeybaseRequestID) (err error) {
  1251  	readError := func() error { return fmt.Errorf("could not find request with ID %s", requestID) }
  1252  
  1253  	details, ok := r.requests[requestID]
  1254  	if !ok {
  1255  		return readError()
  1256  	}
  1257  
  1258  	caller, err := tc.G.GetMeUV(ctx)
  1259  	if err != nil {
  1260  		return fmt.Errorf("could not get self UV: %v", err)
  1261  	}
  1262  
  1263  	if !details.FromUser.Eq(caller) {
  1264  		return readError()
  1265  	}
  1266  
  1267  	details.Status = stellar1.RequestStatus_CANCELED
  1268  	return nil
  1269  }
  1270  
  1271  func (r *BackendMock) MarkAsRead(ctx context.Context, tc *TestContext, acctID stellar1.AccountID, mostRecentID stellar1.TransactionID) error {
  1272  	return nil
  1273  }
  1274  
  1275  func (r *BackendMock) IsAccountMobileOnly(ctx context.Context, tc *TestContext, accountID stellar1.AccountID) (bool, error) {
  1276  	return remote.IsAccountMobileOnly(ctx, tc.G, accountID)
  1277  }
  1278  
  1279  func (r *BackendMock) SetAccountMobileOnly(ctx context.Context, tc *TestContext, accountID stellar1.AccountID) error {
  1280  	return remote.SetAccountMobileOnly(ctx, tc.G, accountID)
  1281  }
  1282  
  1283  func (r *BackendMock) MakeAccountAllDevices(ctx context.Context, tc *TestContext, accountID stellar1.AccountID) error {
  1284  	return remote.MakeAccountAllDevices(ctx, tc.G, accountID)
  1285  }
  1286  
  1287  func (r *BackendMock) ServerTimeboundsRecommendation(ctx context.Context, tc *TestContext) (stellar1.TimeboundsRecommendation, error) {
  1288  	// Call real timebounds endpoint for integration testing.
  1289  	return remote.ServerTimeboundsRecommendation(ctx, tc.G)
  1290  }
  1291  
  1292  func (r *BackendMock) SetInflationDestination(ctx context.Context, tc *TestContext, signedTx string) error {
  1293  	unpackedTx, _, err := unpackTx(signedTx)
  1294  	if err != nil {
  1295  		return err
  1296  	}
  1297  
  1298  	accountID := stellar1.AccountID(unpackedTx.Tx.SourceAccount.Address())
  1299  	account, ok := r.accounts[accountID]
  1300  	require.True(tc.T, ok)
  1301  	require.True(tc.T, account.availableBalance() != "0", "inflation on empty account won't work")
  1302  
  1303  	require.Len(tc.T, unpackedTx.Tx.Operations, 1)
  1304  	op := unpackedTx.Tx.Operations[0]
  1305  	require.Nil(tc.T, op.SourceAccount)
  1306  	require.Equal(tc.T, xdr.OperationTypeSetOptions, op.Body.Type)
  1307  	setOpt, ok := op.Body.GetSetOptionsOp()
  1308  	require.True(tc.T, ok)
  1309  	require.NotNil(tc.T, setOpt.InflationDest)
  1310  
  1311  	require.NotNil(tc.T, unpackedTx.Tx.TimeBounds, "We are expecting TimeBounds in all txs")
  1312  	if unpackedTx.Tx.TimeBounds != nil {
  1313  		require.NotZero(tc.T, unpackedTx.Tx.TimeBounds.MaxTime, "We are expecting non-zero TimeBounds.MaxTime in all txs")
  1314  		require.True(tc.T, time.Now().Before(time.Unix(int64(unpackedTx.Tx.TimeBounds.MaxTime), 0)))
  1315  		// We always send MinTime=0 but this assertion should still hold.
  1316  		require.True(tc.T, time.Now().After(time.Unix(int64(unpackedTx.Tx.TimeBounds.MinTime), 0)))
  1317  	}
  1318  
  1319  	account.inflationDest = stellar1.AccountID(setOpt.InflationDest.Address())
  1320  
  1321  	tc.T.Logf("BackendMock set inflation destination of %q to %q", accountID, account.inflationDest)
  1322  	return nil
  1323  }
  1324  
  1325  func (r *BackendMock) GetInflationDestinations(ctx context.Context, tc *TestContext) ([]stellar1.PredefinedInflationDestination, error) {
  1326  	// Call into real server for integration testing.
  1327  	return remote.GetInflationDestinations(ctx, tc.G)
  1328  }
  1329  
  1330  func (r *BackendMock) ChangeTrustline(ctx context.Context, tc *TestContext, signedTx string) error {
  1331  	unpackedTx, _, err := unpackTx(signedTx)
  1332  	if err != nil {
  1333  		return err
  1334  	}
  1335  
  1336  	accountID := stellar1.AccountID(unpackedTx.Tx.SourceAccount.Address())
  1337  	account, ok := r.accounts[accountID]
  1338  	require.True(tc.T, ok)
  1339  
  1340  	require.Len(tc.T, unpackedTx.Tx.Operations, 1)
  1341  	op := unpackedTx.Tx.Operations[0]
  1342  	require.Nil(tc.T, op.SourceAccount)
  1343  	require.Equal(tc.T, xdr.OperationTypeChangeTrust, op.Body.Type)
  1344  	setOpt, ok := op.Body.GetChangeTrustOp()
  1345  	require.True(tc.T, ok)
  1346  
  1347  	if setOpt.Limit == 0 {
  1348  		// Removing a trustline.
  1349  		var found bool
  1350  		for i, bal := range account.otherBalances {
  1351  			if bal.Asset.String() == setOpt.Line.String() {
  1352  				copy(account.otherBalances[i:], account.otherBalances[i+1:])
  1353  				account.otherBalances = account.otherBalances[:len(account.otherBalances)-1]
  1354  				found = true
  1355  				break
  1356  			}
  1357  		}
  1358  		if !found {
  1359  			return fmt.Errorf("invalid limit=0, trustline not found in account")
  1360  		}
  1361  		tc.T.Logf("BackendMock set limit removed trustline %s for account  %s", setOpt.Line.String(), accountID)
  1362  	} else {
  1363  		limitStr := stellarnet.StringFromStellarAmount(int64(setOpt.Limit))
  1364  		var found bool
  1365  		for i, bal := range account.otherBalances {
  1366  			if bal.Asset.String() == setOpt.Line.String() {
  1367  				account.otherBalances[i].Limit = limitStr
  1368  				found = true
  1369  				break
  1370  			}
  1371  		}
  1372  
  1373  		if found {
  1374  			tc.T.Logf("BackendMock set limit changed trustline %s limit to %s for account %s",
  1375  				setOpt.Line.String(), limitStr, accountID)
  1376  		} else {
  1377  			var t, c, i string
  1378  			if err := setOpt.Line.Extract(&t, &c, &i); err != nil {
  1379  				return err
  1380  			}
  1381  			account.otherBalances = append(account.otherBalances, stellar1.Balance{
  1382  				Asset: stellar1.Asset{
  1383  					Type:   t,
  1384  					Code:   c,
  1385  					Issuer: i,
  1386  				},
  1387  				Limit:        limitStr,
  1388  				Amount:       stellarnet.StringFromStellarAmount(0),
  1389  				IsAuthorized: true,
  1390  			})
  1391  			tc.T.Logf("BackendMock set limit added trustline %s with limit %s for account %s",
  1392  				setOpt.Line.String(), limitStr, accountID)
  1393  		}
  1394  	}
  1395  
  1396  	return nil
  1397  }
  1398  
  1399  // Friendbot sends someone XLM
  1400  func (r *BackendMock) Gift(accountID stellar1.AccountID, amount string) {
  1401  	r.Lock()
  1402  	defer r.Unlock()
  1403  	require.NotNil(r.T, r.accounts[accountID], "account for gift")
  1404  	amt, err := stellarnet.ParseStellarAmount(amount)
  1405  	require.NoError(r.T, err)
  1406  	r.accounts[accountID].AdjustBalance(amt)
  1407  }
  1408  
  1409  func (r *BackendMock) CreateFakeAsset(code string) stellar1.Asset {
  1410  	full, err := keypair.Random()
  1411  	require.NoError(r.T, err)
  1412  	assetType, err := stellar1.CreateNonNativeAssetType(code)
  1413  	require.NoError(r.T, err)
  1414  	return stellar1.Asset{
  1415  		Type:   assetType,
  1416  		Code:   code,
  1417  		Issuer: full.Address(),
  1418  	}
  1419  }
  1420  
  1421  func (r *BackendMock) AllAccountIDs(uid keybase1.UID) []stellar1.AccountID {
  1422  	r.Lock()
  1423  	defer r.Unlock()
  1424  
  1425  	return r.userAccounts[uid]
  1426  }
  1427  
  1428  func randomKeybaseTransactionID(t testing.TB) stellar1.KeybaseTransactionID {
  1429  	b, err := libkb.RandBytesWithSuffix(stellar1.KeybaseTransactionIDLen, stellar1.KeybaseTransactionIDSuffix)
  1430  	require.NoError(t, err)
  1431  	res, err := stellar1.KeybaseTransactionIDFromString(hex.EncodeToString(b))
  1432  	require.NoError(t, err)
  1433  	return res
  1434  }
  1435  
  1436  func extractMemo(tx xdr.Transaction) (memo, memoType string) {
  1437  	switch tx.Memo.Type {
  1438  	case xdr.MemoTypeMemoNone:
  1439  		return "", "none"
  1440  	case xdr.MemoTypeMemoText:
  1441  		return tx.Memo.MustText(), "text"
  1442  	case xdr.MemoTypeMemoId:
  1443  		return fmt.Sprintf("%d", tx.Memo.MustId()), "id"
  1444  	case xdr.MemoTypeMemoHash:
  1445  		h := tx.Memo.MustHash()
  1446  		return base64.StdEncoding.EncodeToString(h[:]), "hash"
  1447  	case xdr.MemoTypeMemoReturn:
  1448  		h := tx.Memo.MustRetHash()
  1449  		return base64.StdEncoding.EncodeToString(h[:]), "return"
  1450  	default:
  1451  		panic(fmt.Errorf("invalid memo type: %v", tx.Memo.Type))
  1452  	}
  1453  }