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