github.com/keybase/client/go@v0.0.0-20240309051027-028f7c731f8b/stellar/format.go (about) 1 package stellar 2 3 import ( 4 "fmt" 5 6 "github.com/keybase/client/go/libkb" 7 "github.com/keybase/client/go/protocol/stellar1" 8 "github.com/keybase/stellarnet" 9 ) 10 11 func FormatCurrency(mctx libkb.MetaContext, amount string, code stellar1.OutsideCurrencyCode, rounding stellarnet.FmtRoundingBehavior) (string, error) { 12 conf, err := mctx.G().GetStellar().GetServerDefinitions(mctx.Ctx()) 13 if err != nil { 14 return "", err 15 } 16 currency, ok := conf.Currencies[code] 17 if !ok { 18 return "", fmt.Errorf("FormatCurrency error: cannot find curency code %q", code) 19 } 20 21 return stellarnet.FmtCurrency(amount, rounding, currency.Symbol.Symbol, currency.Symbol.Postfix) 22 } 23 24 // FormatCurrencyWithCodeSuffix will return a fiat currency amount formatted with 25 // its currency code suffix at the end, like "$123.12 CLP" 26 func FormatCurrencyWithCodeSuffix(mctx libkb.MetaContext, amount string, code stellar1.OutsideCurrencyCode, rounding stellarnet.FmtRoundingBehavior) (string, error) { 27 conf, err := mctx.G().GetStellar().GetServerDefinitions(mctx.Ctx()) 28 if err != nil { 29 return "", err 30 } 31 currency, ok := conf.Currencies[code] 32 if !ok { 33 return "", fmt.Errorf("FormatCurrencyWithCodeSuffix error: cannot find curency code %q", code) 34 } 35 return stellarnet.FmtCurrencyWithCodeSuffix(amount, rounding, string(code), currency.Symbol.Symbol, currency.Symbol.Postfix) 36 } 37 38 // Return an error if asset is completely outside of what we understand, like 39 // asset unknown types or unexpected length. 40 func assertAssetIsSane(asset stellar1.Asset) error { 41 switch asset.Type { 42 case "credit_alphanum4", "credit_alphanum12": 43 case "alphanum4", "alphanum12": // These prefixes that are missing "credit_" shouldn't show up, but just to be on the safe side. 44 default: 45 return fmt.Errorf("unrecognized asset type: %v", asset.Type) 46 } 47 // Sanity check asset code very loosely. We know tighter bounds but there's no need to fail here. 48 if len(asset.Code) == 0 || len(asset.Code) >= 20 { 49 return fmt.Errorf("invalid asset code: %v", asset.Code) 50 } 51 return nil 52 } 53 54 // Example: "157.5000000 XLM" 55 // Example: "12.9000000 USD" 56 // 57 // (where USD is a non-native asset issued by someone). 58 // 59 // User interfaces should be careful to never give user just amount + asset 60 // code, but annotate when it's a non-native asset and make Issuer ID and 61 // Verified Domain visible. 62 // If you are coming from CLI, FormatAmountDescriptionAssetEx might be a better 63 // choice which is more verbose about non-native assets. 64 func FormatAmountDescriptionAsset(mctx libkb.MetaContext, amount string, asset stellar1.Asset) (string, error) { 65 if asset.IsNativeXLM() { 66 return FormatAmountDescriptionXLM(mctx, amount) 67 } 68 if err := assertAssetIsSane(asset); err != nil { 69 return "", err 70 } 71 // Sanity check asset issuer. 72 if _, err := libkb.ParseStellarAccountID(asset.Issuer); err != nil { 73 return "", fmt.Errorf("asset issuer is not account ID: %v", asset.Issuer) 74 } 75 return FormatAmountWithSuffix(mctx, amount, false /* precisionTwo */, false /* simplify */, asset.Code) 76 } 77 78 // FormatAmountDescriptionAssetEx is a more verbose version of FormatAmountDescriptionAsset. 79 // In case of non-native asset, it includes issuer domain (or "Unknown") and issuer ID. 80 // Example: "157.5000000 XLM" 81 // Example: "1,000.15 CATS/catmoney.example.com (GDWVJEG7CMYKRYGB2MWSRZNSPCWIGGA4FRNFTQBIR6RAEPNEGGEH4XYZ)" 82 // Example: "1,000.15 BTC/Unknown (GBPEHURSE52GCBRPDWNV2VL3HRLCI42367OGRPBOO3AW6VAYEW5EO5PM)" 83 func FormatAmountDescriptionAssetEx(mctx libkb.MetaContext, amount string, asset stellar1.Asset) (string, error) { 84 if asset.IsNativeXLM() { 85 return FormatAmountDescriptionXLM(mctx, amount) 86 } 87 if err := assertAssetIsSane(asset); err != nil { 88 return "", err 89 } 90 // Sanity check asset issuer. 91 issuerAccountID, err := libkb.ParseStellarAccountID(asset.Issuer) 92 if err != nil { 93 return "", fmt.Errorf("asset issuer is not account ID: %v", asset.Issuer) 94 } 95 amountFormatted, err := FormatAmount(mctx, amount, false /* precisionTwo */, stellarnet.Round) 96 if err != nil { 97 return "", err 98 } 99 var issuerDesc string 100 if asset.VerifiedDomain != "" { 101 issuerDesc = asset.VerifiedDomain 102 } else { 103 issuerDesc = "Unknown" 104 } 105 return fmt.Sprintf("%s %s/%s (%s)", amountFormatted, asset.Code, issuerDesc, issuerAccountID.String()), nil 106 } 107 108 // FormatAmountDescriptionAssetEx2 is like FormatAmountDescriptionAssetEx, 109 // except that it only shows one of issuer domain and issuer account ID. When 110 // issuer domain is available, the domain is shown. Otherwise account ID is 111 // used. 112 // Example: "157.5000000 XLM" 113 // Example: "1,000.15 CATS/catmoney.example.com 114 // Example: "1,000.15 BTC/GBPEHURSE52GCBRPDWNV2VL3HRLCI42367OGRPBOO3AW6VAYEW5EO5PM" 115 func FormatAmountDescriptionAssetEx2(mctx libkb.MetaContext, amount string, asset stellar1.Asset) (string, error) { 116 if asset.IsNativeXLM() { 117 return FormatAmountDescriptionXLM(mctx, amount) 118 } 119 if err := assertAssetIsSane(asset); err != nil { 120 return "", err 121 } 122 // Sanity check asset issuer. 123 issuerAccountID, err := libkb.ParseStellarAccountID(asset.Issuer) 124 if err != nil { 125 return "", fmt.Errorf("asset issuer is not account ID: %v", asset.Issuer) 126 } 127 amountFormatted, err := FormatAmount(mctx, amount, false /* precisionTwo */, stellarnet.Round) 128 if err != nil { 129 return "", err 130 } 131 var issuerDesc string 132 if asset.VerifiedDomain != "" { 133 issuerDesc = asset.VerifiedDomain 134 } else { 135 issuerDesc = issuerAccountID.String() 136 } 137 return fmt.Sprintf("%s %s/%s", amountFormatted, asset.Code, issuerDesc), nil 138 } 139 140 // FormatAssetIssuerString returns "Unknown issuer" if asset does not have a 141 // verified domain, or returns asset verified domain if it does (e.g. 142 // "example.com"). 143 func FormatAssetIssuerString(asset stellar1.Asset) string { 144 if asset.VerifiedDomain != "" { 145 return asset.VerifiedDomain 146 } 147 iaid := asset.IssuerString() 148 iaidLen := len(iaid) 149 switch { 150 case iaidLen > 16: 151 return iaid[:8] + "..." + iaid[iaidLen-8:] 152 case iaidLen > 0: 153 return iaid 154 default: 155 return "Unknown issuer" 156 } 157 } 158 159 // Example: "157.5000000 XLM" 160 func FormatAmountDescriptionXLM(mctx libkb.MetaContext, amount string) (string, error) { 161 // Do not simplify XLM amounts, all zeroes are important because 162 // that's the exact number of digits that Stellar protocol 163 // supports. 164 return FormatAmountWithSuffix(mctx, amount, false /* precisionTwo */, false /* simplify */, "XLM") 165 } 166 167 func FormatAmountWithSuffix(mctx libkb.MetaContext, amount string, precisionTwo bool, simplify bool, suffix string) (string, error) { 168 formatted, err := FormatAmount(mctx, amount, precisionTwo, stellarnet.Round) 169 if err != nil { 170 return "", err 171 } 172 if simplify { 173 formatted = libkb.StellarSimplifyAmount(formatted) 174 } 175 return fmt.Sprintf("%s %s", formatted, suffix), nil 176 } 177 178 func FormatAmount(mctx libkb.MetaContext, amount string, precisionTwo bool, rounding stellarnet.FmtRoundingBehavior) (string, error) { 179 if amount == "" { 180 EmptyAmountStack(mctx) 181 return "", fmt.Errorf("empty amount") 182 } 183 return stellarnet.FmtAmount(amount, precisionTwo, rounding) 184 }