1package remote
2
3import (
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
18var ErrAccountIDMissing = errors.New("account id parameter missing")
19
20type shouldCreateRes struct {
21	libkb.AppStatusEmbed
22	ShouldCreateResult
23}
24
25type 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.
32func 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
46func 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.
96func 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.
146func 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
177func 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.
210func 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.
222func 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.
236func 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
265func 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
280type fetchAcctRes struct {
281	libkb.AppStatusEmbed
282	bundle.BundleEncoded
283}
284
285type seqnoResult struct {
286	libkb.AppStatusEmbed
287	AccountSeqno string `json:"seqno"`
288}
289
290func 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
314type balancesResult struct {
315	Status   libkb.AppStatus    `json:"status"`
316	Balances []stellar1.Balance `json:"balances"`
317}
318
319func (b *balancesResult) GetAppStatus() *libkb.AppStatus {
320	return &b.Status
321}
322
323func 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
342type detailsResult struct {
343	Status  libkb.AppStatus         `json:"status"`
344	Details stellar1.AccountDetails `json:"details"`
345}
346
347func (b *detailsResult) GetAppStatus() *libkb.AppStatus {
348	return &b.Status
349}
350
351func 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
380type submitResult struct {
381	libkb.AppStatusEmbed
382	PaymentResult stellar1.PaymentResult `json:"payment_result"`
383}
384
385func 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
401func 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
417type submitMultiResult struct {
418	libkb.AppStatusEmbed
419	SubmitMultiRes stellar1.SubmitMultiRes `json:"submit_multi_result"`
420}
421
422func 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
438type submitClaimResult struct {
439	libkb.AppStatusEmbed
440	RelayClaimResult stellar1.RelayClaimResult `json:"claim_result"`
441}
442
443func 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
459type acquireAutoClaimLockResult struct {
460	libkb.AppStatusEmbed
461	Result string `json:"result"`
462}
463
464func 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
477func 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
490type nextAutoClaimResult struct {
491	libkb.AppStatusEmbed
492	Result *stellar1.AutoClaim `json:"result"`
493}
494
495func 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
508type recentPaymentsResult struct {
509	libkb.AppStatusEmbed
510	Result stellar1.PaymentsPage `json:"res"`
511}
512
513func 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
541type pendingPaymentsResult struct {
542	libkb.AppStatusEmbed
543	Result []stellar1.PaymentSummary `json:"res"`
544}
545
546func 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
565type paymentDetailResult struct {
566	libkb.AppStatusEmbed
567	Result stellar1.PaymentDetails `json:"res"`
568}
569
570func 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
588func 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
605type 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
614func 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
636type accountCurrencyResult struct {
637	libkb.AppStatusEmbed
638	CurrencyDisplayPreference string `json:"currency_display_preference"`
639}
640
641func 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
664func 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
688type disclaimerResult struct {
689	libkb.AppStatusEmbed
690	AcceptedDisclaimer bool `json:"accepted_disclaimer"`
691}
692
693func 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
709func 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
719type submitRequestResult struct {
720	libkb.AppStatusEmbed
721	RequestID stellar1.KeybaseRequestID `json:"request_id"`
722}
723
724func 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
740type requestDetailsResult struct {
741	libkb.AppStatusEmbed
742	Request stellar1.RequestDetails `json:"request"`
743}
744
745func 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
764func 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
777func 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
791func 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.
811func 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.
837func 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
860type 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
868func 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.
890type pukFinder struct{}
891
892func (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
901type serverTimeboundsRes struct {
902	libkb.AppStatusEmbed
903	stellar1.TimeboundsRecommendation
904}
905
906func 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
921func 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
934type getInflationDestinationsRes struct {
935	libkb.AppStatusEmbed
936	Destinations []stellar1.PredefinedInflationDestination `json:"destinations"`
937}
938
939func 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
953type networkOptionsRes struct {
954	libkb.AppStatusEmbed
955	Options stellar1.NetworkOptions
956}
957
958func 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
971type detailsPlusPaymentsRes struct {
972	libkb.AppStatusEmbed
973	Result stellar1.DetailsPlusPayments `json:"res"`
974}
975
976func 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
993type allDetailsPlusPaymentsRes struct {
994	libkb.AppStatusEmbed
995	Result []stellar1.DetailsPlusPayments `json:"res"`
996}
997
998func 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
1011type 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
1018func 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
1032func 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
1044type 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
1052type 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
1059type 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
1065type AirdropStatusAPI struct {
1066	libkb.AppStatusEmbed
1067	AlreadyRegistered bool              `json:"already_registered"`
1068	Qualifications    AirQualifications `json:"qualifications"`
1069	AirdropConfig     AirConfig         `json:"airdrop_cfg"`
1070}
1071
1072func 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
1084func 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
1097type findPaymentPathResult struct {
1098	libkb.AppStatusEmbed
1099	Result stellar1.PaymentPath `json:"result"`
1100}
1101
1102func 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
1118func 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
1133func 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
1145type fuzzyAssetSearchResult struct {
1146	libkb.AppStatusEmbed
1147	Assets []stellar1.Asset `json:"matches"`
1148}
1149
1150func 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
1165type popularAssetsResult struct {
1166	libkb.AppStatusEmbed
1167	Assets     []stellar1.Asset `json:"assets"`
1168	TotalCount int              `json:"totalCount"`
1169}
1170
1171func 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}
1186