github.com/keybase/client/go@v0.0.0-20241007131713-f10651d043c8/stellar/remote/remote.go (about) 1 package remote 2 3 import ( 4 "context" 5 "encoding/json" 6 "errors" 7 "fmt" 8 "strconv" 9 "strings" 10 "time" 11 12 "github.com/keybase/client/go/libkb" 13 "github.com/keybase/client/go/protocol/keybase1" 14 "github.com/keybase/client/go/protocol/stellar1" 15 "github.com/keybase/client/go/stellar/bundle" 16 ) 17 18 var ErrAccountIDMissing = errors.New("account id parameter missing") 19 20 type shouldCreateRes struct { 21 libkb.AppStatusEmbed 22 ShouldCreateResult 23 } 24 25 type ShouldCreateResult struct { 26 ShouldCreate bool `json:"shouldcreate"` 27 HasWallet bool `json:"haswallet"` 28 AcceptedDisclaimer bool `json:"accepteddisclaimer"` 29 } 30 31 // ShouldCreate asks the server whether to create this user's initial wallet. 32 func ShouldCreate(ctx context.Context, g *libkb.GlobalContext) (res ShouldCreateResult, err error) { 33 mctx := libkb.NewMetaContext(ctx, g) 34 defer mctx.Trace("Stellar.ShouldCreate", &err)() 35 defer func() { 36 mctx.Debug("Stellar.ShouldCreate: (res:%+v, err:%v)", res, err != nil) 37 }() 38 arg := libkb.NewAPIArg("stellar/shouldcreate") 39 arg.RetryCount = 3 40 arg.SessionType = libkb.APISessionTypeREQUIRED 41 var apiRes shouldCreateRes 42 err = mctx.G().API.GetDecode(mctx, arg, &apiRes) 43 return apiRes.ShouldCreateResult, err 44 } 45 46 func buildChainLinkPayload(m libkb.MetaContext, b stellar1.Bundle, me *libkb.User, pukGen keybase1.PerUserKeyGeneration, pukSeed libkb.PerUserKeySeed, deviceSigKey libkb.GenericKey) (*libkb.JSONPayload, keybase1.Seqno, libkb.LinkID, error) { 47 err := b.CheckInvariants() 48 if err != nil { 49 return nil, 0, nil, err 50 } 51 if len(b.Accounts) < 1 { 52 return nil, 0, nil, errors.New("stellar bundle has no accounts") 53 } 54 // Find the new primary account for the chain link. 55 stellarAccount, err := b.PrimaryAccount() 56 if err != nil { 57 return nil, 0, nil, err 58 } 59 stellarAccountBundle, ok := b.AccountBundles[stellarAccount.AccountID] 60 if !ok { 61 return nil, 0, nil, errors.New("stellar primary account has no account bundle") 62 } 63 if len(stellarAccountBundle.Signers) < 1 { 64 return nil, 0, nil, errors.New("stellar bundle has no signers") 65 } 66 if !stellarAccount.IsPrimary { 67 return nil, 0, nil, errors.New("initial stellar account is not primary") 68 } 69 m.Debug("Stellar.PostWithChainLink: revision:%v accountID:%v pukGen:%v", b.Revision, stellarAccount.AccountID, pukGen) 70 71 boxed, err := bundle.BoxAndEncode(&b, pukGen, pukSeed) 72 if err != nil { 73 return nil, 0, nil, err 74 } 75 76 m.Debug("Stellar.PostWithChainLink: make sigs") 77 78 sig, err := libkb.StellarProofReverseSigned(m, me, stellarAccount.AccountID, stellarAccountBundle.Signers[0], deviceSigKey) 79 if err != nil { 80 return nil, 0, nil, err 81 } 82 83 payload := make(libkb.JSONPayload) 84 payload["sigs"] = []libkb.JSONPayload{sig.Payload} 85 section := make(libkb.JSONPayload) 86 section["encrypted_parent"] = boxed.EncParentB64 87 section["visible_parent"] = boxed.VisParentB64 88 section["version_parent"] = boxed.FormatVersionParent 89 section["account_bundles"] = boxed.AcctBundles 90 payload["stellar"] = section 91 92 return &payload, sig.Seqno, sig.LinkID, nil 93 } 94 95 // Post a bundle to the server with a chainlink. 96 func PostWithChainlink(mctx libkb.MetaContext, clearBundle stellar1.Bundle) (err error) { 97 defer mctx.Trace("Stellar.PostWithChainlink", &err)() 98 99 uid := mctx.G().ActiveDevice.UID() 100 if uid.IsNil() { 101 return libkb.NoUIDError{} 102 } 103 mctx.Debug("Stellar.PostWithChainLink: load self") 104 loadMeArg := libkb.NewLoadUserArg(mctx.G()). 105 WithNetContext(mctx.Ctx()). 106 WithUID(uid). 107 WithSelf(true). 108 WithPublicKeyOptional() 109 me, err := libkb.LoadUser(loadMeArg) 110 if err != nil { 111 return err 112 } 113 114 deviceSigKey, err := mctx.G().ActiveDevice.SigningKey() 115 if err != nil { 116 return fmt.Errorf("signing key not found: (%v)", err) 117 } 118 pukGen, pukSeed, err := getLatestPuk(mctx.Ctx(), mctx.G()) 119 if err != nil { 120 return err 121 } 122 123 payload, seqno, linkID, err := buildChainLinkPayload(mctx, clearBundle, me, pukGen, pukSeed, deviceSigKey) 124 if err != nil { 125 return err 126 } 127 128 mctx.Debug("Stellar.PostWithChainLink: post") 129 _, err = mctx.G().API.PostJSON(mctx, libkb.APIArg{ 130 Endpoint: "key/multi", 131 SessionType: libkb.APISessionTypeREQUIRED, 132 JSONPayload: *payload, 133 }) 134 if err != nil { 135 return err 136 } 137 if err = libkb.MerkleCheckPostedUserSig(mctx, uid, seqno, linkID); err != nil { 138 return err 139 } 140 141 mctx.G().UserChanged(mctx.Ctx(), uid) 142 return nil 143 } 144 145 // Post a bundle to the server. 146 func Post(mctx libkb.MetaContext, clearBundle stellar1.Bundle) (err error) { 147 defer mctx.Trace("Stellar.Post", &err)() 148 149 err = clearBundle.CheckInvariants() 150 if err != nil { 151 return err 152 } 153 pukGen, pukSeed, err := getLatestPuk(mctx.Ctx(), mctx.G()) 154 if err != nil { 155 return err 156 } 157 boxed, err := bundle.BoxAndEncode(&clearBundle, pukGen, pukSeed) 158 if err != nil { 159 return err 160 } 161 162 payload := make(libkb.JSONPayload) 163 section := make(libkb.JSONPayload) 164 section["encrypted_parent"] = boxed.EncParentB64 165 section["visible_parent"] = boxed.VisParentB64 166 section["version_parent"] = boxed.FormatVersionParent 167 section["account_bundles"] = boxed.AcctBundles 168 payload["stellar"] = section 169 _, err = mctx.G().API.PostJSON(mctx, libkb.APIArg{ 170 Endpoint: "stellar/acctbundle", 171 SessionType: libkb.APISessionTypeREQUIRED, 172 JSONPayload: payload, 173 }) 174 return err 175 } 176 177 func fetchBundleForAccount(mctx libkb.MetaContext, accountID *stellar1.AccountID) ( 178 b *stellar1.Bundle, bv stellar1.BundleVersion, pukGen keybase1.PerUserKeyGeneration, accountGens bundle.AccountPukGens, err error) { 179 defer mctx.Trace("Stellar.fetchBundleForAccount", &err)() 180 181 fetchArgs := libkb.HTTPArgs{} 182 if accountID != nil { 183 fetchArgs = libkb.HTTPArgs{"account_id": libkb.S{Val: string(*accountID)}} 184 } 185 apiArg := libkb.APIArg{ 186 Endpoint: "stellar/acctbundle", 187 SessionType: libkb.APISessionTypeREQUIRED, 188 Args: fetchArgs, 189 RetryCount: 3, 190 InitialTimeout: 10 * time.Second, 191 } 192 var apiRes fetchAcctRes 193 if err := mctx.G().API.GetDecode(mctx, apiArg, &apiRes); err != nil { 194 return nil, 0, 0, accountGens, err 195 } 196 197 finder := &pukFinder{} 198 b, bv, pukGen, accountGens, err = bundle.DecodeAndUnbox(mctx, finder, apiRes.BundleEncoded) 199 if err != nil { 200 return b, bv, pukGen, accountGens, err 201 } 202 mctx.G().GetStellar().InformBundle(mctx, b.Revision, b.Accounts) 203 return b, bv, pukGen, accountGens, err 204 } 205 206 // FetchSecretlessBundle gets an account bundle from the server and decrypts it 207 // but without any specified AccountID and therefore no secrets (signers). 208 // This method is safe to be called by any of a user's devices even if one or more of 209 // the accounts is marked as mobile only. 210 func FetchSecretlessBundle(mctx libkb.MetaContext) (bundle *stellar1.Bundle, err error) { 211 defer mctx.Trace("Stellar.FetchSecretlessBundle", &err)() 212 213 bundle, _, _, _, err = fetchBundleForAccount(mctx, nil) 214 return bundle, err 215 } 216 217 // FetchAccountBundle gets a bundle from the server with all of the accounts 218 // in it, but it will only have the secrets for the specified accountID. 219 // This method will bubble up an error if it's called by a Desktop device for 220 // an account that is mobile only. If you don't need the secrets, use 221 // FetchSecretlessBundle instead. 222 func FetchAccountBundle(mctx libkb.MetaContext, accountID stellar1.AccountID) (bundle *stellar1.Bundle, err error) { 223 defer mctx.Trace("Stellar.FetchAccountBundle", &err)() 224 225 bundle, _, _, _, err = fetchBundleForAccount(mctx, &accountID) 226 return bundle, err 227 } 228 229 // FetchBundleWithGens gets a bundle with all of the secrets in it to which this device 230 // has access, i.e. if there are no mobile-only accounts, then this bundle will have 231 // all of the secrets. Also returned is a map of accountID->pukGen. Entries are only in the 232 // map for accounts with secrets in the bundle. Inaccessible accounts will be in the 233 // visible part of the parent bundle but not in the AccountBundle secrets nor in the 234 // AccountPukGens map. FetchBundleWithGens is only for very specific usecases. 235 // FetchAccountBundle and FetchSecretlessBundle are the preferred ways to pull a bundle. 236 func FetchBundleWithGens(mctx libkb.MetaContext) (b *stellar1.Bundle, pukGen keybase1.PerUserKeyGeneration, accountGens bundle.AccountPukGens, err error) { 237 defer mctx.Trace("Stellar.FetchBundleWithGens", &err)() 238 239 b, _, pukGen, _, err = fetchBundleForAccount(mctx, nil) // this bundle no account secrets 240 if err != nil { 241 return nil, 0, bundle.AccountPukGens{}, err 242 } 243 accountGens = make(bundle.AccountPukGens) 244 newAccBundles := make(map[stellar1.AccountID]stellar1.AccountBundle) 245 for _, acct := range b.Accounts { 246 singleBundle, _, _, singleAccountGens, err := fetchBundleForAccount(mctx, &acct.AccountID) 247 if err != nil { 248 // expected errors include SCStellarDeviceNotMobile, SCStellarMobileOnlyPurgatory 249 mctx.Debug("unable to pull secrets for account %v which is not necessarily a problem %v", acct.AccountID, err) 250 continue 251 } 252 accBundle := singleBundle.AccountBundles[acct.AccountID] 253 newAccBundles[acct.AccountID] = accBundle 254 accountGens[acct.AccountID] = singleAccountGens[acct.AccountID] 255 } 256 b.AccountBundles = newAccBundles 257 err = b.CheckInvariants() 258 if err != nil { 259 return nil, 0, bundle.AccountPukGens{}, err 260 } 261 262 return b, pukGen, accountGens, nil 263 } 264 265 func getLatestPuk(ctx context.Context, g *libkb.GlobalContext) (pukGen keybase1.PerUserKeyGeneration, pukSeed libkb.PerUserKeySeed, err error) { 266 pukring, err := g.GetPerUserKeyring(ctx) 267 if err != nil { 268 return pukGen, pukSeed, err 269 } 270 m := libkb.NewMetaContext(ctx, g) 271 err = pukring.Sync(m) 272 if err != nil { 273 return pukGen, pukSeed, err 274 } 275 pukGen = pukring.CurrentGeneration() 276 pukSeed, err = pukring.GetSeedByGeneration(m, pukGen) 277 return pukGen, pukSeed, err 278 } 279 280 type fetchAcctRes struct { 281 libkb.AppStatusEmbed 282 bundle.BundleEncoded 283 } 284 285 type seqnoResult struct { 286 libkb.AppStatusEmbed 287 AccountSeqno string `json:"seqno"` 288 } 289 290 func AccountSeqno(ctx context.Context, g *libkb.GlobalContext, accountID stellar1.AccountID) (uint64, error) { 291 mctx := libkb.NewMetaContext(ctx, g) 292 apiArg := libkb.APIArg{ 293 Endpoint: "stellar/accountseqno", 294 SessionType: libkb.APISessionTypeREQUIRED, 295 Args: libkb.HTTPArgs{"account_id": libkb.S{Val: string(accountID)}}, 296 RetryCount: 3, 297 RetryMultiplier: 1.5, 298 InitialTimeout: 10 * time.Second, 299 } 300 301 var res seqnoResult 302 if err := mctx.G().API.GetDecode(mctx, apiArg, &res); err != nil { 303 return 0, err 304 } 305 306 seqno, err := strconv.ParseUint(res.AccountSeqno, 10, 64) 307 if err != nil { 308 return 0, err 309 } 310 311 return seqno, nil 312 } 313 314 type balancesResult struct { 315 Status libkb.AppStatus `json:"status"` 316 Balances []stellar1.Balance `json:"balances"` 317 } 318 319 func (b *balancesResult) GetAppStatus() *libkb.AppStatus { 320 return &b.Status 321 } 322 323 func Balances(ctx context.Context, g *libkb.GlobalContext, accountID stellar1.AccountID) ([]stellar1.Balance, error) { 324 mctx := libkb.NewMetaContext(ctx, g) 325 apiArg := libkb.APIArg{ 326 Endpoint: "stellar/balances", 327 SessionType: libkb.APISessionTypeREQUIRED, 328 Args: libkb.HTTPArgs{"account_id": libkb.S{Val: string(accountID)}}, 329 RetryCount: 3, 330 RetryMultiplier: 1.5, 331 InitialTimeout: 10 * time.Second, 332 } 333 334 var res balancesResult 335 if err := mctx.G().API.GetDecode(mctx, apiArg, &res); err != nil { 336 return nil, err 337 } 338 339 return res.Balances, nil 340 } 341 342 type detailsResult struct { 343 Status libkb.AppStatus `json:"status"` 344 Details stellar1.AccountDetails `json:"details"` 345 } 346 347 func (b *detailsResult) GetAppStatus() *libkb.AppStatus { 348 return &b.Status 349 } 350 351 func Details(ctx context.Context, g *libkb.GlobalContext, accountID stellar1.AccountID) (stellar1.AccountDetails, error) { 352 // the endpoint requires the account_id parameter, so check it exists 353 if strings.TrimSpace(accountID.String()) == "" { 354 return stellar1.AccountDetails{}, ErrAccountIDMissing 355 } 356 mctx := libkb.NewMetaContext(ctx, g) 357 358 apiArg := libkb.APIArg{ 359 Endpoint: "stellar/details", 360 SessionType: libkb.APISessionTypeREQUIRED, 361 Args: libkb.HTTPArgs{ 362 "account_id": libkb.S{Val: string(accountID)}, 363 "include_multi": libkb.B{Val: true}, 364 "include_advanced": libkb.B{Val: true}, 365 }, 366 RetryCount: 3, 367 RetryMultiplier: 1.5, 368 InitialTimeout: 10 * time.Second, 369 } 370 371 var res detailsResult 372 if err := mctx.G().API.GetDecode(mctx, apiArg, &res); err != nil { 373 return stellar1.AccountDetails{}, err 374 } 375 res.Details.SetDefaultDisplayCurrency() 376 377 return res.Details, nil 378 } 379 380 type submitResult struct { 381 libkb.AppStatusEmbed 382 PaymentResult stellar1.PaymentResult `json:"payment_result"` 383 } 384 385 func SubmitPayment(ctx context.Context, g *libkb.GlobalContext, post stellar1.PaymentDirectPost) (stellar1.PaymentResult, error) { 386 payload := make(libkb.JSONPayload) 387 payload["payment"] = post 388 apiArg := libkb.APIArg{ 389 Endpoint: "stellar/submitpayment", 390 SessionType: libkb.APISessionTypeREQUIRED, 391 JSONPayload: payload, 392 } 393 var res submitResult 394 mctx := libkb.NewMetaContext(ctx, g) 395 if err := g.API.PostDecode(mctx, apiArg, &res); err != nil { 396 return stellar1.PaymentResult{}, err 397 } 398 return res.PaymentResult, nil 399 } 400 401 func SubmitRelayPayment(ctx context.Context, g *libkb.GlobalContext, post stellar1.PaymentRelayPost) (stellar1.PaymentResult, error) { 402 payload := make(libkb.JSONPayload) 403 payload["payment"] = post 404 apiArg := libkb.APIArg{ 405 Endpoint: "stellar/submitrelaypayment", 406 SessionType: libkb.APISessionTypeREQUIRED, 407 JSONPayload: payload, 408 } 409 var res submitResult 410 mctx := libkb.NewMetaContext(ctx, g) 411 if err := g.API.PostDecode(mctx, apiArg, &res); err != nil { 412 return stellar1.PaymentResult{}, err 413 } 414 return res.PaymentResult, nil 415 } 416 417 type submitMultiResult struct { 418 libkb.AppStatusEmbed 419 SubmitMultiRes stellar1.SubmitMultiRes `json:"submit_multi_result"` 420 } 421 422 func SubmitMultiPayment(ctx context.Context, g *libkb.GlobalContext, post stellar1.PaymentMultiPost) (stellar1.SubmitMultiRes, error) { 423 payload := make(libkb.JSONPayload) 424 payload["payment"] = post 425 apiArg := libkb.APIArg{ 426 Endpoint: "stellar/submitmultipayment", 427 SessionType: libkb.APISessionTypeREQUIRED, 428 JSONPayload: payload, 429 } 430 var res submitMultiResult 431 mctx := libkb.NewMetaContext(ctx, g) 432 if err := g.API.PostDecode(mctx, apiArg, &res); err != nil { 433 return stellar1.SubmitMultiRes{}, err 434 } 435 return res.SubmitMultiRes, nil 436 } 437 438 type submitClaimResult struct { 439 libkb.AppStatusEmbed 440 RelayClaimResult stellar1.RelayClaimResult `json:"claim_result"` 441 } 442 443 func SubmitRelayClaim(ctx context.Context, g *libkb.GlobalContext, post stellar1.RelayClaimPost) (stellar1.RelayClaimResult, error) { 444 payload := make(libkb.JSONPayload) 445 payload["claim"] = post 446 apiArg := libkb.APIArg{ 447 Endpoint: "stellar/submitrelayclaim", 448 SessionType: libkb.APISessionTypeREQUIRED, 449 JSONPayload: payload, 450 } 451 var res submitClaimResult 452 mctx := libkb.NewMetaContext(ctx, g) 453 if err := g.API.PostDecode(mctx, apiArg, &res); err != nil { 454 return stellar1.RelayClaimResult{}, err 455 } 456 return res.RelayClaimResult, nil 457 } 458 459 type acquireAutoClaimLockResult struct { 460 libkb.AppStatusEmbed 461 Result string `json:"result"` 462 } 463 464 func AcquireAutoClaimLock(ctx context.Context, g *libkb.GlobalContext) (string, error) { 465 apiArg := libkb.APIArg{ 466 Endpoint: "stellar/acquireautoclaimlock", 467 SessionType: libkb.APISessionTypeREQUIRED, 468 } 469 var res acquireAutoClaimLockResult 470 mctx := libkb.NewMetaContext(ctx, g) 471 if err := g.API.PostDecode(mctx, apiArg, &res); err != nil { 472 return "", err 473 } 474 return res.Result, nil 475 } 476 477 func ReleaseAutoClaimLock(ctx context.Context, g *libkb.GlobalContext, token string) error { 478 payload := make(libkb.JSONPayload) 479 payload["token"] = token 480 apiArg := libkb.APIArg{ 481 Endpoint: "stellar/releaseautoclaimlock", 482 SessionType: libkb.APISessionTypeREQUIRED, 483 JSONPayload: payload, 484 } 485 var res libkb.AppStatusEmbed 486 mctx := libkb.NewMetaContext(ctx, g) 487 return g.API.PostDecode(mctx, apiArg, &res) 488 } 489 490 type nextAutoClaimResult struct { 491 libkb.AppStatusEmbed 492 Result *stellar1.AutoClaim `json:"result"` 493 } 494 495 func NextAutoClaim(ctx context.Context, g *libkb.GlobalContext) (*stellar1.AutoClaim, error) { 496 apiArg := libkb.APIArg{ 497 Endpoint: "stellar/nextautoclaim", 498 SessionType: libkb.APISessionTypeREQUIRED, 499 } 500 var res nextAutoClaimResult 501 mctx := libkb.NewMetaContext(ctx, g) 502 if err := g.API.PostDecode(mctx, apiArg, &res); err != nil { 503 return nil, err 504 } 505 return res.Result, nil 506 } 507 508 type recentPaymentsResult struct { 509 libkb.AppStatusEmbed 510 Result stellar1.PaymentsPage `json:"res"` 511 } 512 513 func RecentPayments(ctx context.Context, g *libkb.GlobalContext, arg RecentPaymentsArg) (stellar1.PaymentsPage, error) { 514 mctx := libkb.NewMetaContext(ctx, g) 515 apiArg := libkb.APIArg{ 516 Endpoint: "stellar/recentpayments", 517 SessionType: libkb.APISessionTypeREQUIRED, 518 Args: libkb.HTTPArgs{ 519 "account_id": libkb.S{Val: arg.AccountID.String()}, 520 "limit": libkb.I{Val: arg.Limit}, 521 "skip_pending": libkb.B{Val: arg.SkipPending}, 522 "include_multi": libkb.B{Val: true}, 523 "include_advanced": libkb.B{Val: arg.IncludeAdvanced}, 524 }, 525 RetryCount: 3, 526 RetryMultiplier: 1.5, 527 InitialTimeout: 10 * time.Second, 528 } 529 530 if arg.Cursor != nil { 531 apiArg.Args["horizon_cursor"] = libkb.S{Val: arg.Cursor.HorizonCursor} 532 apiArg.Args["direct_cursor"] = libkb.S{Val: arg.Cursor.DirectCursor} 533 apiArg.Args["relay_cursor"] = libkb.S{Val: arg.Cursor.RelayCursor} 534 } 535 536 var apiRes recentPaymentsResult 537 err := mctx.G().API.GetDecode(mctx, apiArg, &apiRes) 538 return apiRes.Result, err 539 } 540 541 type pendingPaymentsResult struct { 542 libkb.AppStatusEmbed 543 Result []stellar1.PaymentSummary `json:"res"` 544 } 545 546 func PendingPayments(ctx context.Context, g *libkb.GlobalContext, accountID stellar1.AccountID, limit int) ([]stellar1.PaymentSummary, error) { 547 mctx := libkb.NewMetaContext(ctx, g) 548 apiArg := libkb.APIArg{ 549 Endpoint: "stellar/pendingpayments", 550 SessionType: libkb.APISessionTypeREQUIRED, 551 Args: libkb.HTTPArgs{ 552 "account_id": libkb.S{Val: accountID.String()}, 553 "limit": libkb.I{Val: limit}, 554 }, 555 RetryCount: 3, 556 RetryMultiplier: 1.5, 557 InitialTimeout: 10 * time.Second, 558 } 559 560 var apiRes pendingPaymentsResult 561 err := mctx.G().API.GetDecode(mctx, apiArg, &apiRes) 562 return apiRes.Result, err 563 } 564 565 type paymentDetailResult struct { 566 libkb.AppStatusEmbed 567 Result stellar1.PaymentDetails `json:"res"` 568 } 569 570 func PaymentDetails(ctx context.Context, g *libkb.GlobalContext, accountID stellar1.AccountID, txID string) (res stellar1.PaymentDetails, err error) { 571 mctx := libkb.NewMetaContext(ctx, g) 572 apiArg := libkb.APIArg{ 573 Endpoint: "stellar/paymentdetail", 574 SessionType: libkb.APISessionTypeREQUIRED, 575 Args: libkb.HTTPArgs{ 576 "account_id": libkb.S{Val: string(accountID)}, 577 "txID": libkb.S{Val: txID}, 578 }, 579 RetryCount: 3, 580 RetryMultiplier: 1.5, 581 InitialTimeout: 10 * time.Second, 582 } 583 var apiRes paymentDetailResult 584 err = mctx.G().API.GetDecode(mctx, apiArg, &apiRes) 585 return apiRes.Result, err 586 } 587 588 func PaymentDetailsGeneric(ctx context.Context, g *libkb.GlobalContext, txID string) (res stellar1.PaymentDetails, err error) { 589 mctx := libkb.NewMetaContext(ctx, g) 590 apiArg := libkb.APIArg{ 591 Endpoint: "stellar/paymentdetail", 592 SessionType: libkb.APISessionTypeREQUIRED, 593 Args: libkb.HTTPArgs{ 594 "txID": libkb.S{Val: txID}, 595 }, 596 RetryCount: 3, 597 RetryMultiplier: 1.5, 598 InitialTimeout: 10 * time.Second, 599 } 600 var apiRes paymentDetailResult 601 err = mctx.G().API.GetDecode(mctx, apiArg, &apiRes) 602 return apiRes.Result, err 603 } 604 605 type tickerResult struct { 606 libkb.AppStatusEmbed 607 Price string `json:"price"` 608 PriceInBTC string `json:"xlm_btc"` 609 CachedAt keybase1.Time `json:"cached_at"` 610 URL string `json:"url"` 611 Currency string `json:"currency"` 612 } 613 614 func ExchangeRate(ctx context.Context, g *libkb.GlobalContext, currency string) (stellar1.OutsideExchangeRate, error) { 615 mctx := libkb.NewMetaContext(ctx, g) 616 apiArg := libkb.APIArg{ 617 Endpoint: "stellar/ticker", 618 SessionType: libkb.APISessionTypeREQUIRED, 619 Args: libkb.HTTPArgs{ 620 "currency": libkb.S{Val: currency}, 621 }, 622 RetryCount: 3, 623 RetryMultiplier: 1.5, 624 InitialTimeout: 10 * time.Second, 625 } 626 var apiRes tickerResult 627 if err := mctx.G().API.GetDecode(mctx, apiArg, &apiRes); err != nil { 628 return stellar1.OutsideExchangeRate{}, err 629 } 630 return stellar1.OutsideExchangeRate{ 631 Currency: stellar1.OutsideCurrencyCode(apiRes.Currency), 632 Rate: apiRes.Price, 633 }, nil 634 } 635 636 type accountCurrencyResult struct { 637 libkb.AppStatusEmbed 638 CurrencyDisplayPreference string `json:"currency_display_preference"` 639 } 640 641 func GetAccountDisplayCurrency(ctx context.Context, g *libkb.GlobalContext, accountID stellar1.AccountID) (string, error) { 642 mctx := libkb.NewMetaContext(ctx, g) 643 if strings.TrimSpace(accountID.String()) == "" { 644 return "", ErrAccountIDMissing 645 } 646 647 // NOTE: If you are calling this, you might want to call 648 // stellar.GetAccountDisplayCurrency instead which checks for 649 // NULLs and returns a sane default ("USD"). 650 apiArg := libkb.APIArg{ 651 Endpoint: "stellar/accountcurrency", 652 SessionType: libkb.APISessionTypeREQUIRED, 653 Args: libkb.HTTPArgs{ 654 "account_id": libkb.S{Val: string(accountID)}, 655 }, 656 RetryCount: 3, 657 InitialTimeout: 10 * time.Second, 658 } 659 var apiRes accountCurrencyResult 660 err := mctx.G().API.GetDecode(mctx, apiArg, &apiRes) 661 return apiRes.CurrencyDisplayPreference, err 662 } 663 664 func SetAccountDefaultCurrency(ctx context.Context, g *libkb.GlobalContext, accountID stellar1.AccountID, 665 currency string) error { 666 mctx := libkb.NewMetaContext(ctx, g) 667 668 conf, err := mctx.G().GetStellar().GetServerDefinitions(ctx) 669 if err != nil { 670 return err 671 } 672 if _, ok := conf.Currencies[stellar1.OutsideCurrencyCode(currency)]; !ok { 673 return fmt.Errorf("Unknown currency code: %q", currency) 674 } 675 apiArg := libkb.APIArg{ 676 Endpoint: "stellar/accountcurrency", 677 SessionType: libkb.APISessionTypeREQUIRED, 678 Args: libkb.HTTPArgs{ 679 "account_id": libkb.S{Val: string(accountID)}, 680 "currency": libkb.S{Val: currency}, 681 }, 682 } 683 _, err = mctx.G().API.Post(mctx, apiArg) 684 mctx.G().GetStellar().InformDefaultCurrencyChange(mctx) 685 return err 686 } 687 688 type disclaimerResult struct { 689 libkb.AppStatusEmbed 690 AcceptedDisclaimer bool `json:"accepted_disclaimer"` 691 } 692 693 func GetAcceptedDisclaimer(ctx context.Context, g *libkb.GlobalContext) (ret bool, err error) { 694 mctx := libkb.NewMetaContext(ctx, g) 695 apiArg := libkb.APIArg{ 696 Endpoint: "stellar/disclaimer", 697 SessionType: libkb.APISessionTypeREQUIRED, 698 RetryCount: 3, 699 InitialTimeout: 10 * time.Second, 700 } 701 var apiRes disclaimerResult 702 err = mctx.G().API.GetDecode(mctx, apiArg, &apiRes) 703 if err != nil { 704 return ret, err 705 } 706 return apiRes.AcceptedDisclaimer, nil 707 } 708 709 func SetAcceptedDisclaimer(ctx context.Context, g *libkb.GlobalContext) error { 710 mctx := libkb.NewMetaContext(ctx, g) 711 apiArg := libkb.APIArg{ 712 Endpoint: "stellar/disclaimer", 713 SessionType: libkb.APISessionTypeREQUIRED, 714 } 715 _, err := mctx.G().API.Post(mctx, apiArg) 716 return err 717 } 718 719 type submitRequestResult struct { 720 libkb.AppStatusEmbed 721 RequestID stellar1.KeybaseRequestID `json:"request_id"` 722 } 723 724 func SubmitRequest(ctx context.Context, g *libkb.GlobalContext, post stellar1.RequestPost) (ret stellar1.KeybaseRequestID, err error) { 725 payload := make(libkb.JSONPayload) 726 payload["request"] = post 727 apiArg := libkb.APIArg{ 728 Endpoint: "stellar/submitrequest", 729 SessionType: libkb.APISessionTypeREQUIRED, 730 JSONPayload: payload, 731 } 732 var res submitRequestResult 733 mctx := libkb.NewMetaContext(ctx, g) 734 if err := g.API.PostDecode(mctx, apiArg, &res); err != nil { 735 return ret, err 736 } 737 return res.RequestID, nil 738 } 739 740 type requestDetailsResult struct { 741 libkb.AppStatusEmbed 742 Request stellar1.RequestDetails `json:"request"` 743 } 744 745 func RequestDetails(ctx context.Context, g *libkb.GlobalContext, requestID stellar1.KeybaseRequestID) (ret stellar1.RequestDetails, err error) { 746 mctx := libkb.NewMetaContext(ctx, g) 747 apiArg := libkb.APIArg{ 748 Endpoint: "stellar/requestdetails", 749 SessionType: libkb.APISessionTypeREQUIRED, 750 Args: libkb.HTTPArgs{ 751 "id": libkb.S{Val: requestID.String()}, 752 }, 753 RetryCount: 3, 754 RetryMultiplier: 1.5, 755 InitialTimeout: 10 * time.Second, 756 } 757 var res requestDetailsResult 758 if err := mctx.G().API.GetDecode(mctx, apiArg, &res); err != nil { 759 return ret, err 760 } 761 return res.Request, nil 762 } 763 764 func CancelRequest(ctx context.Context, g *libkb.GlobalContext, requestID stellar1.KeybaseRequestID) (err error) { 765 payload := make(libkb.JSONPayload) 766 payload["id"] = requestID 767 apiArg := libkb.APIArg{ 768 Endpoint: "stellar/cancelrequest", 769 SessionType: libkb.APISessionTypeREQUIRED, 770 JSONPayload: payload, 771 } 772 var res libkb.AppStatusEmbed 773 mctx := libkb.NewMetaContext(ctx, g) 774 return g.API.PostDecode(mctx, apiArg, &res) 775 } 776 777 func MarkAsRead(ctx context.Context, g *libkb.GlobalContext, accountID stellar1.AccountID, mostRecentID stellar1.TransactionID) error { 778 payload := make(libkb.JSONPayload) 779 payload["account_id"] = accountID 780 payload["most_recent_id"] = mostRecentID 781 apiArg := libkb.APIArg{ 782 Endpoint: "stellar/markasread", 783 SessionType: libkb.APISessionTypeREQUIRED, 784 JSONPayload: payload, 785 } 786 var res libkb.AppStatusEmbed 787 mctx := libkb.NewMetaContext(ctx, g) 788 return g.API.PostDecode(mctx, apiArg, &res) 789 } 790 791 func IsAccountMobileOnly(ctx context.Context, g *libkb.GlobalContext, accountID stellar1.AccountID) (bool, error) { 792 mctx := libkb.NewMetaContext(ctx, g) 793 bundle, err := FetchSecretlessBundle(mctx) 794 if err != nil { 795 return false, err 796 } 797 for _, account := range bundle.Accounts { 798 if account.AccountID == accountID { 799 return account.Mode == stellar1.AccountMode_MOBILE, nil 800 } 801 } 802 err = libkb.AppStatusError{ 803 Code: libkb.SCStellarMissingAccount, 804 Desc: "account does not exist for user", 805 } 806 return false, err 807 } 808 809 // SetAccountMobileOnly will fetch the account bundle and flip the mobile-only switch, 810 // then send the new account bundle revision to the server. 811 func SetAccountMobileOnly(ctx context.Context, g *libkb.GlobalContext, accountID stellar1.AccountID) error { 812 mctx := libkb.NewMetaContext(ctx, g) 813 b, err := FetchAccountBundle(mctx, accountID) 814 if err != nil { 815 return err 816 } 817 err = bundle.MakeMobileOnly(b, accountID) 818 if err == bundle.ErrNoChangeNecessary { 819 g.Log.CDebugf(ctx, "SetAccountMobileOnly account %s is already mobile-only", accountID) 820 return nil 821 } 822 if err != nil { 823 return err 824 } 825 nextBundle := bundle.AdvanceAccounts(*b, []stellar1.AccountID{accountID}) 826 if err := Post(mctx, nextBundle); err != nil { 827 mctx.Debug("SetAccountMobileOnly Post error: %s", err) 828 return err 829 } 830 831 return nil 832 } 833 834 // MakeAccountAllDevices will fetch the account bundle and flip the mobile-only switch to off 835 // (so that any device can get the account secret keys) then send the new account bundle 836 // to the server. 837 func MakeAccountAllDevices(ctx context.Context, g *libkb.GlobalContext, accountID stellar1.AccountID) error { 838 mctx := libkb.NewMetaContext(ctx, g) 839 b, err := FetchAccountBundle(mctx, accountID) 840 if err != nil { 841 return err 842 } 843 err = bundle.MakeAllDevices(b, accountID) 844 if err == bundle.ErrNoChangeNecessary { 845 g.Log.CDebugf(ctx, "MakeAccountAllDevices account %s is already in all-device mode", accountID) 846 return nil 847 } 848 if err != nil { 849 return err 850 } 851 nextBundle := bundle.AdvanceAccounts(*b, []stellar1.AccountID{accountID}) 852 if err := Post(mctx, nextBundle); err != nil { 853 mctx.Debug("MakeAccountAllDevices Post error: %s", err) 854 return err 855 } 856 857 return nil 858 } 859 860 type lookupUnverifiedResult struct { 861 libkb.AppStatusEmbed 862 Users []struct { 863 UID keybase1.UID `json:"uid"` 864 EldestSeqno keybase1.Seqno `json:"eldest_seqno"` 865 } `json:"users"` 866 } 867 868 func LookupUnverified(ctx context.Context, g *libkb.GlobalContext, accountID stellar1.AccountID) (ret []keybase1.UserVersion, err error) { 869 mctx := libkb.NewMetaContext(ctx, g) 870 apiArg := libkb.APIArg{ 871 Endpoint: "stellar/lookup", 872 SessionType: libkb.APISessionTypeOPTIONAL, 873 Args: libkb.HTTPArgs{ 874 "account_id": libkb.S{Val: accountID.String()}, 875 }, 876 RetryCount: 3, 877 InitialTimeout: 10 * time.Second, 878 } 879 var res lookupUnverifiedResult 880 if err := mctx.G().API.GetDecode(mctx, apiArg, &res); err != nil { 881 return ret, err 882 } 883 for _, user := range res.Users { 884 ret = append(ret, keybase1.NewUserVersion(user.UID, user.EldestSeqno)) 885 } 886 return ret, nil 887 } 888 889 // pukFinder implements the bundle.PukFinder interface. 890 type pukFinder struct{} 891 892 func (p *pukFinder) SeedByGeneration(m libkb.MetaContext, generation keybase1.PerUserKeyGeneration) (libkb.PerUserKeySeed, error) { 893 pukring, err := m.G().GetPerUserKeyring(m.Ctx()) 894 if err != nil { 895 return libkb.PerUserKeySeed{}, err 896 } 897 898 return pukring.GetSeedByGenerationOrSync(m, generation) 899 } 900 901 type serverTimeboundsRes struct { 902 libkb.AppStatusEmbed 903 stellar1.TimeboundsRecommendation 904 } 905 906 func ServerTimeboundsRecommendation(ctx context.Context, g *libkb.GlobalContext) (ret stellar1.TimeboundsRecommendation, err error) { 907 mctx := libkb.NewMetaContext(ctx, g) 908 apiArg := libkb.APIArg{ 909 Endpoint: "stellar/timebounds", 910 SessionType: libkb.APISessionTypeREQUIRED, 911 Args: libkb.HTTPArgs{}, 912 RetryCount: 3, 913 } 914 var res serverTimeboundsRes 915 if err := mctx.G().API.GetDecode(mctx, apiArg, &res); err != nil { 916 return ret, err 917 } 918 return res.TimeboundsRecommendation, nil 919 } 920 921 func SetInflationDestination(ctx context.Context, g *libkb.GlobalContext, signedTx string) (err error) { 922 mctx := libkb.NewMetaContext(ctx, g) 923 apiArg := libkb.APIArg{ 924 Endpoint: "stellar/setinflation", 925 SessionType: libkb.APISessionTypeREQUIRED, 926 Args: libkb.HTTPArgs{ 927 "sig": libkb.S{Val: signedTx}, 928 }, 929 } 930 _, err = mctx.G().API.Post(mctx, apiArg) 931 return err 932 } 933 934 type getInflationDestinationsRes struct { 935 libkb.AppStatusEmbed 936 Destinations []stellar1.PredefinedInflationDestination `json:"destinations"` 937 } 938 939 func GetInflationDestinations(ctx context.Context, g *libkb.GlobalContext) (ret []stellar1.PredefinedInflationDestination, err error) { 940 mctx := libkb.NewMetaContext(ctx, g) 941 apiArg := libkb.APIArg{ 942 Endpoint: "stellar/inflation_destinations", 943 SessionType: libkb.APISessionTypeREQUIRED, 944 } 945 var apiRes getInflationDestinationsRes 946 err = mctx.G().API.GetDecode(mctx, apiArg, &apiRes) 947 if err != nil { 948 return ret, err 949 } 950 return apiRes.Destinations, nil 951 } 952 953 type networkOptionsRes struct { 954 libkb.AppStatusEmbed 955 Options stellar1.NetworkOptions 956 } 957 958 func NetworkOptions(ctx context.Context, g *libkb.GlobalContext) (stellar1.NetworkOptions, error) { 959 mctx := libkb.NewMetaContext(ctx, g) 960 apiArg := libkb.APIArg{ 961 Endpoint: "stellar/network_options", 962 SessionType: libkb.APISessionTypeREQUIRED, 963 } 964 var apiRes networkOptionsRes 965 if err := mctx.G().API.GetDecode(mctx, apiArg, &apiRes); err != nil { 966 return stellar1.NetworkOptions{}, err 967 } 968 return apiRes.Options, nil 969 } 970 971 type detailsPlusPaymentsRes struct { 972 libkb.AppStatusEmbed 973 Result stellar1.DetailsPlusPayments `json:"res"` 974 } 975 976 func DetailsPlusPayments(ctx context.Context, g *libkb.GlobalContext, accountID stellar1.AccountID) (stellar1.DetailsPlusPayments, error) { 977 mctx := libkb.NewMetaContext(ctx, g) 978 apiArg := libkb.APIArg{ 979 Endpoint: "stellar/details_plus_payments", 980 SessionType: libkb.APISessionTypeREQUIRED, 981 Args: libkb.HTTPArgs{ 982 "account_id": libkb.S{Val: accountID.String()}, 983 "include_advanced": libkb.B{Val: true}, 984 }, 985 } 986 var apiRes detailsPlusPaymentsRes 987 if err := mctx.G().API.GetDecode(mctx, apiArg, &apiRes); err != nil { 988 return stellar1.DetailsPlusPayments{}, err 989 } 990 return apiRes.Result, nil 991 } 992 993 type allDetailsPlusPaymentsRes struct { 994 libkb.AppStatusEmbed 995 Result []stellar1.DetailsPlusPayments `json:"res"` 996 } 997 998 func AllDetailsPlusPayments(mctx libkb.MetaContext) ([]stellar1.DetailsPlusPayments, error) { 999 apiArg := libkb.APIArg{ 1000 Endpoint: "stellar/all_details_plus_payments", 1001 SessionType: libkb.APISessionTypeREQUIRED, 1002 } 1003 var apiRes allDetailsPlusPaymentsRes 1004 if err := mctx.G().API.GetDecode(mctx, apiArg, &apiRes); err != nil { 1005 return nil, err 1006 } 1007 1008 return apiRes.Result, nil 1009 } 1010 1011 type airdropDetails struct { 1012 libkb.AppStatusEmbed 1013 Details json.RawMessage `json:"details"` 1014 Disclaimer json.RawMessage `json:"disclaimer"` 1015 IsPromoted bool `json:"is_promoted"` 1016 } 1017 1018 func AirdropDetails(mctx libkb.MetaContext) (bool, string, string, error) { 1019 apiArg := libkb.APIArg{ 1020 Endpoint: "stellar/airdrop/details", 1021 SessionType: libkb.APISessionTypeREQUIRED, 1022 } 1023 1024 var res airdropDetails 1025 if err := mctx.G().API.GetDecode(mctx, apiArg, &res); err != nil { 1026 return false, "", "", err 1027 } 1028 1029 return res.IsPromoted, string(res.Details), string(res.Disclaimer), nil 1030 } 1031 1032 func AirdropRegister(mctx libkb.MetaContext, register bool) error { 1033 apiArg := libkb.APIArg{ 1034 Endpoint: "stellar/airdrop/register", 1035 SessionType: libkb.APISessionTypeREQUIRED, 1036 Args: libkb.HTTPArgs{ 1037 "remove": libkb.B{Val: !register}, 1038 }, 1039 } 1040 _, err := mctx.G().API.Post(mctx, apiArg) 1041 return err 1042 } 1043 1044 type AirConfig struct { 1045 MinActiveDevices int `json:"min_active_devices"` 1046 MinActiveDevicesTitle string `json:"min_active_devices_title"` 1047 AccountCreationTitle string `json:"account_creation_title"` 1048 AccountCreationSubtitle string `json:"account_creation_subtitle"` 1049 AccountUsed string `json:"account_used"` 1050 } 1051 1052 type AirSvc struct { 1053 Qualifies bool `json:"qualifies"` 1054 IsOldEnough bool `json:"is_old_enough"` 1055 IsUsedAlready bool `json:"is_used_already"` 1056 Username string `json:"service_username"` 1057 } 1058 1059 type AirQualifications struct { 1060 QualifiesOverall bool `json:"qualifies_overall"` 1061 HasEnoughDevices bool `json:"has_enough_devices"` 1062 ServiceChecks map[string]AirSvc `json:"service_checks"` 1063 } 1064 1065 type AirdropStatusAPI struct { 1066 libkb.AppStatusEmbed 1067 AlreadyRegistered bool `json:"already_registered"` 1068 Qualifications AirQualifications `json:"qualifications"` 1069 AirdropConfig AirConfig `json:"airdrop_cfg"` 1070 } 1071 1072 func AirdropStatus(mctx libkb.MetaContext) (AirdropStatusAPI, error) { 1073 apiArg := libkb.APIArg{ 1074 Endpoint: "stellar/airdrop/status_check", 1075 SessionType: libkb.APISessionTypeREQUIRED, 1076 } 1077 var status AirdropStatusAPI 1078 if err := mctx.G().API.GetDecode(mctx, apiArg, &status); err != nil { 1079 return AirdropStatusAPI{}, err 1080 } 1081 return status, nil 1082 } 1083 1084 func ChangeTrustline(ctx context.Context, g *libkb.GlobalContext, signedTx string) (err error) { 1085 mctx := libkb.NewMetaContext(ctx, g) 1086 apiArg := libkb.APIArg{ 1087 Endpoint: "stellar/change_trustline", 1088 SessionType: libkb.APISessionTypeREQUIRED, 1089 Args: libkb.HTTPArgs{ 1090 "sig": libkb.S{Val: signedTx}, 1091 }, 1092 } 1093 _, err = mctx.G().API.Post(mctx, apiArg) 1094 return err 1095 } 1096 1097 type findPaymentPathResult struct { 1098 libkb.AppStatusEmbed 1099 Result stellar1.PaymentPath `json:"result"` 1100 } 1101 1102 func FindPaymentPath(mctx libkb.MetaContext, query stellar1.PaymentPathQuery) (stellar1.PaymentPath, error) { 1103 payload := make(libkb.JSONPayload) 1104 payload["query"] = query 1105 apiArg := libkb.APIArg{ 1106 Endpoint: "stellar/findpaymentpath", 1107 SessionType: libkb.APISessionTypeREQUIRED, 1108 JSONPayload: payload, 1109 } 1110 1111 var res findPaymentPathResult 1112 if err := mctx.G().API.PostDecode(mctx, apiArg, &res); err != nil { 1113 return stellar1.PaymentPath{}, err 1114 } 1115 return res.Result, nil 1116 } 1117 1118 func SubmitPathPayment(mctx libkb.MetaContext, post stellar1.PathPaymentPost) (stellar1.PaymentResult, error) { 1119 payload := make(libkb.JSONPayload) 1120 payload["payment"] = post 1121 apiArg := libkb.APIArg{ 1122 Endpoint: "stellar/submitpathpayment", 1123 SessionType: libkb.APISessionTypeREQUIRED, 1124 JSONPayload: payload, 1125 } 1126 var res submitResult 1127 if err := mctx.G().API.PostDecode(mctx, apiArg, &res); err != nil { 1128 return stellar1.PaymentResult{}, err 1129 } 1130 return res.PaymentResult, nil 1131 } 1132 1133 func PostAnyTransaction(mctx libkb.MetaContext, signedTx string) (err error) { 1134 apiArg := libkb.APIArg{ 1135 Endpoint: "stellar/postanytransaction", 1136 SessionType: libkb.APISessionTypeREQUIRED, 1137 Args: libkb.HTTPArgs{ 1138 "sig": libkb.S{Val: signedTx}, 1139 }, 1140 } 1141 _, err = mctx.G().API.Post(mctx, apiArg) 1142 return err 1143 } 1144 1145 type fuzzyAssetSearchResult struct { 1146 libkb.AppStatusEmbed 1147 Assets []stellar1.Asset `json:"matches"` 1148 } 1149 1150 func FuzzyAssetSearch(mctx libkb.MetaContext, arg stellar1.FuzzyAssetSearchArg) ([]stellar1.Asset, error) { 1151 apiArg := libkb.APIArg{ 1152 Endpoint: "stellar/fuzzy_asset_search", 1153 SessionType: libkb.APISessionTypeREQUIRED, 1154 Args: libkb.HTTPArgs{ 1155 "search_string": libkb.S{Val: arg.SearchString}, 1156 }, 1157 } 1158 var apiRes fuzzyAssetSearchResult 1159 if err := mctx.G().API.GetDecode(mctx, apiArg, &apiRes); err != nil { 1160 return []stellar1.Asset{}, err 1161 } 1162 return apiRes.Assets, nil 1163 } 1164 1165 type popularAssetsResult struct { 1166 libkb.AppStatusEmbed 1167 Assets []stellar1.Asset `json:"assets"` 1168 TotalCount int `json:"totalCount"` 1169 } 1170 1171 func ListPopularAssets(mctx libkb.MetaContext, arg stellar1.ListPopularAssetsArg) (stellar1.AssetListResult, error) { 1172 apiArg := libkb.APIArg{ 1173 Endpoint: "stellar/list_popular_assets", 1174 SessionType: libkb.APISessionTypeREQUIRED, 1175 Args: libkb.HTTPArgs{}, 1176 } 1177 var apiRes popularAssetsResult 1178 if err := mctx.G().API.GetDecode(mctx, apiArg, &apiRes); err != nil { 1179 return stellar1.AssetListResult{}, err 1180 } 1181 return stellar1.AssetListResult{ 1182 Assets: apiRes.Assets, 1183 TotalCount: apiRes.TotalCount, 1184 }, nil 1185 }