github.com/keybase/client/go@v0.0.0-20240309051027-028f7c731f8b/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 }