decred.org/dcrdex@v1.0.5/client/core/wallet.go (about) 1 // This code is available on the terms of the project LICENSE.md file, 2 // also available online at https://blueoakcouncil.org/license/1.0.0. 3 4 package core 5 6 import ( 7 "context" 8 "errors" 9 "fmt" 10 "strings" 11 "sync" 12 "sync/atomic" 13 "time" 14 15 "decred.org/dcrdex/client/asset" 16 "decred.org/dcrdex/dex" 17 "decred.org/dcrdex/dex/encode" 18 "decred.org/dcrdex/dex/encrypt" 19 "github.com/decred/dcrd/dcrec/secp256k1/v4" 20 ) 21 22 var errWalletNotConnected = errors.New("wallet not connected") 23 24 // runWithTimeout runs the provided function, returning either the error from 25 // the function or errTimeout if the function fails to return within the 26 // timeout. This function is for wallet methods that may not have a context or 27 // timeout of their own, or we simply cannot rely on third party packages to 28 // respect context cancellation or deadlines. 29 func runWithTimeout(f func() error, timeout time.Duration) error { 30 errChan := make(chan error, 1) 31 go func() { 32 defer close(errChan) 33 errChan <- f() 34 }() 35 36 select { 37 case err := <-errChan: 38 return err 39 case <-time.After(timeout): 40 return errTimeout 41 } 42 } 43 44 // xcWallet is a wallet. Use (*Core).loadWallet to construct a xcWallet. 45 type xcWallet struct { 46 asset.Wallet 47 log dex.Logger 48 connector *dex.ConnectionMaster 49 AssetID uint32 50 Symbol string 51 supportedVersions []uint32 52 dbID []byte 53 walletType string 54 traits asset.WalletTrait 55 parent *xcWallet 56 feeState atomic.Value // *FeeState 57 connectMtx sync.Mutex 58 59 mtx sync.RWMutex 60 encPass []byte // empty means wallet not password protected 61 balance *WalletBalance 62 pw encode.PassBytes 63 address string 64 peerCount int32 // -1 means no count yet 65 monitored uint32 // startWalletSyncMonitor goroutines monitoring sync status 66 hookedUp bool 67 syncStatus *asset.SyncStatus 68 disabled bool 69 70 // When wallets are being reconfigured and especially when the wallet type 71 // or host is being changed, we want to suppress "walletstate" notes to 72 // prevent subscribers from prematurely adopting the new WalletState before 73 // the new wallet is fully validated and added to the Core wallets map. 74 // WalletState notes during reconfiguration can come from the sync loop or 75 // from the PeersChange callback. 76 broadcasting *uint32 77 } 78 79 // encPW returns xcWallet's encrypted password. 80 func (w *xcWallet) encPW() []byte { 81 w.mtx.RLock() 82 defer w.mtx.RUnlock() 83 return w.encPass 84 } 85 86 // setEncPW sets xcWallet's encrypted password. 87 func (w *xcWallet) setEncPW(encPW []byte) { 88 w.mtx.Lock() 89 w.encPass = encPW 90 w.mtx.Unlock() 91 } 92 93 func (w *xcWallet) supportsVer(ver uint32) bool { 94 for _, v := range w.supportedVersions { 95 if v == ver { 96 return true 97 } 98 } 99 return false 100 } 101 102 // Unlock unlocks the wallet backend and caches the decrypted wallet password so 103 // the wallet may be unlocked without user interaction using refreshUnlock. 104 func (w *xcWallet) Unlock(crypter encrypt.Crypter) error { 105 if w.isDisabled() { // cannot unlock disabled wallet. 106 return fmt.Errorf(walletDisabledErrStr, strings.ToUpper(unbip(w.AssetID))) 107 } 108 if w.parent != nil { 109 return w.parent.Unlock(crypter) 110 } 111 if !w.connected() { 112 return errWalletNotConnected 113 } 114 115 a, is := w.Wallet.(asset.Authenticator) 116 if !is { 117 return nil 118 } 119 120 if len(w.encPW()) == 0 { 121 if a.Locked() { 122 return fmt.Errorf("wallet reporting as locked, but no password has been set") 123 } 124 return nil 125 } 126 pw, err := crypter.Decrypt(w.encPW()) 127 if err != nil { 128 return fmt.Errorf("%s unlockWallet decryption error: %w", unbip(w.AssetID), err) 129 } 130 131 err = a.Unlock(pw) // can be slow - no timeout and NOT in the critical section! 132 if err != nil { 133 return err 134 } 135 w.mtx.Lock() 136 w.pw = pw 137 w.mtx.Unlock() 138 return nil 139 } 140 141 // refreshUnlock is used to ensure the wallet is unlocked. If the wallet backend 142 // reports as already unlocked, which includes a wallet with no password 143 // protection, no further action is taken and a nil error is returned. If the 144 // wallet is reporting as locked, and the wallet is not known to be password 145 // protected (no encPW set) or the decrypted password is not cached, a non-nil 146 // error is returned. If no encrypted password is set, the xcWallet is 147 // misconfigured and should be recreated. If the decrypted password is not 148 // stored, the Unlock method should be used to decrypt the password. Finally, a 149 // non-nil error will be returned if the cached password fails to unlock the 150 // wallet, in which case unlockAttempted will also be true. 151 func (w *xcWallet) refreshUnlock() (unlockAttempted bool, err error) { 152 if w.isDisabled() { // disabled wallet cannot be unlocked. 153 return false, fmt.Errorf(walletDisabledErrStr, strings.ToUpper(unbip(w.AssetID))) 154 } 155 if w.parent != nil { 156 return w.parent.refreshUnlock() 157 } 158 if !w.connected() { 159 return false, errWalletNotConnected 160 } 161 162 a, is := w.Wallet.(asset.Authenticator) 163 if !is { 164 return false, nil 165 } 166 167 // Check if the wallet backend is already unlocked. 168 if !a.Locked() { 169 return false, nil // unlocked 170 } 171 172 // Locked backend requires both encrypted and decrypted passwords. 173 w.mtx.RLock() 174 pwUnset := len(w.encPass) == 0 175 locked := len(w.pw) == 0 176 w.mtx.RUnlock() 177 if pwUnset { 178 return false, fmt.Errorf("%s wallet reporting as locked but no password"+ 179 " has been set", unbip(w.AssetID)) 180 } 181 if locked { 182 return false, fmt.Errorf("cannot refresh unlock on a locked %s wallet", 183 unbip(w.AssetID)) 184 } 185 186 return true, a.Unlock(w.pw) 187 } 188 189 // Lock the wallet. For encrypted wallets (encPW set), this clears the cached 190 // decrypted password and attempts to lock the wallet backend. 191 func (w *xcWallet) Lock(timeout time.Duration) error { 192 a, is := w.Wallet.(asset.Authenticator) 193 if w.isDisabled() || !is { // wallet is disabled and is locked or it's not an authenticator. 194 return nil 195 } 196 w.mtx.Lock() 197 defer w.mtx.Unlock() 198 if w.parent != nil { 199 return w.parent.Lock(timeout) 200 } 201 if !w.hookedUp { 202 return errWalletNotConnected 203 } 204 if len(w.encPass) == 0 { 205 return nil 206 } 207 err := runWithTimeout(a.Lock, timeout) 208 if err == nil { 209 w.pw.Clear() 210 w.pw = nil 211 } 212 return err 213 } 214 215 // unlocked will only return true if both the wallet backend is unlocked and we 216 // have cached the decrypted wallet password. The wallet backend may be queried 217 // directly, likely involving an RPC call. Use locallyUnlocked to determine if 218 // the wallet is automatically unlockable rather than actually unlocked. 219 func (w *xcWallet) unlocked() bool { 220 if w.isDisabled() { 221 return false 222 } 223 a, is := w.Wallet.(asset.Authenticator) 224 if !is { 225 return w.locallyUnlocked() 226 } 227 if w.parent != nil { 228 return w.parent.unlocked() 229 } 230 if !w.connected() { 231 return false 232 } 233 return w.locallyUnlocked() && !a.Locked() 234 } 235 236 // locallyUnlocked checks whether we think the wallet is unlocked, but without 237 // asking the wallet itself. More precisely, for encrypted wallets (encPW set) 238 // this is true only if the decrypted password is cached. Use this to determine 239 // if the wallet may be unlocked without user interaction (via refreshUnlock). 240 func (w *xcWallet) locallyUnlocked() bool { 241 if w.isDisabled() { 242 return false 243 } 244 if w.parent != nil { 245 return w.parent.locallyUnlocked() 246 } 247 w.mtx.RLock() 248 defer w.mtx.RUnlock() 249 if len(w.encPass) == 0 { 250 return true // unencrypted wallet 251 } 252 return len(w.pw) > 0 // cached password for encrypted wallet 253 } 254 255 func (w *xcWallet) unitInfo() dex.UnitInfo { 256 return w.Info().UnitInfo 257 } 258 259 func (w *xcWallet) amtString(amt uint64) string { 260 ui := w.unitInfo() 261 return fmt.Sprintf("%s %s", ui.ConventionalString(amt), ui.Conventional.Unit) 262 } 263 264 func (w *xcWallet) amtStringSigned(amt int64) string { 265 if amt >= 0 { 266 return w.amtString(uint64(amt)) 267 } 268 return "-" + w.amtString(uint64(-amt)) 269 } 270 271 // state returns the current WalletState. 272 func (w *xcWallet) state() *WalletState { 273 winfo := w.Info() 274 275 w.mtx.RLock() 276 var peerCount uint32 277 if w.peerCount > 0 { // initialized to -1 initially, means no count yet 278 peerCount = uint32(w.peerCount) 279 } 280 281 var tokenApprovals map[uint32]asset.ApprovalStatus 282 if w.connector.On() { 283 tokenApprovals = w.ApprovalStatus() 284 } 285 286 var feeState *FeeState 287 if feeStateI := w.feeState.Load(); feeStateI != nil { 288 feeState = feeStateI.(*FeeState) 289 } 290 291 state := &WalletState{ 292 Symbol: unbip(w.AssetID), 293 AssetID: w.AssetID, 294 Open: len(w.encPass) == 0 || len(w.pw) > 0, 295 Running: w.connector.On(), 296 Balance: w.balance, 297 Address: w.address, 298 Units: winfo.UnitInfo.AtomicUnit, 299 Encrypted: len(w.encPass) > 0, 300 PeerCount: peerCount, 301 Synced: w.syncStatus.Synced, 302 SyncProgress: w.syncStatus.BlockProgress(), 303 SyncStatus: w.syncStatus, 304 WalletType: w.walletType, 305 Traits: w.traits, 306 Disabled: w.disabled, 307 Approved: tokenApprovals, 308 FeeState: feeState, 309 } 310 w.mtx.RUnlock() 311 312 if w.parent != nil { 313 w.parent.mtx.RLock() 314 state.Open = len(w.parent.encPass) == 0 || len(w.parent.pw) > 0 315 w.parent.mtx.RUnlock() 316 } 317 318 return state 319 } 320 321 // setBalance sets the wallet balance. 322 func (w *xcWallet) setBalance(bal *WalletBalance) { 323 w.mtx.Lock() 324 w.balance = bal 325 w.mtx.Unlock() 326 } 327 328 // setDisabled sets the wallet disabled field. 329 func (w *xcWallet) setDisabled(status bool) { 330 w.mtx.Lock() 331 w.disabled = status 332 w.mtx.Unlock() 333 } 334 335 func (w *xcWallet) isDisabled() bool { 336 w.mtx.RLock() 337 defer w.mtx.RUnlock() 338 return w.disabled 339 } 340 341 func (w *xcWallet) currentDepositAddress() string { 342 w.mtx.RLock() 343 defer w.mtx.RUnlock() 344 return w.address 345 } 346 347 func (w *xcWallet) refreshDepositAddress() (string, error) { 348 if !w.connected() { 349 return "", fmt.Errorf("cannot get address from unconnected %s wallet", 350 unbip(w.AssetID)) 351 } 352 353 na, is := w.Wallet.(asset.NewAddresser) 354 if !is { 355 return "", fmt.Errorf("wallet does not generate new addresses") 356 } 357 358 addr, err := na.NewAddress() 359 if err != nil { 360 return "", fmt.Errorf("%s Wallet.Address error: %w", unbip(w.AssetID), err) 361 } 362 363 w.mtx.Lock() 364 w.address = addr 365 w.mtx.Unlock() 366 367 return addr, nil 368 } 369 370 // connected is true if the wallet has already been connected. 371 func (w *xcWallet) connected() bool { 372 w.mtx.RLock() 373 defer w.mtx.RUnlock() 374 return w.hookedUp 375 } 376 377 // checkPeersAndSyncStatus checks that the wallet is synced, and has peers 378 // otherwise we might double spend if the wallet keys were used elsewhere. This 379 // should be checked before attempting to send funds but does not replace any 380 // other checks that may be required. 381 func (w *xcWallet) checkPeersAndSyncStatus() error { 382 w.mtx.RLock() 383 defer w.mtx.RUnlock() 384 if w.peerCount < 1 { 385 return fmt.Errorf("%s wallet has no connected peers", unbip(w.AssetID)) 386 } 387 if !w.syncStatus.Synced { 388 return fmt.Errorf("%s wallet is not synchronized", unbip(w.AssetID)) 389 } 390 return nil 391 } 392 393 // Connect calls the dex.Connector's Connect method, sets the xcWallet.hookedUp 394 // flag to true, and validates the deposit address. Use Disconnect to cleanly 395 // shutdown the wallet. 396 func (w *xcWallet) Connect() error { 397 w.connectMtx.Lock() 398 defer w.connectMtx.Unlock() 399 400 // Disabled wallet cannot be connected to unless it is enabled. 401 if w.isDisabled() { 402 return fmt.Errorf(walletDisabledErrStr, strings.ToUpper(unbip(w.AssetID))) 403 } 404 405 if w.connected() { 406 return nil 407 } 408 409 // No parent context; use Disconnect instead. Also note that there's no 410 // reconnect loop for wallet like with the server Connectors, so we use 411 // ConnectOnce so that the ConnectionMaster's On method will report false. 412 err := w.connector.ConnectOnce(context.Background()) 413 if err != nil { 414 return fmt.Errorf("ConnectOnce error: %w", err) 415 } 416 417 var ready bool 418 defer func() { 419 // Now that we are connected, we must Disconnect if any calls fail below 420 // since we are considering this wallet not "hookedUp". 421 if !ready { 422 w.connector.Disconnect() 423 } 424 }() 425 426 ss, err := w.SyncStatus() 427 if err != nil { 428 return fmt.Errorf("SyncStatus error: %w", err) 429 } 430 431 w.mtx.Lock() 432 defer w.mtx.Unlock() 433 haveAddress := w.address != "" 434 if haveAddress { 435 if haveAddress, err = w.OwnsDepositAddress(w.address); err != nil { 436 return fmt.Errorf("OwnsDepositAddress error: %w", err) 437 } 438 } 439 if !haveAddress { 440 if w.address, err = w.DepositAddress(); err != nil { 441 return fmt.Errorf("DepositAddress error: %w", err) 442 } 443 } 444 w.feeRate() // prime the feeState 445 w.hookedUp = true 446 w.syncStatus = ss 447 ready = true 448 449 return nil 450 } 451 452 // Disconnect calls the dex.Connector's Disconnect method and sets the 453 // xcWallet.hookedUp flag to false. 454 func (w *xcWallet) Disconnect() { 455 // Disabled wallet is already disconnected. 456 if w.isDisabled() { 457 return 458 } 459 w.connector.Disconnect() 460 w.mtx.Lock() 461 w.hookedUp = false 462 w.mtx.Unlock() 463 } 464 465 // rescan will initiate a rescan of the wallet if the asset.Wallet 466 // implementation is a Rescanner. 467 func (w *xcWallet) rescan(ctx context.Context, bday /* unix time seconds*/ uint64) error { 468 if !w.connected() { 469 return errWalletNotConnected 470 } 471 rescanner, ok := w.Wallet.(asset.Rescanner) 472 if !ok { 473 return errors.New("wallet does not support rescanning") 474 } 475 return rescanner.Rescan(ctx, bday) 476 } 477 478 // logFilePath returns the path of the wallet's log file if the 479 // asset.Wallet implementation is a LogFiler. 480 func (w *xcWallet) logFilePath() (string, error) { 481 logFiler, ok := w.Wallet.(asset.LogFiler) 482 if !ok { 483 return "", errors.New("wallet does not support getting log file") 484 } 485 return logFiler.LogFilePath(), nil 486 } 487 488 // accelerateOrder uses the Child-Pays-For-Parent technique to accelerate an 489 // order if the wallet is an Accelerator. 490 func (w *xcWallet) accelerateOrder(swapCoins, accelerationCoins []dex.Bytes, changeCoin dex.Bytes, requiredForRemainingSwaps, newFeeRate uint64) (asset.Coin, string, error) { 491 if w.isDisabled() { // cannot perform order acceleration with disabled wallet. 492 return nil, "", fmt.Errorf(walletDisabledErrStr, strings.ToUpper(unbip(w.AssetID))) 493 } 494 if !w.connected() { 495 return nil, "", errWalletNotConnected 496 } 497 accelerator, ok := w.Wallet.(asset.Accelerator) 498 if !ok { 499 return nil, "", errors.New("wallet does not support acceleration") 500 } 501 return accelerator.AccelerateOrder(swapCoins, accelerationCoins, changeCoin, requiredForRemainingSwaps, newFeeRate) 502 } 503 504 // accelerationEstimate estimates the cost to accelerate an order if the wallet 505 // is an Accelerator. 506 func (w *xcWallet) accelerationEstimate(swapCoins, accelerationCoins []dex.Bytes, changeCoin dex.Bytes, requiredForRemainingSwaps, feeSuggestion uint64) (uint64, error) { 507 if w.isDisabled() { // cannot perform acceleration estimate with disabled wallet. 508 return 0, fmt.Errorf(walletDisabledErrStr, strings.ToUpper(unbip(w.AssetID))) 509 } 510 if !w.connected() { 511 return 0, errWalletNotConnected 512 } 513 accelerator, ok := w.Wallet.(asset.Accelerator) 514 if !ok { 515 return 0, errors.New("wallet does not support acceleration") 516 } 517 518 return accelerator.AccelerationEstimate(swapCoins, accelerationCoins, changeCoin, requiredForRemainingSwaps, feeSuggestion) 519 } 520 521 // preAccelerate gives the user information about accelerating an order if the 522 // wallet is an Accelerator. 523 func (w *xcWallet) preAccelerate(swapCoins, accelerationCoins []dex.Bytes, changeCoin dex.Bytes, requiredForRemainingSwaps, feeSuggestion uint64) (uint64, *asset.XYRange, *asset.EarlyAcceleration, error) { 524 if w.isDisabled() { // cannot perform operation with disabled wallet. 525 return 0, &asset.XYRange{}, nil, fmt.Errorf(walletDisabledErrStr, strings.ToUpper(unbip(w.AssetID))) 526 } 527 if !w.connected() { 528 return 0, &asset.XYRange{}, nil, errWalletNotConnected 529 } 530 accelerator, ok := w.Wallet.(asset.Accelerator) 531 if !ok { 532 return 0, &asset.XYRange{}, nil, errors.New("wallet does not support acceleration") 533 } 534 535 return accelerator.PreAccelerate(swapCoins, accelerationCoins, changeCoin, requiredForRemainingSwaps, feeSuggestion) 536 } 537 538 // swapConfirmations calls (asset.Wallet).SwapConfirmations with a timeout 539 // Context. If the coin cannot be located, an asset.CoinNotFoundError is 540 // returned. If the coin is located, but recognized as spent, no error is 541 // returned. 542 func (w *xcWallet) swapConfirmations(ctx context.Context, coinID []byte, contract []byte, matchTime uint64) (uint32, bool, error) { 543 if w.isDisabled() { // cannot check swap confirmation with disabled wallet. 544 return 0, false, fmt.Errorf(walletDisabledErrStr, strings.ToUpper(unbip(w.AssetID))) 545 } 546 if !w.connected() { 547 return 0, false, errWalletNotConnected 548 } 549 return w.Wallet.SwapConfirmations(ctx, coinID, contract, time.UnixMilli(int64(matchTime))) 550 } 551 552 // TxHistory returns all the transactions a wallet has made. If refID 553 // is nil, then transactions starting from the most recent are returned 554 // (past is ignored). If past is true, the transactions prior to the 555 // refID are returned, otherwise the transactions after the refID are 556 // returned. n is the number of transactions to return. If n is <= 0, 557 // all the transactions will be returned. 558 func (w *xcWallet) TxHistory(n int, refID *string, past bool) ([]*asset.WalletTransaction, error) { 559 if !w.connected() { 560 return nil, errWalletNotConnected 561 } 562 563 historian, ok := w.Wallet.(asset.WalletHistorian) 564 if !ok { 565 return nil, fmt.Errorf("wallet does not support transaction history") 566 } 567 568 return historian.TxHistory(n, refID, past) 569 } 570 571 // WalletTransaction returns information about a transaction that the wallet 572 // has made or one in which that wallet received funds. 573 func (w *xcWallet) WalletTransaction(ctx context.Context, txID string) (*asset.WalletTransaction, error) { 574 if !w.connected() { 575 return nil, errWalletNotConnected 576 } 577 578 historian, ok := w.Wallet.(asset.WalletHistorian) 579 if !ok { 580 return nil, fmt.Errorf("wallet does not support transaction history") 581 } 582 583 return historian.WalletTransaction(ctx, txID) 584 } 585 586 // MakeBondTx authors a DEX time-locked fidelity bond transaction if the 587 // asset.Wallet implementation is a Bonder. 588 func (w *xcWallet) MakeBondTx(ver uint16, amt, feeRate uint64, lockTime time.Time, priv *secp256k1.PrivateKey, acctID []byte) (*asset.Bond, func(), error) { 589 bonder, ok := w.Wallet.(asset.Bonder) 590 if !ok { 591 return nil, nil, errors.New("wallet does not support making bond transactions") 592 } 593 return bonder.MakeBondTx(ver, amt, feeRate, lockTime, priv, acctID) 594 } 595 596 // BondsFeeBuffer gets the bonds fee buffer based on the provided fee rate. 597 func (w *xcWallet) BondsFeeBuffer(feeRate uint64) uint64 { 598 bonder, ok := w.Wallet.(asset.Bonder) 599 if !ok { 600 return 0 601 } 602 return bonder.BondsFeeBuffer(feeRate) 603 } 604 605 // RefundBond will refund the bond if the asset.Wallet implementation is a 606 // Bonder. The lock time must be passed to spend the bond. LockTimeExpired 607 // should be used to check first. 608 func (w *xcWallet) RefundBond(ctx context.Context, ver uint16, coinID, script []byte, amt uint64, priv *secp256k1.PrivateKey) (asset.Coin, error) { 609 bonder, ok := w.Wallet.(asset.Bonder) 610 if !ok { 611 return nil, errors.New("wallet does not support refunding bond transactions") 612 } 613 return bonder.RefundBond(ctx, ver, coinID, script, amt, priv) 614 } 615 616 // FindBond finds the bond with coinID and returns the values used to create it. 617 // The output should be unspent with the lockTime set to some time in the future. 618 func (w *xcWallet) FindBond(ctx context.Context, coinID []byte, searchUntil time.Time) (*asset.BondDetails, error) { 619 bonder, ok := w.Wallet.(asset.Bonder) 620 if !ok { 621 return nil, errors.New("wallet does not support making bond transactions") 622 } 623 return bonder.FindBond(ctx, coinID, searchUntil) 624 } 625 626 // SendTransaction broadcasts a raw transaction if the wallet is a Broadcaster. 627 func (w *xcWallet) SendTransaction(tx []byte) ([]byte, error) { 628 bonder, ok := w.Wallet.(asset.Broadcaster) 629 if !ok { 630 return nil, errors.New("wallet is not a Broadcaster") 631 } 632 return bonder.SendTransaction(tx) 633 } 634 635 // ApproveToken sends an approval transaction if the wallet is a TokenApprover. 636 func (w *xcWallet) ApproveToken(assetVersion uint32, onConfirm func()) (string, error) { 637 approver, ok := w.Wallet.(asset.TokenApprover) 638 if !ok { 639 return "", fmt.Errorf("%s wallet is not a TokenApprover", unbip(w.AssetID)) 640 } 641 return approver.ApproveToken(assetVersion, onConfirm) 642 } 643 644 // ApproveToken sends an approval transaction if the wallet is a TokenApprover. 645 func (w *xcWallet) UnapproveToken(assetVersion uint32, onConfirm func()) (string, error) { 646 approver, ok := w.Wallet.(asset.TokenApprover) 647 if !ok { 648 return "", fmt.Errorf("%s wallet is not a TokenApprover", unbip(w.AssetID)) 649 } 650 return approver.UnapproveToken(assetVersion, onConfirm) 651 } 652 653 // ApprovalFee returns the estimated fee to send an approval transaction if the 654 // wallet is a TokenApprover. 655 func (w *xcWallet) ApprovalFee(assetVersion uint32, approval bool) (uint64, error) { 656 approver, ok := w.Wallet.(asset.TokenApprover) 657 if !ok { 658 return 0, fmt.Errorf("%s wallet is not a TokenApprover", unbip(w.AssetID)) 659 } 660 return approver.ApprovalFee(assetVersion, approval) 661 } 662 663 // ApprovalStatus returns the approval status of each version of the asset if 664 // the wallet is a TokenApprover. 665 func (w *xcWallet) ApprovalStatus() map[uint32]asset.ApprovalStatus { 666 approver, ok := w.Wallet.(asset.TokenApprover) 667 if !ok { 668 return nil 669 } 670 671 return approver.ApprovalStatus() 672 } 673 674 func (w *xcWallet) setFeeState(feeRate uint64) { 675 swapFees, refundFees, err := w.SingleLotSwapRefundFees(asset.VersionNewest, feeRate, false) 676 if err != nil { 677 w.log.Errorf("Error getting single-lot swap+refund estimates: %v", err) 678 } 679 redeemFees, err := w.SingleLotRedeemFees(asset.VersionNewest, feeRate) 680 if err != nil { 681 w.log.Errorf("Error getting single-lot redeem estimates: %v", err) 682 } 683 sendFees := w.StandardSendFee(feeRate) 684 w.feeState.Store(&FeeState{ 685 Rate: feeRate, 686 Send: sendFees, 687 Swap: swapFees, 688 Redeem: redeemFees, 689 Refund: refundFees, 690 StampMS: time.Now().UnixMilli(), 691 }) 692 } 693 694 // feeRate returns a fee rate for a FeeRater is available and generates a 695 // non-zero rate. 696 func (w *xcWallet) feeRate() uint64 { 697 if rater, is := w.Wallet.(asset.FeeRater); !is { 698 return 0 699 } else if r := rater.FeeRate(); r != 0 { 700 w.setFeeState(r) 701 return r 702 } 703 return 0 704 }