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