github.com/keybase/client/go@v0.0.0-20240309051027-028f7c731f8b/stellar/transform.go (about) 1 package stellar 2 3 import ( 4 "errors" 5 "fmt" 6 "strings" 7 "time" 8 9 "github.com/keybase/client/go/chat/utils" 10 "github.com/keybase/client/go/libkb" 11 "github.com/keybase/client/go/protocol/keybase1" 12 "github.com/keybase/client/go/protocol/stellar1" 13 "github.com/keybase/client/go/stellar/relays" 14 "github.com/keybase/client/go/stellar/remote" 15 "github.com/keybase/stellarnet" 16 ) 17 18 // TransformPaymentSummaryGeneric converts a stellar1.PaymentSummary (p) into a 19 // stellar1.PaymentLocal, without any modifications based on who is viewing the transaction. 20 func TransformPaymentSummaryGeneric(mctx libkb.MetaContext, p stellar1.PaymentSummary, oc OwnAccountLookupCache) (*stellar1.PaymentLocal, error) { 21 var emptyAccountID stellar1.AccountID 22 return transformPaymentSummary(mctx, p, oc, emptyAccountID) 23 } 24 25 // TransformPaymentSummaryAccount converts a stellar1.PaymentSummary (p) into a 26 // stellar1.PaymentLocal, from the perspective of an owner of accountID. 27 func TransformPaymentSummaryAccount(mctx libkb.MetaContext, p stellar1.PaymentSummary, oc OwnAccountLookupCache, accountID stellar1.AccountID) (*stellar1.PaymentLocal, error) { 28 return transformPaymentSummary(mctx, p, oc, accountID) 29 } 30 31 // transformPaymentSummary converts a stellar1.PaymentSummary (p) into a stellar1.PaymentLocal. 32 // accountID can be empty ("") and exchRate can be nil, if a generic response that isn't tied 33 // to an account is necessary. 34 func transformPaymentSummary(mctx libkb.MetaContext, p stellar1.PaymentSummary, oc OwnAccountLookupCache, accountID stellar1.AccountID) (*stellar1.PaymentLocal, error) { 35 typ, err := p.Typ() 36 if err != nil { 37 return nil, err 38 } 39 40 switch typ { 41 case stellar1.PaymentSummaryType_STELLAR: 42 return transformPaymentStellar(mctx, accountID, p.Stellar(), oc) 43 case stellar1.PaymentSummaryType_DIRECT: 44 return transformPaymentDirect(mctx, accountID, p.Direct(), oc) 45 case stellar1.PaymentSummaryType_RELAY: 46 return transformPaymentRelay(mctx, accountID, p.Relay(), oc) 47 default: 48 return nil, fmt.Errorf("unrecognized payment type: %T", typ) 49 } 50 } 51 52 func TransformRequestDetails(mctx libkb.MetaContext, details stellar1.RequestDetails) (*stellar1.RequestDetailsLocal, error) { 53 fromAssertion, err := lookupUsername(mctx, details.FromUser.Uid) 54 if err != nil { 55 return nil, err 56 } 57 58 loc := stellar1.RequestDetailsLocal{ 59 Id: details.Id, 60 FromAssertion: fromAssertion, 61 FromCurrentUser: mctx.G().GetMyUID().Equal(details.FromUser.Uid), 62 ToAssertion: details.ToAssertion, 63 Amount: details.Amount, 64 Asset: details.Asset, 65 Currency: details.Currency, 66 Status: details.Status, 67 } 68 69 if details.ToUser != nil { 70 loc.ToUserType = stellar1.ParticipantType_KEYBASE 71 } else { 72 loc.ToUserType = stellar1.ParticipantType_SBS 73 } 74 75 switch { 76 case details.Currency != nil: 77 amountDesc, err := FormatCurrency(mctx, details.Amount, *details.Currency, stellarnet.Round) 78 if err != nil { 79 amountDesc = details.Amount 80 mctx.Debug("error formatting external currency: %s", err) 81 } 82 loc.AmountDescription = fmt.Sprintf("%s %s", amountDesc, *details.Currency) 83 case details.Asset != nil: 84 var code string 85 if details.Asset.IsNativeXLM() { 86 code = "XLM" 87 if loc.FromCurrentUser { 88 loc.WorthAtRequestTime, _, _ = formatWorth(mctx, &details.FromDisplayAmount, &details.FromDisplayCurrency) 89 } else { 90 loc.WorthAtRequestTime, _, _ = formatWorth(mctx, &details.ToDisplayAmount, &details.ToDisplayCurrency) 91 } 92 } else { 93 code = details.Asset.Code 94 } 95 96 amountDesc, err := FormatAmountWithSuffix(mctx, details.Amount, false /* precisionTwo */, true /* simplify */, code) 97 if err != nil { 98 amountDesc = fmt.Sprintf("%s %s", details.Amount, code) 99 mctx.Debug("error formatting amount for asset: %s", err) 100 } 101 loc.AmountDescription = amountDesc 102 default: 103 return nil, errors.New("malformed request - currency/asset not defined") 104 } 105 106 return &loc, nil 107 } 108 109 // transformPaymentStellar converts a stellar1.PaymentSummaryStellar into a stellar1.PaymentLocal. 110 func transformPaymentStellar(mctx libkb.MetaContext, acctID stellar1.AccountID, p stellar1.PaymentSummaryStellar, oc OwnAccountLookupCache) (res *stellar1.PaymentLocal, err error) { 111 loc := stellar1.NewPaymentLocal(p.TxID, p.Ctime) 112 if p.IsAdvanced { 113 if len(p.Amount) > 0 { 114 // It is expected that there is no amount. 115 // But might as well future proof it so that if an amount shows up it gets formatted. 116 loc.AmountDescription, err = FormatAmountDescriptionAsset(mctx, p.Amount, p.Asset) 117 if err != nil { 118 loc.AmountDescription = "" 119 } 120 } 121 122 asset := p.Asset // p.Asset is also expected to be missing. 123 if p.Trustline != nil && asset.IsEmpty() { 124 asset = p.Trustline.Asset 125 } 126 if !asset.IsEmpty() && !asset.IsNativeXLM() { 127 loc.IssuerDescription = FormatAssetIssuerString(asset) 128 issuerAcc := stellar1.AccountID(asset.Issuer) 129 loc.IssuerAccountID = &issuerAcc 130 } 131 } else { 132 loc, err = newPaymentCommonLocal(mctx, p.TxID, p.Ctime, p.Amount, p.Asset) 133 if err != nil { 134 return nil, err 135 } 136 } 137 138 isSender := p.From.Eq(acctID) 139 isRecipient := p.To.Eq(acctID) 140 switch { 141 case isSender && isRecipient: 142 loc.Delta = stellar1.BalanceDelta_NONE 143 case isSender: 144 loc.Delta = stellar1.BalanceDelta_DECREASE 145 case isRecipient: 146 loc.Delta = stellar1.BalanceDelta_INCREASE 147 } 148 149 loc.AssetCode = p.Asset.Code 150 loc.FromAccountID = p.From 151 loc.FromType = stellar1.ParticipantType_STELLAR 152 loc.ToAccountID = &p.To 153 loc.ToType = stellar1.ParticipantType_STELLAR 154 fillOwnAccounts(mctx, loc, oc) 155 156 loc.StatusSimplified = stellar1.PaymentStatus_COMPLETED 157 loc.StatusDescription = strings.ToLower(loc.StatusSimplified.String()) 158 loc.IsInflation = p.IsInflation 159 loc.InflationSource = p.InflationSource 160 loc.SourceAsset = p.SourceAsset 161 loc.SourceAmountMax = p.SourceAmountMax 162 loc.SourceAmountActual = p.SourceAmountActual 163 sourceConvRate, err := stellarnet.GetStellarExchangeRate(loc.SourceAmountActual, p.Amount) 164 if err == nil { 165 loc.SourceConvRate = sourceConvRate 166 } else { 167 loc.SourceConvRate = "" 168 } 169 170 loc.IsAdvanced = p.IsAdvanced 171 loc.SummaryAdvanced = p.SummaryAdvanced 172 loc.Operations = p.Operations 173 loc.Unread = p.Unread 174 loc.Trustline = p.Trustline 175 176 return loc, nil 177 } 178 179 func formatWorthAtSendTime(mctx libkb.MetaContext, p stellar1.PaymentSummaryDirect, isSender bool) (worthAtSendTime, worthCurrencyAtSendTime string, err error) { 180 if p.DisplayCurrency == nil || len(*p.DisplayCurrency) == 0 { 181 if isSender { 182 return formatWorth(mctx, &p.FromDisplayAmount, &p.FromDisplayCurrency) 183 } 184 return formatWorth(mctx, &p.ToDisplayAmount, &p.ToDisplayCurrency) 185 } 186 // payment has a display currency, don't need this field 187 return "", "", nil 188 } 189 190 // transformPaymentDirect converts a stellar1.PaymentSummaryDirect into a stellar1.PaymentLocal. 191 func transformPaymentDirect(mctx libkb.MetaContext, acctID stellar1.AccountID, p stellar1.PaymentSummaryDirect, oc OwnAccountLookupCache) (*stellar1.PaymentLocal, error) { 192 loc, err := newPaymentCommonLocal(mctx, p.TxID, p.Ctime, p.Amount, p.Asset) 193 if err != nil { 194 return nil, err 195 } 196 197 isSender := p.FromStellar.Eq(acctID) 198 isRecipient := p.ToStellar.Eq(acctID) 199 switch { 200 case isSender && isRecipient: 201 loc.Delta = stellar1.BalanceDelta_NONE 202 case isSender: 203 loc.Delta = stellar1.BalanceDelta_DECREASE 204 case isRecipient: 205 loc.Delta = stellar1.BalanceDelta_INCREASE 206 } 207 208 loc.Worth, _, err = formatWorth(mctx, p.DisplayAmount, p.DisplayCurrency) 209 if err != nil { 210 return nil, err 211 } 212 213 loc.FromAccountID = p.FromStellar 214 loc.FromType = stellar1.ParticipantType_STELLAR 215 if username, err := lookupUsername(mctx, p.From.Uid); err == nil { 216 loc.FromUsername = username 217 loc.FromType = stellar1.ParticipantType_KEYBASE 218 } 219 220 loc.ToAccountID = &p.ToStellar 221 loc.ToType = stellar1.ParticipantType_STELLAR 222 if p.To != nil { 223 if username, err := lookupUsername(mctx, p.To.Uid); err == nil { 224 loc.ToUsername = username 225 loc.ToType = stellar1.ParticipantType_KEYBASE 226 } 227 } 228 229 loc.AssetCode = p.Asset.Code 230 231 fillOwnAccounts(mctx, loc, oc) 232 switch { 233 case loc.FromAccountName != "": 234 // we are sender 235 loc.WorthAtSendTime, _, err = formatWorthAtSendTime(mctx, p, true) 236 case loc.ToAccountName != "": 237 // we are recipient 238 loc.WorthAtSendTime, _, err = formatWorthAtSendTime(mctx, p, false) 239 } 240 if err != nil { 241 return nil, err 242 } 243 244 loc.StatusSimplified = p.TxStatus.ToPaymentStatus() 245 loc.StatusDescription = strings.ToLower(loc.StatusSimplified.String()) 246 loc.StatusDetail = p.TxErrMsg 247 248 loc.Note, loc.NoteErr = decryptNote(mctx, p.TxID, p.NoteB64) 249 250 loc.SourceAmountMax = p.SourceAmountMax 251 loc.SourceAmountActual = p.SourceAmountActual 252 loc.SourceAsset = p.SourceAsset 253 sourceConvRate, err := stellarnet.GetStellarExchangeRate(loc.SourceAmountActual, p.Amount) 254 if err == nil { 255 loc.SourceConvRate = sourceConvRate 256 } else { 257 loc.SourceConvRate = "" 258 } 259 loc.Unread = p.Unread 260 261 loc.FromAirdrop = p.FromAirdrop 262 263 return loc, nil 264 } 265 266 // transformPaymentRelay converts a stellar1.PaymentSummaryRelay into a stellar1.PaymentLocal. 267 func transformPaymentRelay(mctx libkb.MetaContext, acctID stellar1.AccountID, p stellar1.PaymentSummaryRelay, oc OwnAccountLookupCache) (*stellar1.PaymentLocal, error) { 268 loc, err := newPaymentCommonLocal(mctx, p.TxID, p.Ctime, p.Amount, stellar1.AssetNative()) 269 if err != nil { 270 return nil, err 271 } 272 273 // isSender compares uid but not eldest-seqno because relays can survive resets. 274 isSender := p.From.Uid.Equal(mctx.G().GetMyUID()) 275 loc.Delta = stellar1.BalanceDelta_INCREASE 276 if isSender { 277 loc.Delta = stellar1.BalanceDelta_DECREASE 278 } 279 280 loc.Worth, _, err = formatWorth(mctx, p.DisplayAmount, p.DisplayCurrency) 281 if err != nil { 282 return nil, err 283 } 284 285 loc.AssetCode = "XLM" // We can hardcode relay payments, since the asset will always be XLM 286 loc.FromAccountID = p.FromStellar 287 loc.FromUsername, err = lookupUsername(mctx, p.From.Uid) 288 if err != nil { 289 mctx.Debug("sender lookup failed: %s", err) 290 return nil, errors.New("sender lookup failed") 291 } 292 loc.FromType = stellar1.ParticipantType_KEYBASE 293 294 loc.ToAssertion = p.ToAssertion 295 loc.ToType = stellar1.ParticipantType_SBS 296 toName := loc.ToAssertion 297 if p.To != nil { 298 username, err := lookupUsername(mctx, p.To.Uid) 299 if err != nil { 300 mctx.Debug("recipient lookup failed: %s", err) 301 return nil, errors.New("recipient lookup failed") 302 } 303 loc.ToUsername = username 304 loc.ToType = stellar1.ParticipantType_KEYBASE 305 toName = username 306 } 307 308 if p.TxStatus != stellar1.TransactionStatus_SUCCESS { 309 // The funding tx is not complete. 310 loc.StatusSimplified = p.TxStatus.ToPaymentStatus() 311 loc.StatusDetail = p.TxErrMsg 312 } else { 313 loc.StatusSimplified = stellar1.PaymentStatus_CLAIMABLE 314 if isSender { 315 loc.StatusDetail = fmt.Sprintf("%v can claim this when they set up their wallet.", toName) 316 loc.ShowCancel = true 317 } 318 } 319 if p.Claim != nil { 320 loc.StatusSimplified = p.Claim.ToPaymentStatus() 321 loc.ToAccountID = &p.Claim.ToStellar 322 loc.ToType = stellar1.ParticipantType_STELLAR 323 loc.ToUsername = "" 324 loc.ToAccountName = "" 325 claimStatus := p.Claim.ToPaymentStatus() 326 if claimStatus != stellar1.PaymentStatus_ERROR { 327 // if there's a claim and it's not currently erroring, then hide the 328 // `cancel` button 329 loc.ShowCancel = false 330 } 331 if claimStatus == stellar1.PaymentStatus_CANCELED { 332 // canceled payment. blank out toAssertion and stow in originalToAssertion 333 // set delta to what it would have been had the payment completed 334 loc.ToAssertion = "" 335 loc.OriginalToAssertion = p.ToAssertion 336 loc.Delta = stellar1.BalanceDelta_INCREASE 337 if acctID == p.FromStellar { 338 loc.Delta = stellar1.BalanceDelta_DECREASE 339 } 340 } 341 if username, err := lookupUsername(mctx, p.Claim.To.Uid); err == nil { 342 loc.ToUsername = username 343 loc.ToType = stellar1.ParticipantType_KEYBASE 344 } 345 if p.Claim.TxStatus == stellar1.TransactionStatus_SUCCESS { 346 // If the claim succeeded, the relay payment is done. 347 loc.StatusDetail = "" 348 } else { 349 claimantUsername, err := lookupUsername(mctx, p.Claim.To.Uid) 350 if err != nil { 351 return nil, err 352 } 353 if p.Claim.TxErrMsg != "" { 354 loc.StatusDetail = p.Claim.TxErrMsg 355 } else { 356 words := "is in the works" 357 switch p.Claim.TxStatus { 358 case stellar1.TransactionStatus_PENDING: 359 words = "is pending" 360 case stellar1.TransactionStatus_SUCCESS: 361 words = "has succeeded" 362 case stellar1.TransactionStatus_ERROR_TRANSIENT, stellar1.TransactionStatus_ERROR_PERMANENT: 363 words = "has failed" 364 } 365 loc.StatusDetail = fmt.Sprintf("Funded. %v's claim %v.", claimantUsername, words) 366 } 367 } 368 } 369 loc.StatusDescription = strings.ToLower(loc.StatusSimplified.String()) 370 fillOwnAccounts(mctx, loc, oc) 371 372 relaySecrets, err := relays.DecryptB64(mctx, p.TeamID, p.BoxB64) 373 if err == nil { 374 loc.Note = relaySecrets.Note 375 } else { 376 loc.NoteErr = fmt.Sprintf("error decrypting note: %s", err) 377 } 378 379 loc.FromAirdrop = p.FromAirdrop 380 381 return loc, nil 382 } 383 384 func formatWorth(mctx libkb.MetaContext, amount, currency *string) (worth, worthCurrency string, err error) { 385 if amount == nil || currency == nil { 386 return "", "", nil 387 } 388 389 if len(*amount) == 0 || len(*currency) == 0 { 390 return "", "", nil 391 } 392 393 worth, err = FormatCurrencyWithCodeSuffix(mctx, *amount, stellar1.OutsideCurrencyCode(*currency), stellarnet.Round) 394 if err != nil { 395 return "", "", err 396 } 397 398 return worth, *currency, nil 399 } 400 401 func lookupUsername(mctx libkb.MetaContext, uid keybase1.UID) (string, error) { 402 uname, err := mctx.G().GetUPAKLoader().LookupUsername(mctx.Ctx(), uid) 403 if err != nil { 404 return "", err 405 } 406 return uname.String(), nil 407 } 408 409 func fillOwnAccounts(mctx libkb.MetaContext, loc *stellar1.PaymentLocal, oc OwnAccountLookupCache) { 410 lookupOwnAccountQuick := func(accountID *stellar1.AccountID) (accountName string) { 411 if accountID == nil { 412 return "" 413 } 414 own, name, err := oc.OwnAccount(mctx.Ctx(), *accountID) 415 if err != nil || !own { 416 return "" 417 } 418 if name != "" { 419 return name 420 } 421 return accountID.String() 422 } 423 loc.FromAccountName = lookupOwnAccountQuick(&loc.FromAccountID) 424 loc.ToAccountName = lookupOwnAccountQuick(loc.ToAccountID) 425 if loc.FromAccountName != "" && loc.ToAccountName != "" { 426 loc.FromType = stellar1.ParticipantType_OWNACCOUNT 427 loc.ToType = stellar1.ParticipantType_OWNACCOUNT 428 } 429 } 430 431 func decryptNote(mctx libkb.MetaContext, txid stellar1.TransactionID, note string) (plaintext, errOutput string) { 432 if len(note) == 0 { 433 return "", "" 434 } 435 436 decrypted, err := NoteDecryptB64(mctx, note) 437 if err != nil { 438 return "", fmt.Sprintf("failed to decrypt payment note: %s", err) 439 } 440 441 if decrypted.StellarID != txid { 442 return "", "discarded note for wrong transaction ID" 443 } 444 445 return utils.EscapeForDecorate(mctx.Ctx(), decrypted.Note), "" 446 } 447 448 func newPaymentCommonLocal(mctx libkb.MetaContext, txID stellar1.TransactionID, ctime stellar1.TimeMs, amount string, asset stellar1.Asset) (*stellar1.PaymentLocal, error) { 449 loc := stellar1.NewPaymentLocal(txID, ctime) 450 451 formatted, err := FormatAmountDescriptionAsset(mctx, amount, asset) 452 if err != nil { 453 return nil, err 454 } 455 loc.AmountDescription = formatted 456 457 if !asset.IsNativeXLM() { 458 loc.IssuerDescription = FormatAssetIssuerString(asset) 459 issuerAcc := stellar1.AccountID(asset.Issuer) 460 loc.IssuerAccountID = &issuerAcc 461 } 462 463 return loc, nil 464 } 465 466 func RemoteRecentPaymentsToPage(mctx libkb.MetaContext, remoter remote.Remoter, accountID stellar1.AccountID, remotePage stellar1.PaymentsPage) (page stellar1.PaymentsPageLocal, err error) { 467 oc := NewOwnAccountLookupCache(mctx) 468 page.Payments = make([]stellar1.PaymentOrErrorLocal, len(remotePage.Payments)) 469 for i, p := range remotePage.Payments { 470 page.Payments[i].Payment, err = TransformPaymentSummaryAccount(mctx, p, oc, accountID) 471 if err != nil { 472 mctx.Debug("RemoteRecentPaymentsToPage error transforming payment %v: %v", i, err) 473 s := err.Error() 474 page.Payments[i].Err = &s 475 page.Payments[i].Payment = nil // just to make sure 476 } 477 } 478 page.Cursor = remotePage.Cursor 479 480 if remotePage.OldestUnread != nil { 481 oldestUnread := stellar1.NewPaymentID(*remotePage.OldestUnread) 482 page.OldestUnread = &oldestUnread 483 } 484 485 return page, nil 486 487 } 488 489 func RemotePendingToLocal(mctx libkb.MetaContext, remoter remote.Remoter, accountID stellar1.AccountID, pending []stellar1.PaymentSummary) (payments []stellar1.PaymentOrErrorLocal, err error) { 490 oc := NewOwnAccountLookupCache(mctx) 491 492 payments = make([]stellar1.PaymentOrErrorLocal, len(pending)) 493 for i, p := range pending { 494 payment, err := TransformPaymentSummaryAccount(mctx, p, oc, accountID) 495 if err != nil { 496 s := err.Error() 497 payments[i].Err = &s 498 payments[i].Payment = nil // just to make sure 499 500 } else { 501 payments[i].Payment = payment 502 payments[i].Err = nil 503 } 504 } 505 506 return payments, nil 507 } 508 509 func AccountDetailsToWalletAccountLocal(mctx libkb.MetaContext, accountID stellar1.AccountID, details stellar1.AccountDetails, 510 isPrimary bool, accountName string, accountMode stellar1.AccountMode) (stellar1.WalletAccountLocal, error) { 511 512 var empty stellar1.WalletAccountLocal 513 balance, err := balanceList(details.Balances).balanceDescription(mctx) 514 if err != nil { 515 return empty, err 516 } 517 518 activeDeviceType, err := mctx.G().ActiveDevice.DeviceType(mctx) 519 if err != nil { 520 return empty, err 521 } 522 isMobile := activeDeviceType == keybase1.DeviceTypeV2_MOBILE 523 524 // AccountModeEditable - can user change "account mode" to mobile only or 525 // back? This setting can only be changed from a mobile device that's over 526 // 7 days old (since provisioning). 527 editable := false 528 if isMobile { 529 ctime, err := mctx.G().ActiveDevice.Ctime(mctx) 530 if err != nil { 531 return empty, err 532 } 533 deviceProvisionedAt := time.Unix(int64(ctime)/1000, 0) 534 deviceAge := mctx.G().Clock().Since(deviceProvisionedAt) 535 if deviceAge > 7*24*time.Hour { 536 editable = true 537 } 538 } 539 540 // AccountDeviceReadOnly - if account is mobileOnly and current device is 541 // either desktop, or mobile but not only enough (7 days since 542 // provisioning). 543 readOnly := false 544 if accountMode == stellar1.AccountMode_MOBILE { 545 if isMobile { 546 // Mobile devices eligible to edit are also eligible to do 547 // transactions. 548 readOnly = !editable 549 } else { 550 // All desktop devices are read only. 551 readOnly = true 552 } 553 } 554 555 // Is there enough to make any transaction? 556 var availableInt int64 557 if details.Available != "" { 558 availableInt, err = stellarnet.ParseStellarAmount(details.Available) 559 if err != nil { 560 return empty, err 561 } 562 } 563 baseFee := getGlobal(mctx.G()).BaseFee(mctx) 564 // TODO: this is something that stellard can just tell us. 565 isFunded, err := hasPositiveLumenBalance(details.Balances) 566 if err != nil { 567 return empty, err 568 } 569 const trustlineReserveStroops int64 = stellarnet.StroopsPerLumen / 2 570 571 acct := stellar1.WalletAccountLocal{ 572 AccountID: accountID, 573 IsDefault: isPrimary, 574 Name: accountName, 575 BalanceDescription: balance, 576 Seqno: details.Seqno, 577 AccountMode: accountMode, 578 AccountModeEditable: editable, 579 DeviceReadOnly: readOnly, 580 IsFunded: isFunded, 581 CanSubmitTx: availableInt > int64(baseFee), 582 CanAddTrustline: availableInt > int64(baseFee)+trustlineReserveStroops, 583 } 584 585 conf, err := mctx.G().GetStellar().GetServerDefinitions(mctx.Ctx()) 586 if err == nil { 587 for _, currency := range []string{details.DisplayCurrency, DefaultCurrencySetting} { 588 currency, ok := conf.GetCurrencyLocal(stellar1.OutsideCurrencyCode(currency)) 589 if ok { 590 acct.CurrencyLocal = currency 591 break 592 } 593 } 594 } 595 if acct.CurrencyLocal.Code == "" { 596 mctx.Debug("warning: AccountDetails for %v has empty currency code", details.AccountID) 597 } 598 599 return acct, nil 600 } 601 602 type balanceList []stellar1.Balance 603 604 // Example: "56.0227002 XLM + more" 605 func (a balanceList) balanceDescription(mctx libkb.MetaContext) (res string, err error) { 606 var more bool 607 for _, b := range a { 608 if b.Asset.IsNativeXLM() { 609 res, err = FormatAmountDescriptionXLM(mctx, b.Amount) 610 if err != nil { 611 return "", err 612 } 613 } else { 614 more = true 615 } 616 } 617 if res == "" { 618 res = "0 XLM" 619 } 620 if more { 621 res += " + more" 622 } 623 return res, nil 624 } 625 626 // TransformToAirdropStatus takes the result from api server status_check 627 // and transforms it into stellar1.AirdropStatus. 628 func TransformToAirdropStatus(status remote.AirdropStatusAPI) stellar1.AirdropStatus { 629 var out stellar1.AirdropStatus 630 switch { 631 case status.AlreadyRegistered: 632 out.State = stellar1.AirdropAccepted 633 case status.Qualifications.QualifiesOverall: 634 out.State = stellar1.AirdropQualified 635 default: 636 out.State = stellar1.AirdropUnqualified 637 } 638 639 dq := stellar1.AirdropQualification{ 640 Title: status.AirdropConfig.MinActiveDevicesTitle, 641 Valid: status.Qualifications.HasEnoughDevices, 642 } 643 out.Rows = append(out.Rows, dq) 644 645 aq := stellar1.AirdropQualification{ 646 Title: status.AirdropConfig.AccountCreationTitle, 647 } 648 649 var used []string 650 for k, q := range status.Qualifications.ServiceChecks { 651 if q.Qualifies { 652 aq.Valid = true 653 break 654 } 655 if q.Username == "" { 656 continue 657 } 658 if !q.IsOldEnough { 659 continue 660 } 661 if q.IsUsedAlready { 662 used = append(used, fmt.Sprintf("%s@%s", q.Username, k)) 663 } 664 } 665 if !aq.Valid { 666 aq.Subtitle = status.AirdropConfig.AccountCreationSubtitle 667 if len(used) > 0 { 668 usedDisplay := strings.Join(used, ", ") 669 aq.Subtitle += " " + fmt.Sprintf(status.AirdropConfig.AccountUsed, usedDisplay) 670 } 671 } 672 out.Rows = append(out.Rows, aq) 673 674 return out 675 }