code.vegaprotocol.io/vega@v0.79.0/core/types/asset.go (about) 1 // Copyright (C) 2023 Gobalsky Labs Limited 2 // 3 // This program is free software: you can redistribute it and/or modify 4 // it under the terms of the GNU Affero General Public License as 5 // published by the Free Software Foundation, either version 3 of the 6 // License, or (at your option) any later version. 7 // 8 // This program is distributed in the hope that it will be useful, 9 // but WITHOUT ANY WARRANTY; without even the implied warranty of 10 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 // GNU Affero General Public License for more details. 12 // 13 // You should have received a copy of the GNU Affero General Public License 14 // along with this program. If not, see <http://www.gnu.org/licenses/>. 15 16 package types 17 18 import ( 19 "errors" 20 "fmt" 21 22 "code.vegaprotocol.io/vega/libs/crypto" 23 "code.vegaprotocol.io/vega/libs/num" 24 "code.vegaprotocol.io/vega/libs/stringer" 25 vegapb "code.vegaprotocol.io/vega/protos/vega" 26 ) 27 28 var ( 29 ErrMissingERC20ContractAddress = errors.New("missing erc20 contract address") 30 ErrMissingBuiltinAssetField = errors.New("missing builtin asset field") 31 ErrInvalidAssetNameEmpty = errors.New("invalid asset, name must not be empty") 32 ErrInvalidAssetSymbolEmpty = errors.New("invalid asset, symbol must not be empty") 33 ErrInvalidAssetQuantumZero = errors.New("invalid asset, quantum must not be zero") 34 ErrLifetimeLimitMustBePositive = errors.New("lifetime limit must be positive") 35 ErrWithdrawThresholdMustBePositive = errors.New("withdraw threshold must be positive") 36 ) 37 38 type AssetStatus = vegapb.Asset_Status 39 40 const ( 41 // AssetStatusUnspecified is the default value, always invalid. 42 AssetStatusUnspecified AssetStatus = vegapb.Asset_STATUS_UNSPECIFIED 43 // AssetStatusProposed states the asset is proposed and under vote. 44 AssetStatusProposed AssetStatus = vegapb.Asset_STATUS_PROPOSED 45 // AssetStatusRejected states the asset has been rejected from governance. 46 AssetStatusRejected AssetStatus = vegapb.Asset_STATUS_REJECTED 47 // AssetStatusPendingListing states the asset is pending listing from the bridge. 48 AssetStatusPendingListing AssetStatus = vegapb.Asset_STATUS_PENDING_LISTING 49 // AssetStatusEnabled states the asset is fully usable in the network. 50 AssetStatusEnabled AssetStatus = vegapb.Asset_STATUS_ENABLED 51 ) 52 53 type Asset struct { 54 // Internal identifier of the asset 55 ID string 56 // Details of the asset (e.g: Great British Pound) 57 Details *AssetDetails 58 // Status of the asset 59 Status AssetStatus 60 } 61 62 type isAssetDetails interface { 63 isAssetDetails() 64 adIntoProto() interface{} 65 DeepClone() isAssetDetails 66 Validate() (ProposalError, error) 67 String() string 68 } 69 70 func (a Asset) IntoProto() *vegapb.Asset { 71 var details *vegapb.AssetDetails 72 if a.Details != nil { 73 details = a.Details.IntoProto() 74 } 75 return &vegapb.Asset{ 76 Id: a.ID, 77 Details: details, 78 Status: a.Status, 79 } 80 } 81 82 func (a Asset) DeepClone() *Asset { 83 cpy := a 84 if a.Details == nil { 85 return &cpy 86 } 87 cpy.Details.Quantum = a.Details.Quantum 88 if a.Details.Source != nil { 89 cpy.Details.Source = a.Details.Source.DeepClone() 90 } 91 return &cpy 92 } 93 94 func AssetFromProto(p *vegapb.Asset) (*Asset, error) { 95 var ( 96 details *AssetDetails 97 err error 98 ) 99 if p.Details != nil { 100 details, err = AssetDetailsFromProto(p.Details) 101 if err != nil { 102 return nil, err 103 } 104 } 105 return &Asset{ 106 ID: p.Id, 107 Details: details, 108 Status: p.Status, 109 }, nil 110 } 111 112 type AssetDetails struct { 113 Name string 114 Symbol string 115 Decimals uint64 116 Quantum num.Decimal 117 Source isAssetDetails 118 } 119 120 func (a AssetDetails) String() string { 121 return fmt.Sprintf( 122 "name(%s) symbol(%s) quantum(%s) decimals(%d) source(%s)", 123 a.Name, 124 a.Symbol, 125 a.Quantum.String(), 126 a.Decimals, 127 stringer.ObjToString(a.Source), 128 ) 129 } 130 131 func (a AssetDetails) Validate() (ProposalError, error) { 132 if len(a.Name) == 0 { 133 return ProposalErrorInvalidAssetDetails, ErrInvalidAssetNameEmpty 134 } 135 136 if len(a.Symbol) == 0 { 137 return ProposalErrorInvalidAssetDetails, ErrInvalidAssetSymbolEmpty 138 } 139 140 if a.Quantum.IsZero() { 141 return ProposalErrorInvalidAssetDetails, ErrInvalidAssetQuantumZero 142 } 143 144 return ProposalErrorUnspecified, nil 145 } 146 147 func (a AssetDetails) IntoProto() *vegapb.AssetDetails { 148 r := &vegapb.AssetDetails{ 149 Name: a.Name, 150 Symbol: a.Symbol, 151 Decimals: a.Decimals, 152 Quantum: a.Quantum.String(), 153 } 154 if a.Source == nil { 155 return r 156 } 157 src := a.Source.adIntoProto() 158 switch s := src.(type) { 159 case *vegapb.AssetDetails_Erc20: 160 r.Source = s 161 case *vegapb.AssetDetails_BuiltinAsset: 162 r.Source = s 163 } 164 return r 165 } 166 167 func (a AssetDetails) GetERC20() *ERC20 { 168 switch s := a.Source.(type) { 169 case *AssetDetailsErc20: 170 return s.ERC20 171 default: 172 return nil 173 } 174 } 175 176 func (a AssetDetails) DeepClone() *AssetDetails { 177 var src isAssetDetails 178 if a.Source != nil { 179 src = a.Source.DeepClone() 180 } 181 cpy := &AssetDetails{ 182 Name: a.Name, 183 Symbol: a.Symbol, 184 Decimals: a.Decimals, 185 Source: src, 186 } 187 cpy.Quantum = a.Quantum 188 return cpy 189 } 190 191 func AssetDetailsFromProto(p *vegapb.AssetDetails) (*AssetDetails, error) { 192 var ( 193 src isAssetDetails 194 err error 195 ) 196 switch st := p.Source.(type) { 197 case *vegapb.AssetDetails_Erc20: 198 src, err = AssetDetailsERC20FromProto(st) 199 if err != nil { 200 return nil, err 201 } 202 case *vegapb.AssetDetails_BuiltinAsset: 203 src = AssetDetailsBuiltinFromProto(st) 204 } 205 quantum := num.DecimalZero() 206 if len(p.Quantum) > 0 { 207 var err error 208 quantum, err = num.DecimalFromString(p.Quantum) 209 if err != nil { 210 return nil, fmt.Errorf("invalid quantum: %w", err) 211 } 212 } 213 return &AssetDetails{ 214 Name: p.Name, 215 Symbol: p.Symbol, 216 Decimals: p.Decimals, 217 Quantum: quantum, 218 Source: src, 219 }, nil 220 } 221 222 type AssetDetailsBuiltinAsset struct { 223 BuiltinAsset *BuiltinAsset 224 } 225 226 func (a AssetDetailsBuiltinAsset) String() string { 227 return fmt.Sprintf( 228 "builtinAsset(%s)", 229 stringer.PtrToString(a.BuiltinAsset), 230 ) 231 } 232 233 func (a AssetDetailsBuiltinAsset) IntoProto() *vegapb.AssetDetails_BuiltinAsset { 234 p := &vegapb.AssetDetails_BuiltinAsset{ 235 BuiltinAsset: &vegapb.BuiltinAsset{}, 236 } 237 if a.BuiltinAsset != nil && a.BuiltinAsset.MaxFaucetAmountMint != nil { 238 p.BuiltinAsset.MaxFaucetAmountMint = a.BuiltinAsset.MaxFaucetAmountMint.String() 239 } 240 return p 241 } 242 243 func (a AssetDetailsBuiltinAsset) adIntoProto() interface{} { 244 return a.IntoProto() 245 } 246 247 func (AssetDetailsBuiltinAsset) isAssetDetails() {} 248 249 func (a AssetDetailsBuiltinAsset) DeepClone() isAssetDetails { 250 cpy := a 251 if a.BuiltinAsset == nil { 252 return &cpy 253 } 254 if a.BuiltinAsset.MaxFaucetAmountMint != nil { 255 cpy.BuiltinAsset.MaxFaucetAmountMint = a.BuiltinAsset.MaxFaucetAmountMint.Clone() 256 } 257 return &cpy 258 } 259 260 func (a AssetDetailsBuiltinAsset) Validate() (ProposalError, error) { 261 if a.BuiltinAsset.MaxFaucetAmountMint.IsZero() { 262 return ProposalErrorMissingBuiltinAssetField, ErrMissingBuiltinAssetField 263 } 264 return ProposalErrorUnspecified, nil 265 } 266 267 func AssetDetailsBuiltinFromProto(p *vegapb.AssetDetails_BuiltinAsset) *AssetDetailsBuiltinAsset { 268 if p.BuiltinAsset.MaxFaucetAmountMint != "" { 269 max, _ := num.UintFromString(p.BuiltinAsset.MaxFaucetAmountMint, 10) 270 return &AssetDetailsBuiltinAsset{ 271 BuiltinAsset: &BuiltinAsset{ 272 MaxFaucetAmountMint: max, 273 }, 274 } 275 } 276 return &AssetDetailsBuiltinAsset{} 277 } 278 279 // BuiltinAsset is a Vega internal asset. 280 type BuiltinAsset struct { 281 MaxFaucetAmountMint *num.Uint 282 } 283 284 func (a BuiltinAsset) String() string { 285 return fmt.Sprintf( 286 "maxFaucetAmountMint(%s)", 287 stringer.PtrToString(a.MaxFaucetAmountMint), 288 ) 289 } 290 291 type AssetDetailsErc20 struct { 292 ERC20 *ERC20 293 } 294 295 func (a AssetDetailsErc20) String() string { 296 return fmt.Sprintf( 297 "erc20(%s)", 298 stringer.PtrToString(a.ERC20), 299 ) 300 } 301 302 func (a AssetDetailsErc20) IntoProto() *vegapb.AssetDetails_Erc20 { 303 lifetimeLimit := "0" 304 if a.ERC20.LifetimeLimit != nil { 305 lifetimeLimit = a.ERC20.LifetimeLimit.String() 306 } 307 withdrawThreshold := "0" 308 if a.ERC20.WithdrawThreshold != nil { 309 withdrawThreshold = a.ERC20.WithdrawThreshold.String() 310 } 311 return &vegapb.AssetDetails_Erc20{ 312 Erc20: &vegapb.ERC20{ 313 ContractAddress: a.ERC20.ContractAddress, 314 ChainId: a.ERC20.ChainID, 315 LifetimeLimit: lifetimeLimit, 316 WithdrawThreshold: withdrawThreshold, 317 }, 318 } 319 } 320 321 func (a AssetDetailsErc20) adIntoProto() interface{} { 322 return a.IntoProto() 323 } 324 325 func (AssetDetailsErc20) isAssetDetails() {} 326 327 func (a AssetDetailsErc20) DeepClone() isAssetDetails { 328 if a.ERC20 == nil { 329 return &AssetDetailsErc20{} 330 } 331 return &AssetDetailsErc20{ 332 ERC20: a.ERC20.DeepClone(), 333 } 334 } 335 336 func (a AssetDetailsErc20) Validate() (ProposalError, error) { 337 if len(a.ERC20.ContractAddress) <= 0 { 338 return ProposalErrorMissingErc20ContractAddress, ErrMissingERC20ContractAddress 339 } 340 341 return ProposalErrorUnspecified, nil 342 } 343 344 func AssetDetailsERC20FromProto(p *vegapb.AssetDetails_Erc20) (*AssetDetailsErc20, error) { 345 var ( 346 lifetimeLimit = num.UintZero() 347 withdrawThreshold = num.UintZero() 348 overflow bool 349 ) 350 if len(p.Erc20.LifetimeLimit) > 0 { 351 lifetimeLimit, overflow = num.UintFromString(p.Erc20.LifetimeLimit, 10) 352 if overflow { 353 return nil, errors.New("invalid lifetime limit") 354 } 355 } 356 if len(p.Erc20.WithdrawThreshold) > 0 { 357 withdrawThreshold, overflow = num.UintFromString(p.Erc20.WithdrawThreshold, 10) 358 if overflow { 359 return nil, errors.New("invalid withdraw threshold") 360 } 361 } 362 return &AssetDetailsErc20{ 363 ERC20: &ERC20{ 364 ChainID: p.Erc20.ChainId, 365 ContractAddress: crypto.EthereumChecksumAddress(p.Erc20.ContractAddress), 366 LifetimeLimit: lifetimeLimit, 367 WithdrawThreshold: withdrawThreshold, 368 }, 369 }, nil 370 } 371 372 // An ERC20 token based asset, living on the ethereum network. 373 type ERC20 struct { 374 // Chain ID from which the asset originated from. 375 ChainID string 376 377 ContractAddress string 378 LifetimeLimit *num.Uint 379 WithdrawThreshold *num.Uint 380 } 381 382 func (e ERC20) DeepClone() *ERC20 { 383 cpy := &ERC20{ 384 ContractAddress: e.ContractAddress, 385 ChainID: e.ChainID, 386 } 387 if e.LifetimeLimit != nil { 388 cpy.LifetimeLimit = e.LifetimeLimit.Clone() 389 } else { 390 cpy.LifetimeLimit = num.UintZero() 391 } 392 if e.WithdrawThreshold != nil { 393 cpy.WithdrawThreshold = e.WithdrawThreshold.Clone() 394 } else { 395 cpy.WithdrawThreshold = num.UintZero() 396 } 397 return cpy 398 } 399 400 func (e ERC20) String() string { 401 return fmt.Sprintf( 402 "contractAddress(%s) chainID(%s) lifetimeLimit(%s) withdrawThreshold(%s)", 403 e.ContractAddress, 404 e.ChainID, 405 stringer.PtrToString(e.LifetimeLimit), 406 stringer.PtrToString(e.WithdrawThreshold), 407 ) 408 }