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