diff --git a/client/asset/bch/bch.go b/client/asset/bch/bch.go index 502653d831..2938a5a18d 100644 --- a/client/asset/bch/bch.go +++ b/client/asset/bch/bch.go @@ -89,6 +89,7 @@ var ( rpcWalletDefinition, // electrumWalletDefinition, // getinfo RPC needs backport: https://github.com/Electron-Cash/Electron-Cash/pull/2399 }, + BlockchainClass: asset.BlockchainClassUTXO, } externalFeeRate = btc.BitcoreRateFetcher("BCH") diff --git a/client/asset/btc/btc.go b/client/asset/btc/btc.go index 0e09d5cf82..7937319c4a 100644 --- a/client/asset/btc/btc.go +++ b/client/asset/btc/btc.go @@ -216,6 +216,7 @@ var ( electrumWalletDefinition, }, LegacyWalletIndex: 1, + BlockchainClass: asset.BlockchainClassUTXO, } ) diff --git a/client/asset/dash/dash.go b/client/asset/dash/dash.go index 508abbd9d9..bc8d9676de 100644 --- a/client/asset/dash/dash.go +++ b/client/asset/dash/dash.go @@ -74,6 +74,7 @@ var ( ConfigOpts: configOpts, }, }, + BlockchainClass: asset.BlockchainClassUTXO, } ) diff --git a/client/asset/dcr/dcr.go b/client/asset/dcr/dcr.go index ff557bdffd..aa95f8fc98 100644 --- a/client/asset/dcr/dcr.go +++ b/client/asset/dcr/dcr.go @@ -289,6 +289,7 @@ var ( MultiFundingOpts: multiFundingOpts, }, }, + BlockchainClass: asset.BlockchainClassUTXO, } swapFeeBumpKey = "swapfeebump" splitKey = "swapsplit" diff --git a/client/asset/dgb/dgb.go b/client/asset/dgb/dgb.go index 962fa5ce0c..8290c107b3 100644 --- a/client/asset/dgb/dgb.go +++ b/client/asset/dgb/dgb.go @@ -78,6 +78,7 @@ var ( DefaultConfigPath: dexbtc.SystemConfigPath("digibyte"), ConfigOpts: configOpts, }}, + BlockchainClass: asset.BlockchainClassUTXO, } ) diff --git a/client/asset/doge/doge.go b/client/asset/doge/doge.go index 97ee04ff33..175ce8c11c 100644 --- a/client/asset/doge/doge.go +++ b/client/asset/doge/doge.go @@ -98,6 +98,7 @@ var ( DefaultConfigPath: dexbtc.SystemConfigPath("dogecoin"), ConfigOpts: configOpts, }}, + BlockchainClass: asset.BlockchainClassUTXO, } ) diff --git a/client/asset/eth/eth.go b/client/asset/eth/eth.go index 8eef685728..a29c1bd579 100644 --- a/client/asset/eth/eth.go +++ b/client/asset/eth/eth.go @@ -182,7 +182,7 @@ var ( // MaxSwapsInTx and MaxRedeemsInTx are set in (Wallet).Info, since // the value cannot be known until we connect and get network info. }, - IsAccountBased: true, + BlockchainClass: asset.BlockchainClassEVM, } // unlimitedAllowance is the maximum supported allowance for an erc20 @@ -660,7 +660,7 @@ func CreateEVMWallet(chainID int64, createWalletParams *asset.CreateWalletParams func newWallet(assetCFG *asset.WalletConfig, logger dex.Logger, net dex.Network) (w *ETHWallet, err error) { chainCfg, err := ChainConfig(net) if err != nil { - return nil, fmt.Errorf("failed to locate Ethereum genesis configuration for network %s", net) + return nil, fmt.Errorf("failed to locate Ethereum genesis configuration for network %s: %v", net, err) } comp, err := NetworkCompatibilityData(net) if err != nil { @@ -1284,6 +1284,7 @@ func (w *ETHWallet) OpenTokenWallet(tokenCfg *asset.TokenConfig) (asset.Wallet, Name: token.Name, SupportedVersions: w.wi.SupportedVersions, UnitInfo: token.UnitInfo, + BlockchainClass: asset.BlockchainClassEVM, }, pendingTxCheckBal: new(big.Int), } diff --git a/client/asset/firo/firo.go b/client/asset/firo/firo.go index b2349da9f0..383836c8ff 100644 --- a/client/asset/firo/firo.go +++ b/client/asset/firo/firo.go @@ -84,6 +84,7 @@ var ( ConfigOpts: append(btc.ElectrumConfigOpts, btc.CommonConfigOpts("FIRO", true)...), }, }, + BlockchainClass: asset.BlockchainClassUTXO, } ) diff --git a/client/asset/interface.go b/client/asset/interface.go index 68af08d7f4..00b8b531ea 100644 --- a/client/asset/interface.go +++ b/client/asset/interface.go @@ -304,6 +304,17 @@ type Token struct { ContractAddress string `json:"contractAddress"` // Set in SetNetwork } +type BlockchainClass string + +const ( + BlockchainClassUTXO BlockchainClass = "UTXO" + BlockchainClassEVM BlockchainClass = "EVM" +) + +func (c BlockchainClass) IsEVM() bool { + return c == BlockchainClassEVM +} + // WalletInfo is auxiliary information about an ExchangeWallet. type WalletInfo struct { // Name is the display name for the currency, e.g. "Decred" @@ -331,10 +342,8 @@ type WalletInfo struct { // MaxRedeemsInTx is the max amount of redemptions that this wallet can do // in a single transaction. MaxRedeemsInTx uint64 - // IsAccountBased should be set to true for account-based (EVM) assets, so - // that a common seed will be generated and wallets will generate the - // same address. - IsAccountBased bool + // BlockchainClass is the type of the wallet's blockchain. + BlockchainClass BlockchainClass } // ConfigOption is a wallet configuration option. diff --git a/client/asset/ltc/ltc.go b/client/asset/ltc/ltc.go index 385f1a21f6..75e0ea4d07 100644 --- a/client/asset/ltc/ltc.go +++ b/client/asset/ltc/ltc.go @@ -79,6 +79,7 @@ var ( rpcWalletDefinition, electrumWalletDefinition, }, + BlockchainClass: asset.BlockchainClassUTXO, } ) diff --git a/client/asset/polygon/polygon.go b/client/asset/polygon/polygon.go index 6e65cf8dff..6ba83da800 100644 --- a/client/asset/polygon/polygon.go +++ b/client/asset/polygon/polygon.go @@ -87,7 +87,7 @@ var ( // MaxSwapsInTx and MaxRedeemsInTx are set in (Wallet).Info, since // the value cannot be known until we connect and get network info. }, - IsAccountBased: true, + BlockchainClass: asset.BlockchainClassEVM, } ) diff --git a/client/asset/zcl/zcl.go b/client/asset/zcl/zcl.go index c7cb75c7d0..2d00548238 100644 --- a/client/asset/zcl/zcl.go +++ b/client/asset/zcl/zcl.go @@ -99,6 +99,7 @@ var ( ConfigOpts: configOpts, NoAuth: true, }}, + BlockchainClass: asset.BlockchainClassUTXO, } ) diff --git a/client/asset/zec/zec.go b/client/asset/zec/zec.go index bd68bb57d1..ff714bf086 100644 --- a/client/asset/zec/zec.go +++ b/client/asset/zec/zec.go @@ -139,6 +139,7 @@ var ( ConfigOpts: configOpts, NoAuth: true, }}, + BlockchainClass: asset.BlockchainClassUTXO, } feeReservesPerLot = dexzec.TxFeesZIP317(dexbtc.RedeemP2PKHInputSize+1, 2*dexbtc.P2PKHOutputSize+1, 0, 0, 0, 0) diff --git a/client/core/core.go b/client/core/core.go index f3572b99d2..4d852457d5 100644 --- a/client/core/core.go +++ b/client/core/core.go @@ -1529,9 +1529,6 @@ type Core struct { reFiat chan struct{} - pendingWalletsMtx sync.RWMutex - pendingWallets map[uint32]bool - notes chan asset.WalletNotification pokesCache *pokesCache @@ -1672,7 +1669,6 @@ func New(cfg *Config) (*Core, error) { fiatRateSources: make(map[string]*commonRateSource), reFiat: make(chan struct{}, 1), - pendingWallets: make(map[uint32]bool), notes: make(chan asset.WalletNotification, 128), requestedActions: make(map[string]*asset.ActionRequiredNote), @@ -2020,11 +2016,41 @@ func (c *Core) dexConnections() []*dexConnection { // wallet gets the wallet for the specified asset ID in a thread-safe way. func (c *Core) wallet(assetID uint32) (*xcWallet, bool) { c.walletMtx.RLock() - defer c.walletMtx.RUnlock() w, found := c.wallets[assetID] + c.walletMtx.RUnlock() return w, found } +func (c *Core) initializeTokenWallet(tokenID uint32, tkn *asset.Token) error { + if _, found := c.wallet(tkn.ParentID); !found { + return fmt.Errorf("no parent wallet %d for token %s", tkn.ParentID, tkn.Name) + } + dbWallet, err := c.createTokenDBWallet(tokenID, tkn, &WalletForm{ + AssetID: tokenID, + Config: make(map[string]string), + Type: tkn.Definition.Type, + }) + if err != nil { + return fmt.Errorf("error creating %s token wallet with existing %s parent wallet: %w", + dex.BipIDSymbol(tokenID), dex.BipIDSymbol(tkn.ParentID), err) + } + w, err := c.loadXCWallet(dbWallet) + if err != nil { + return fmt.Errorf("error loading newly created %s token wallet: %w", tkn.Name, err) + } + bals := &WalletBalance{Balance: &db.Balance{Balance: asset.Balance{Other: make(map[asset.BalanceCategory]asset.CustomBalance)}}} + w.setBalance(bals) // update xcWallet's WalletBalance + dbWallet.Balance = bals.Balance + // Store the wallet in the database. + err = c.db.UpdateWallet(dbWallet) + if err != nil { + return fmt.Errorf("error storing new token wallet credentials: %w", err) + } + c.log.Infof("Token wallet for %s automatically created", dex.BipIDSymbol(tokenID)) + c.updateWallet(tokenID, w) + return nil +} + // encryptionKey retrieves the application encryption key. The password is used // to recreate the outer key/crypter, which is then used to decode and recreate // the inner key/crypter. @@ -2449,28 +2475,6 @@ func (c *Core) SupportedAssets() map[uint32]*SupportedAsset { return c.assetMap() } -func (c *Core) walletCreationPending(tokenID uint32) bool { - c.pendingWalletsMtx.RLock() - defer c.pendingWalletsMtx.RUnlock() - return c.pendingWallets[tokenID] -} - -func (c *Core) setWalletCreationPending(tokenID uint32) error { - c.pendingWalletsMtx.Lock() - defer c.pendingWalletsMtx.Unlock() - if c.pendingWallets[tokenID] { - return fmt.Errorf("creation already pending for %s", unbip(tokenID)) - } - c.pendingWallets[tokenID] = true - return nil -} - -func (c *Core) setWalletCreationComplete(tokenID uint32) { - c.pendingWalletsMtx.Lock() - delete(c.pendingWallets, tokenID) - c.pendingWalletsMtx.Unlock() -} - // assetMap returns a map of asset information for supported assets. func (c *Core) assetMap() map[uint32]*SupportedAsset { supported := asset.Assets() @@ -2498,13 +2502,12 @@ func (c *Core) assetMap() map[uint32]*SupportedAsset { wallet = w.state() } assets[tokenID] = &SupportedAsset{ - ID: tokenID, - Symbol: dex.BipIDSymbol(tokenID), - Wallet: wallet, - Token: token, - Name: token.Name, - UnitInfo: token.UnitInfo, - WalletCreationPending: c.walletCreationPending(tokenID), + ID: tokenID, + Symbol: dex.BipIDSymbol(tokenID), + Wallet: wallet, + Token: token, + Name: token.Name, + UnitInfo: token.UnitInfo, } } } @@ -2535,13 +2538,12 @@ func (c *Core) asset(assetID uint32) *SupportedAsset { } return &SupportedAsset{ - ID: assetID, - Symbol: dex.BipIDSymbol(assetID), - Wallet: wallet, - Token: token, - Name: token.Name, - UnitInfo: token.UnitInfo, - WalletCreationPending: c.walletCreationPending(assetID), + ID: assetID, + Symbol: dex.BipIDSymbol(assetID), + Wallet: wallet, + Token: token, + Name: token.Name, + UnitInfo: token.UnitInfo, } } @@ -2571,132 +2573,42 @@ func (c *Core) requestedActionsList() []*asset.ActionRequiredNote { // CreateWallet creates a new exchange wallet. func (c *Core) CreateWallet(appPW, walletPW []byte, form *WalletForm) error { - assetID := form.AssetID - symbol := unbip(assetID) - _, exists := c.wallet(assetID) - if exists { - return fmt.Errorf("%s wallet already exists", symbol) - } - crypter, err := c.encryptionKey(appPW) if err != nil { return err } - - var creationQueued bool - defer func() { - if !creationQueued { - crypter.Close() - } - }() - - // If this isn't a token, easy route. - token := asset.TokenInfo(assetID) - if token == nil { - _, err = c.createWalletOrToken(crypter, walletPW, form) - return err - } - - // Prevent two different tokens from trying to create the parent simultaneously. - if err = c.setWalletCreationPending(token.ParentID); err != nil { - return err - } - defer c.setWalletCreationComplete(token.ParentID) - - // If the parent already exists, easy route. - _, found := c.wallet(token.ParentID) - if found { - _, err = c.createWalletOrToken(crypter, walletPW, form) - return err - } - - // Double-registration mode. The parent wallet will be created - // synchronously, then a goroutine is launched to wait for the parent to - // sync before creating the token wallet. The caller can get information - // about the asynchronous creation from WalletCreationNote notifications. - - // First check that they configured the parent asset. - if form.ParentForm == nil { - return fmt.Errorf("no parent wallet %d for token %d (%s), and no parent asset configuration provided", - token.ParentID, assetID, unbip(assetID)) - } - if form.ParentForm.AssetID != token.ParentID { - return fmt.Errorf("parent form asset ID %d is not expected value %d", - form.ParentForm.AssetID, token.ParentID) - } - - // Create the parent synchronously. - parentWallet, err := c.createWalletOrToken(crypter, walletPW, form.ParentForm) - if err != nil { - return fmt.Errorf("error creating parent wallet: %v", err) - } - - if err = c.setWalletCreationPending(assetID); err != nil { - return err - } - - // Start a goroutine to wait until the parent wallet is synced, and then - // begin creation of the token wallet. - c.wg.Add(1) - - c.notify(newWalletCreationNote(TopicCreationQueued, "", "", db.Data, assetID)) - - go func() { - defer c.wg.Done() - defer c.setWalletCreationComplete(assetID) - defer crypter.Close() - - for { - parentWallet.mtx.RLock() - synced := parentWallet.syncStatus.Synced - parentWallet.mtx.RUnlock() - if synced { - break - } - select { - case <-c.ctx.Done(): - return - case <-time.After(time.Second): - } - } - // If there was a walletPW provided, it was for the parent wallet, so - // use nil here. - if _, err := c.createWalletOrToken(crypter, nil, form); err != nil { - c.log.Errorf("failed to create token wallet: %v", err) - subject, details := c.formatDetails(TopicQueuedCreationFailed, unbip(token.ParentID), symbol) - c.notify(newWalletCreationNote(TopicQueuedCreationFailed, subject, details, db.ErrorLevel, assetID)) - } else { - c.notify(newWalletCreationNote(TopicQueuedCreationSuccess, "", "", db.Data, assetID)) - } - }() - creationQueued = true - return nil + return c.createWallet(crypter, walletPW, form) } -func (c *Core) createWalletOrToken(crypter encrypt.Crypter, walletPW []byte, form *WalletForm) (wallet *xcWallet, err error) { +func (c *Core) createWallet(crypter encrypt.Crypter, walletPW []byte, form *WalletForm) (err error) { assetID := form.AssetID symbol := unbip(assetID) + _, exists := c.wallet(assetID) + if exists { + return fmt.Errorf("%s wallet already exists", symbol) + } + token := asset.TokenInfo(assetID) var dbWallet *db.Wallet - if token != nil { - dbWallet, err = c.createTokenWallet(assetID, token, form) + if token == nil { + dbWallet, err = c.createDBWallet(crypter, form, walletPW) } else { - dbWallet, err = c.createWallet(crypter, walletPW, assetID, form) + dbWallet, err = c.createTokenDBWallet(assetID, token, form) } if err != nil { - return nil, err + return err } - wallet, err = c.loadWallet(dbWallet) + wallet, err := c.loadXCWallet(dbWallet) if err != nil { - return nil, fmt.Errorf("error loading wallet for %d -> %s: %w", assetID, symbol, err) + return fmt.Errorf("error loading wallet for %d -> %s: %w", assetID, symbol, err) } // Block PeersChange until we know this wallet is ready. atomic.StoreUint32(wallet.broadcasting, 0) dbWallet.Address, err = c.connectWallet(wallet) if err != nil { - return nil, err + return err } if c.cfg.UnlockCoinsOnLogin { @@ -2705,16 +2617,16 @@ func (c *Core) createWalletOrToken(crypter encrypt.Crypter, walletPW []byte, for } } - initErr := func(s string, a ...any) (*xcWallet, error) { + initErr := func(s string, a ...any) error { _ = wallet.Lock(2 * time.Second) // just try, but don't confuse the user with an error wallet.Disconnect() - return nil, fmt.Errorf(s, a...) + return fmt.Errorf(s, a...) } err = c.unlockWallet(crypter, wallet) // no-op if !wallet.Wallet.Locked() && len(encPW) == 0 if err != nil { wallet.Disconnect() - return nil, fmt.Errorf("%s wallet authentication error: %w", symbol, err) + return fmt.Errorf("%s wallet authentication error: %w", symbol, err) } balances, err := c.walletBalance(wallet) @@ -2742,11 +2654,25 @@ func (c *Core) createWalletOrToken(crypter encrypt.Crypter, walletPW []byte, for c.notify(newWalletStateNote(wallet.state())) c.walletCheckAndNotify(wallet) - return wallet, nil + // Create all token wallets + if token == nil { + for tokenID, tkn := range asset.Asset(assetID).Tokens { + form := &WalletForm{ + AssetID: tokenID, + Config: make(map[string]string), + Type: tkn.Definition.Type, + } + if err := c.createWallet(crypter, nil, form); err != nil { + c.log.Errorf("Error creating token %s wallet for parent %s", tkn.Name, symbol) + } + } + } + + return nil } -func (c *Core) createWallet(crypter encrypt.Crypter, walletPW []byte, assetID uint32, form *WalletForm) (*db.Wallet, error) { - walletDef, err := asset.WalletDef(assetID, form.Type) +func (c *Core) createDBWallet(crypter encrypt.Crypter, form *WalletForm, walletPW []byte) (*db.Wallet, error) { + walletDef, err := asset.WalletDef(form.AssetID, form.Type) if err != nil { return nil, newError(assetSupportErr, "asset.WalletDef error: %w", err) } @@ -2776,7 +2702,7 @@ func (c *Core) createWallet(crypter encrypt.Crypter, walletPW []byte, assetID ui if len(walletPW) > 0 { return nil, errors.New("external password incompatible with seeded wallet") } - walletPW, err = c.createSeededWallet(assetID, crypter, form) + walletPW, err = c.createSeededWallet(form.AssetID, crypter, form) if err != nil { return nil, err } @@ -2792,14 +2718,14 @@ func (c *Core) createWallet(crypter encrypt.Crypter, walletPW []byte, assetID ui return &db.Wallet{ Type: walletDef.Type, - AssetID: assetID, + AssetID: form.AssetID, Settings: form.Config, EncryptedPW: encPW, // Balance and Address are set after connect. }, nil } -func (c *Core) createTokenWallet(tokenID uint32, token *asset.Token, form *WalletForm) (*db.Wallet, error) { +func (c *Core) createTokenDBWallet(tokenID uint32, token *asset.Token, form *WalletForm) (*db.Wallet, error) { wallet, found := c.wallet(token.ParentID) if !found { return nil, fmt.Errorf("no parent wallet %d for token %d (%s)", token.ParentID, tokenID, unbip(tokenID)) @@ -2889,14 +2815,14 @@ func (c *Core) assetSeedAndPass(assetID uint32, crypter encrypt.Crypter) (seed, func AssetSeedAndPass(assetID uint32, appSeed []byte) ([]byte, []byte) { const accountBasedSeedAssetID = 60 // ETH seedAssetID := assetID - if ai, _ := asset.Info(assetID); ai != nil && ai.IsAccountBased { + if ai, _ := asset.Info(assetID); ai != nil && ai.BlockchainClass.IsEVM() { seedAssetID = accountBasedSeedAssetID } // Tokens asset IDs shouldn't be passed in, but if they are, return the seed // for the parent ID. if tkn := asset.TokenInfo(assetID); tkn != nil { if ai, _ := asset.Info(tkn.ParentID); ai != nil { - if ai.IsAccountBased { + if ai.BlockchainClass.IsEVM() { seedAssetID = accountBasedSeedAssetID } } @@ -2924,7 +2850,7 @@ func (c *Core) assetDataBackupDirectory(assetID uint32) string { // loadWallet uses the data from the database to construct a new exchange // wallet. The returned wallet is running but not connected. -func (c *Core) loadWallet(dbWallet *db.Wallet) (*xcWallet, error) { +func (c *Core) loadXCWallet(dbWallet *db.Wallet) (*xcWallet, error) { var parent *xcWallet assetID := dbWallet.AssetID @@ -2969,7 +2895,6 @@ func (c *Core) loadWallet(dbWallet *db.Wallet) (*xcWallet, error) { var w asset.Wallet var err error if token == nil { - walletCfg := &asset.WalletConfig{ Type: dbWallet.Type, Settings: dbWallet.Settings, @@ -3351,7 +3276,7 @@ func (c *Core) RecoverWallet(assetID uint32, appPW []byte, force bool) error { return fmt.Errorf("error creating wallet: %w", err) } - newWallet, err := c.loadWallet(dbWallet) + newWallet, err := c.loadXCWallet(dbWallet) if err != nil { return newError(walletErr, "error loading wallet for %d -> %s: %w", assetID, unbip(assetID), err) @@ -3716,7 +3641,7 @@ func (c *Core) ReconfigureWallet(appPW, newWalletPW []byte, form *WalletForm) er } // Reload the wallet with the new settings. - wallet, err := c.loadWallet(dbWallet) + wallet, err := c.loadXCWallet(dbWallet) if err != nil { return newError(walletErr, "error loading wallet for %d -> %s: %w", assetID, unbip(assetID), err) @@ -3819,7 +3744,7 @@ func (c *Core) ReconfigureWallet(appPW, newWalletPW []byte, form *WalletForm) er continue } tokenWallet.Disconnect() - tokenWallet, err = c.loadWallet(tokenDBWallet) + tokenWallet, err = c.loadXCWallet(tokenDBWallet) if err != nil { c.log.Errorf("Error loading wallet for token %s: %w", unbip(tokenID), err) continue @@ -7186,13 +7111,15 @@ func (c *Core) initialize() error { wg.Wait() c.log.Infof("Connected to %d of %d DEX servers", liveConns, len(accts)) + existingTokenWallets := make(map[uint32]bool) for _, dbWallet := range dbWallets { - if asset.Asset(dbWallet.AssetID) == nil && asset.TokenInfo(dbWallet.AssetID) == nil { + tkn := asset.TokenInfo(dbWallet.AssetID) + if asset.Asset(dbWallet.AssetID) == nil && tkn == nil { c.log.Infof("Wallet for asset %s no longer supported", dex.BipIDSymbol(dbWallet.AssetID)) continue } assetID := dbWallet.AssetID - wallet, err := c.loadWallet(dbWallet) + wallet, err := c.loadXCWallet(dbWallet) if err != nil { c.log.Errorf("error loading %d -> %s wallet: %v", assetID, unbip(assetID), err) continue @@ -7200,6 +7127,27 @@ func (c *Core) initialize() error { // Wallet is loaded from the DB, but not yet connected. c.log.Tracef("Loaded %s wallet configuration.", unbip(assetID)) c.updateWallet(assetID, wallet) + + if tkn != nil { + existingTokenWallets[dbWallet.AssetID] = true + } + } + + // Check for missing token wallets + for _, dbWallet := range dbWallets { + a := asset.Asset(dbWallet.AssetID) + if a == nil { + continue + } + for tokenID, tkn := range a.Tokens { + if existingTokenWallets[tokenID] { + continue + } + // Let's create the missing token wallet + if err := c.initializeTokenWallet(tokenID, tkn); err != nil { + c.log.Errorf("Couldn't create missing token wallet: %v", err) + } + } } // Check DB for active orders on any DEX. @@ -9452,7 +9400,7 @@ func (c *Core) peerChange(w *xcWallet, numPeers uint32, peerChangeErr error) { func (c *Core) handleWalletNotification(ni asset.WalletNotification) { switch n := ni.(type) { case *asset.TipChangeNote: - c.tipChange(n.AssetID) + c.tipChange(n.AssetID, n.Tip) case *asset.BalanceChangeNote: w, ok := c.wallet(n.AssetID) if !ok { @@ -9485,7 +9433,7 @@ func (c *Core) handleWalletNotification(ni asset.WalletNotification) { // tipChange is called by a wallet backend when the tip block changes, or when // a connection error is encountered such that tip change reporting may be // adversely affected. -func (c *Core) tipChange(assetID uint32) { +func (c *Core) tipChange(assetID uint32, tip uint64) { c.log.Tracef("Processing tip change for %s", unbip(assetID)) c.waiterMtx.RLock() for id, waiter := range c.blockWaiters { @@ -9507,6 +9455,15 @@ func (c *Core) tipChange(assetID uint32) { } c.waiterMtx.RUnlock() + w, found := c.wallet(assetID) + if found { + w.mtx.Lock() + ss := *w.syncStatus + ss.Blocks = tip + w.syncStatus = &ss + w.mtx.Unlock() + } + assets := make(assetMap) for _, dc := range c.dexConnections() { newUpdates := c.tickAsset(dc, assetID) diff --git a/client/core/core_test.go b/client/core/core_test.go index 5bdacecb94..ebd099b728 100644 --- a/client/core/core_test.go +++ b/client/core/core_test.go @@ -2041,7 +2041,7 @@ func TestPostBond(t *testing.T) { tWallet.setConfs(tWallet.bondTxCoinID, confs+1, nil) } - tCore.tipChange(tUTXOAssetA.ID) + tCore.tipChange(tUTXOAssetA.ID, 100) return } case <-timeout.C: diff --git a/client/core/types.go b/client/core/types.go index f05a9f806e..0a9f3acb9f 100644 --- a/client/core/types.go +++ b/client/core/types.go @@ -86,14 +86,6 @@ type WalletForm struct { AssetID uint32 Config map[string]string Type string - // ParentForm is the configuration settings for a parent asset. If this is a - // token whose parent asset needs configuration, a non-nil ParentForm can be - // supplied. This will cause CreateWallet to run in a special mode which - // will create the parent asset's wallet synchronously, but schedule the - // creation of the token wallet to occur asynchronously after the parent - // wallet is fully synced, sending NoteTypeCreateWallet notifications to - // update with progress. - ParentForm *WalletForm } // WalletBalance is an exchange wallet's balance which includes various locked @@ -124,12 +116,12 @@ type WalletState struct { AssetID uint32 `json:"assetID"` Version uint32 `json:"version"` WalletType string `json:"type"` + Class asset.BlockchainClass `json:"class"` Traits asset.WalletTrait `json:"traits"` Open bool `json:"open"` Running bool `json:"running"` Balance *WalletBalance `json:"balance"` Address string `json:"address"` - Units string `json:"units"` Encrypted bool `json:"encrypted"` PeerCount uint32 `json:"peerCount"` Synced bool `json:"synced"` @@ -203,9 +195,6 @@ type SupportedAsset struct { // Token is only populated for token assets. Token *asset.Token `json:"token"` UnitInfo dex.UnitInfo `json:"unitInfo"` - // WalletCreationPending will be true if this wallet's parent wallet is - // being synced before this wallet is created. - WalletCreationPending bool `json:"walletCreationPending"` } // BondOptionsForm is used from the settings page to change the auto-bond diff --git a/client/core/wallet.go b/client/core/wallet.go index dd27454d41..eb9a8c3d7d 100644 --- a/client/core/wallet.go +++ b/client/core/wallet.go @@ -295,13 +295,13 @@ func (w *xcWallet) state() *WalletState { Running: w.connector.On(), Balance: w.balance, Address: w.address, - Units: winfo.UnitInfo.AtomicUnit, Encrypted: len(w.encPass) > 0, PeerCount: peerCount, Synced: w.syncStatus.Synced, SyncProgress: w.syncStatus.BlockProgress(), SyncStatus: w.syncStatus, WalletType: w.walletType, + Class: winfo.BlockchainClass, Traits: w.traits, Disabled: w.disabled, Approved: tokenApprovals, diff --git a/client/webserver/api.go b/client/webserver/api.go index 9dde92640e..1a8fb4a92d 100644 --- a/client/webserver/api.go +++ b/client/webserver/api.go @@ -461,20 +461,11 @@ func (s *WebServer) apiNewWallet(w http.ResponseWriter, r *http.Request) { return } defer zero(pass) - var parentForm *core.WalletForm - if f := form.ParentForm; f != nil { - parentForm = &core.WalletForm{ - AssetID: f.AssetID, - Config: f.Config, - Type: f.WalletType, - } - } // Wallet does not exist yet. Try to create it. err = s.core.CreateWallet(pass, form.Pass, &core.WalletForm{ - AssetID: form.AssetID, - Type: form.WalletType, - Config: form.Config, - ParentForm: parentForm, + AssetID: form.AssetID, + Type: form.WalletType, + Config: form.Config, }) if err != nil { s.writeAPIError(w, fmt.Errorf("error creating %s wallet: %w", unbip(form.AssetID), err)) diff --git a/client/webserver/jsintl.go b/client/webserver/jsintl.go index 1ac06d56f3..bc28f8c390 100644 --- a/client/webserver/jsintl.go +++ b/client/webserver/jsintl.go @@ -53,7 +53,6 @@ const ( changeWalletTypeID = "CHANGE_WALLET_TYPE" keepWalletTypeID = "KEEP_WALLET_TYPE" walletReadyID = "WALLET_READY" - walletPendingID = "WALLET_PENDING" setupNeededID = "SETUP_NEEDED" sendSuccessID = "SEND_SUCCESS" reconfigSuccessID = "RECONFIG_SUCCESS" @@ -274,7 +273,6 @@ var enUS = map[string]*intl.Translation{ changeWalletTypeID: {T: "change the wallet type"}, keepWalletTypeID: {T: "don't change the wallet type"}, setupNeededID: {T: "Setup Needed"}, - walletPendingID: {T: "Creating Wallet"}, sendSuccessID: {T: "{{ assetName }} Sent!"}, reconfigSuccessID: {T: "Wallet Reconfigured!"}, rescanStartedID: {T: "Wallet Rescan Running"}, @@ -548,7 +546,6 @@ var zhCN = map[string]*intl.Translation{ changeWalletTypeID: {T: "切换钱包类型"}, keepWalletTypeID: {T: "不要切换钱包类型"}, setupNeededID: {T: "需要设置"}, - walletPendingID: {T: "创建钱包"}, sendSuccessID: {T: "{{ assetName }} 发送!"}, reconfigSuccessID: {T: "钱包重置!"}, rescanStartedID: {T: "钱包扫描运行中"}, @@ -772,7 +769,6 @@ var plPL = map[string]*intl.Translation{ txTypeAccelerationID: {T: "Przyspieszenie"}, orderBttnQtyErrID: {T: "Ilość zamówień musi zostać określona."}, botTypeSimpleArbID: {T: "Prosty arbitraż"}, - walletPendingID: {T: "Tworzenie portfela"}, bondReservesID: {T: "Rezerwy kaucji"}, invalidValueID: {T: "nieprawidłowa wartość"}, disabledMsgID: {T: "portfel jest wyłączony"}, @@ -946,7 +942,6 @@ var deDE = map[string]*intl.Translation{ changeWalletTypeID: {T: "den Wallet-Typ ändern"}, keepWalletTypeID: {T: "den Wallet-Typ nicht ändern"}, setupNeededID: {T: "Einrichtung erforderlich"}, - walletPendingID: {T: "Erstelle Wallet"}, sendSuccessID: {T: "{{ assetName }} gesendet!"}, reconfigSuccessID: {T: "Wallet neu konfiguriert!"}, rescanStartedID: {T: "Wallet Rescan läuft"}, @@ -1159,7 +1154,6 @@ var ar = map[string]*intl.Translation{ keepWalletTypeID: {T: "لا تغير نوع المحفظة"}, walletReadyID: {T: "المحفظة جاهزة"}, setupNeededID: {T: "الإعداد مطلوب"}, - walletPendingID: {T: "إنشاء المحفظة"}, sendSuccessID: {T: "{{ assetName }} تم الإرسال!"}, reconfigSuccessID: {T: "تمت إعادة تهيئة المحفظة!!"}, rescanStartedID: {T: "إعادة فحص المحفظة قيد التشغيل"}, diff --git a/client/webserver/live_test.go b/client/webserver/live_test.go index 86aff69cad..01c8d4f8cc 100644 --- a/client/webserver/live_test.go +++ b/client/webserver/live_test.go @@ -57,10 +57,9 @@ const ( var ( tCtx context.Context - maxDelay = time.Second * 4 - epochDuration = time.Second * 30 // milliseconds - feedPeriod = time.Second * 10 - creationPendingAsset uint32 = 0xFFFFFFFF + maxDelay = time.Second * 4 + epochDuration = time.Second * 30 // milliseconds + feedPeriod = time.Second * 10 forceDisconnectWallet bool wipeWalletBalance bool gapWidthFactor = 1.0 // Should be 0 < gapWidthFactor <= 1.0 @@ -226,14 +225,13 @@ func mkSupportedAsset(symbol string, state *core.WalletState) *core.SupportedAss } return &core.SupportedAsset{ - ID: assetID, - Symbol: symbol, - Info: winfo, - Wallet: state, - Token: tinfos[assetID], - Name: name, - UnitInfo: unitInfo, - WalletCreationPending: assetID == atomic.LoadUint32(&creationPendingAsset), + ID: assetID, + Symbol: symbol, + Info: winfo, + Wallet: state, + Token: tinfos[assetID], + Name: name, + UnitInfo: unitInfo, } } @@ -1389,35 +1387,7 @@ func (c *TCore) CreateWallet(appPW, walletPW []byte, form *core.WalletForm) erro c.mtx.Lock() defer c.mtx.Unlock() - // If this is a token, simulate parent syncing. - token := asset.TokenInfo(form.AssetID) - if token == nil || form.ParentForm == nil { - c.createWallet(form, false) - return nil - } - - atomic.StoreUint32(&creationPendingAsset, form.AssetID) - - synced := c.createWallet(form.ParentForm, false) - - c.noteFeed <- &core.WalletCreationNote{ - Notification: db.NewNotification(core.NoteTypeCreateWallet, core.TopicCreationQueued, "", "", db.Data), - AssetID: form.AssetID, - } - - go func() { - <-synced - defer atomic.StoreUint32(&creationPendingAsset, 0xFFFFFFFF) - if doubleCreateAsyncErr { - c.noteFeed <- &core.WalletCreationNote{ - Notification: db.NewNotification(core.NoteTypeCreateWallet, core.TopicQueuedCreationFailed, - "Test Error", "This failed because doubleCreateAsyncErr is true in live_test.go", db.Data), - AssetID: form.AssetID, - } - return - } - c.createWallet(form, true) - }() + c.createWallet(form, false) return nil } diff --git a/client/webserver/locales/ar.go b/client/webserver/locales/ar.go index b36fc2bc3f..073dd15731 100644 --- a/client/webserver/locales/ar.go +++ b/client/webserver/locales/ar.go @@ -167,7 +167,7 @@ var Ar = map[string]*intl.Translation{ "dont_share": {T: "لا تشاركها. ولا تفقدها."}, "Show Me": {T: "أرني"}, "Wallet Settings": {T: "اعدادات المحفظة"}, - "add_a_x_wallet": {T: `أضف محفظة`}, + "add_a_x_wallet": {T: `أضف محفظة`}, "ready": {T: "جاهز"}, "off": {T: "انطلق"}, "Export Trades": {T: "تصدير التداولات"}, diff --git a/client/webserver/locales/de-de.go b/client/webserver/locales/de-de.go index a7b122c1c2..fe2ef923ef 100644 --- a/client/webserver/locales/de-de.go +++ b/client/webserver/locales/de-de.go @@ -200,7 +200,7 @@ var DeDE = map[string]*intl.Translation{ "dont_share": {T: "Teile es nicht. Verliere es nicht."}, "Show Me": {T: "Anzeigen"}, "Wallet Settings": {T: "Wallet Einstellungen"}, - "add_a_x_wallet": {T: `Füge ein Wallet hinzu`}, + "add_a_x_wallet": {T: `Füge ein Wallet hinzu`}, "ready": {T: "fertig"}, "off": {T: "Ausgeschaltet"}, "Export Trades": {T: "Exportiere Trades"}, @@ -612,8 +612,6 @@ var DeDE = map[string]*intl.Translation{ "err_with_cex_creds": {T: "Bei der Verbindung mit diesen Anmeldedaten ist ein Fehler aufgetreten"}, "approve_token_wallet_addr": {T: ` Adresse:`}, "Available fee balance": {T: "Verfügbares Gebührenguthaben"}, - "add_provider_tooltip": {T: "Du verwendest öffentliche RPC-Anbieter. Diese Anbieter könnten veraltet oder unzuverlässig werden. Wenn möglich, konfiguriere stattdessen deine eigenen vertrauenswürdigen Anbieter an."}, - "add_custom_rpc_provider": {T: "RPC Anbieter hinzufügen"}, "Profit": {T: "Profit"}, "Inventory": {T: "Inventar"}, "Booked orders": {T: "Gebuchte Aufträge"}, diff --git a/client/webserver/locales/en-us.go b/client/webserver/locales/en-us.go index 0db0ae7c77..16595afbb1 100644 --- a/client/webserver/locales/en-us.go +++ b/client/webserver/locales/en-us.go @@ -205,7 +205,7 @@ var EnUS = map[string]*intl.Translation{ "dont_share": {T: "Don't share it. Don't lose it."}, "Show Me": {T: "Show Me"}, "Wallet Settings": {T: "Wallet Settings"}, - "add_a_x_wallet": {T: `Add a Wallet`}, + "add_a_x_wallet": {Version: 1, T: `Set Up Your Wallet`}, "ready": {T: "ready"}, "off": {T: "off"}, "Export Trades": {T: "Export Trades"}, @@ -617,8 +617,6 @@ var EnUS = map[string]*intl.Translation{ "err_with_cex_creds": {T: "There was an error encountered connecting with these credentials"}, "approve_token_wallet_addr": {T: ` address:`}, "Available fee balance": {T: "Available fee balance"}, - "add_provider_tooltip": {T: "You are using the default set of public RPC providers. These providers could become outdated or unreliable. Specify your own trusted provider instead."}, - "add_custom_rpc_provider": {T: "Add a custom RPC provider"}, "Profit": {T: "Profit"}, "Inventory": {T: "Inventory"}, "Booked orders": {T: "Booked orders"}, diff --git a/client/webserver/locales/pl-pl.go b/client/webserver/locales/pl-pl.go index 147fa170fe..18b4e091bd 100644 --- a/client/webserver/locales/pl-pl.go +++ b/client/webserver/locales/pl-pl.go @@ -162,7 +162,7 @@ var PlPL = map[string]*intl.Translation{ "dont_share": {T: "Nie udostępniaj nikomu. Nie zgub go."}, "Show Me": {T: "Pokaż"}, "Wallet Settings": {T: "Ustawienia portfela"}, - "add_a_x_wallet": {T: `Dodaj portfel `}, + "add_a_x_wallet": {T: `Dodaj portfel `}, "ready": {T: "gotowy"}, "off": {T: "wyłączony"}, "Export Trades": {T: "Eksportuj zlecenia wymiany"}, diff --git a/client/webserver/locales/pt-br.go b/client/webserver/locales/pt-br.go index b77f4ed314..5a0a1c4883 100644 --- a/client/webserver/locales/pt-br.go +++ b/client/webserver/locales/pt-br.go @@ -162,7 +162,7 @@ var PtBr = map[string]*intl.Translation{ "dont_share": {T: "Não compartilhe e não perca sua seed."}, "Show Me": {T: "Mostre me"}, "Wallet Settings": {T: "Configurações da Carteira"}, - "add_a_x_wallet": {T: `Adicionar uma carteira `}, + "add_a_x_wallet": {T: `Adicionar uma carteira `}, "ready": {T: "destrancado"}, "off": {T: "desligado"}, "Export Trades": {T: "Exportar Trocas"}, diff --git a/client/webserver/locales/zh-cn.go b/client/webserver/locales/zh-cn.go index 5c6a6c1fb6..8479daad2a 100644 --- a/client/webserver/locales/zh-cn.go +++ b/client/webserver/locales/zh-cn.go @@ -199,7 +199,7 @@ var ZhCN = map[string]*intl.Translation{ "dont_share": {T: "不要分享,不要丢失。"}, "Show Me": {T: "查看"}, "Wallet Settings": {T: "钱包设置"}, - "add_a_x_wallet": {T: `添加一个 钱包`}, + "add_a_x_wallet": {T: `添加一个 钱包`}, "ready": {T: "准备好"}, "off": {T: "关"}, "Export Trades": {T: "退出交易"}, @@ -612,8 +612,6 @@ var ZhCN = map[string]*intl.Translation{ "err_with_cex_creds": {T: "连接这些凭证时遇到错误"}, "approve_token_wallet_addr": {T: ` address: 地址:`}, "Available fee balance": {T: "可用费用余额"}, - "add_provider_tooltip": {T: "您正在使用默认的公共 RPC 提供商。这些提供商可能会过时或不可靠。请指定您信任的提供商。"}, - "add_custom_rpc_provider": {T: "添加自定义RPC提供商"}, "Profit": {T: "利润"}, "Inventory": {T: "库存"}, "Booked orders": {T: "已预定订单"}, diff --git a/client/webserver/middleware.go b/client/webserver/middleware.go index f028dfa43c..d5c92ab2ea 100644 --- a/client/webserver/middleware.go +++ b/client/webserver/middleware.go @@ -30,7 +30,7 @@ func (s *WebServer) securityMiddleware(next http.Handler) http.Handler { w.Header().Set("X-Content-Type-Options", "nosniff") w.Header().Set("Referrer-Policy", "no-referrer") w.Header().Set("Content-Security-Policy", s.csp) - w.Header().Set("Feature-Policy", "geolocation 'none'; midi 'none'; notifications 'none'; push 'none'; sync-xhr 'self'; microphone 'none'; camera 'none'; magnetometer 'none'; gyroscope 'none'; speaker 'none'; vibrate 'none'; fullscreen 'self'; payment 'none'") + w.Header().Set("Permissions-Policy", "geolocation=(), midi=(), sync-xhr=(self), microphone=(), camera=(), magnetometer=(), gyroscope=(), fullscreen=(self), payment=()") next.ServeHTTP(w, r) }) } diff --git a/client/webserver/site/src/css/components.scss b/client/webserver/site/src/css/components.scss index af2a3b3d3e..4135709b44 100644 --- a/client/webserver/site/src/css/components.scss +++ b/client/webserver/site/src/css/components.scss @@ -29,11 +29,21 @@ button { font-size: .9rem; } + &.micro { + padding: 0.1rem 0.25rem; + font-size: .8rem; + } + &.large { padding: 0.5rem 1rem; font-size: 1.25rem; } + &.noborder { + border: none; + border-radius: 0; + } + &.feature { background-color: var(--btn-feature-bg); border-color: var(--btn-feature-border-color); @@ -150,6 +160,14 @@ table { background-color: #7772; } } + + &.no-bottom-border > tbody { + border-bottom-style: none; + } + + & > thead.unbold th { + font-weight: normal; + } } a { @@ -160,12 +178,6 @@ a { } } -@include media-breakpoint-up(md) { - table#walletInfoTable { - width: auto; - } -} - table.reg-asset-markets { @extend .stylish-overflow; diff --git a/client/webserver/site/src/css/main.scss b/client/webserver/site/src/css/main.scss index 60db707897..3ab1f8e529 100644 --- a/client/webserver/site/src/css/main.scss +++ b/client/webserver/site/src/css/main.scss @@ -216,7 +216,9 @@ z-index: 1000; } [data-unit-box] { - cursor: default; + @extend .hoverbg; + + cursor: pointer; position: relative; overflow: visible; diff --git a/client/webserver/site/src/css/wallets.scss b/client/webserver/site/src/css/wallets.scss index 3d14469964..a51e9c1062 100644 --- a/client/webserver/site/src/css/wallets.scss +++ b/client/webserver/site/src/css/wallets.scss @@ -1,4 +1,4 @@ -.walletspage { +[data-handler=wallets] { .ico-unlocked { color: var(--indicator-good); } @@ -110,18 +110,6 @@ #walletInfo { border-left: none; - - table#walletInfoTable { - td { - padding: 2px 5px 2px 0; - line-height: 1; - - &:last-child { - text-align: right; - padding-left: 1rem; - } - } - } } #earlierTxs, #txViewBlockExplorer { @@ -160,6 +148,18 @@ width: 18px; height: 18px; } + + &:not(.multinet) .multinetonly { + display: none !important; + } + + &.multinet .uninetonly { + display: none !important; + } + + &:not(.token) .tokenonly { + display: none !important; + } } .peers-table-icon { @@ -187,6 +187,13 @@ .negative-tx { color: $danger; } + + #docs { + .plainlink:visited, + .plainlink:hover { + text-decoration: none; + } + } } @include media-breakpoint-up(xl) { @@ -209,7 +216,7 @@ @include stylish-overflow; } - .walletspage { + [data-handler=wallets] { #walletDetailsBox { border-bottom: none !important; } @@ -225,7 +232,7 @@ } @include media-breakpoint-up(sm) { - .walletspage { + [data-handler=wallets] { #walletDetailsBox { #assetLogo { width: 40px; @@ -278,7 +285,7 @@ } @include media-breakpoint-up(md) { - .walletspage { + [data-handler=wallets] { #sendReceive { border-bottom: none; } diff --git a/client/webserver/site/src/html/docs.tmpl b/client/webserver/site/src/html/docs.tmpl new file mode 100644 index 0000000000..c2af20137f --- /dev/null +++ b/client/webserver/site/src/html/docs.tmpl @@ -0,0 +1,212 @@ +{{define "dcrDocs"}} +
+
About Decred
+
+ Decred is a blockchain-based cryptocurrency with a strong focus on community input, open governance, and sustainable funding for development. + Decred uses a hybrid proof-of-work (PoW) / proof-of-stake (PoS) mining model that gives ultra-high security with a fraction of the resource requirements. + Decred's on-chain governance system is tied in to the PoS system, and includes the world's first fully-decentralized development treasury fully-controlled by the stakeholders. + Combined with an on-chain voting mechanism, this means that stakeholders have complete control over all blockchain upgrades and development funding. + This is an alignment of incentives that no other blockchain can boast. +
+
+ Website + Documentation +
+
+{{end}} + +{{define "btcDocs"}} +
+
About Bitcoin
+
+ Bitcoin is the first and most widely recognized cryptocurrency, created in 2009 by the pseudonymous developer Satoshi Nakamoto. + It operates on a decentralized, peer-to-peer network using blockchain technology, where transactions are recorded transparently and securely without the need for intermediaries. + Bitcoin relies on a proof-of-work (PoW) consensus mechanism, where miners validate transactions and secure the network by solving complex cryptographic puzzles. + With a fixed supply of 21 million coins, Bitcoin is often considered digital gold, serving as a store of value, hedge against inflation, and alternative to traditional fiat currencies. + Its decentralized nature and limited supply make it a key asset in the cryptocurrency and financial ecosystems. +
+
+ Introduction Video + Learn More +
+
+{{end}} + +{{define "ethDocs"}} +
+
About Ethereum
+
+ Ethereum is a decentralized, open-source blockchain platform that enables smart contracts and decentralized applications (dApps) to run without intermediaries. + Launched in 2015 by Vitalik Buterin and others, it introduced the Ethereum Virtual Machine (EVM), allowing developers to execute code on a global, distributed network. + Its native cryptocurrency, Ether (ETH), is used for transactions, staking, and gas fees that power computations. + Ethereum transitioned from a proof-of-work (PoW) to a proof-of-stake (PoS) consensus mechanism with the Merge in 2022, improving energy efficiency and security. + As a foundational layer for DeFi, NFTs, and other Web3 applications, Ethereum continues to evolve with scalability upgrades like rollups and sharding. +
+
+ Website + Documentation +
+
+{{end}} + +{{define "usdcDocs"}} +
+
About USDC
+
+ USDC (USD Coin) is a fully collateralized, fiat-backed stablecoin pegged 1:1 to the U.S. dollar, issued by Circle and governed by the Centre consortium. + It operates on multiple blockchains, including Ethereum, Polygon, and Base, and is widely used for payments, remittances, and decentralized finance (DeFi). + Each USDC token is backed by dollar-denominated reserves held in regulated financial institutions, with regular audits to ensure transparency. + Unlike algorithmic stablecoins, USDC maintains its peg through direct redemption mechanisms, allowing users to exchange it for USD at any time. + It is one of the most trusted stablecoins due to its regulatory compliance and transparency. +
+
+ Website +
+
+{{end}} + +{{define "usdtDocs"}} +
+
About USD Tether
+
+ USDT (Tether) is a widely used stablecoin pegged 1:1 to the U.S. dollar, issued by Tether Limited. + It operates on multiple blockchains, including Ethereum, Polygon, and Base, facilitating fast and low-cost transactions in crypto markets. + USDT is commonly used for trading, remittances, and decentralized finance (DeFi), offering liquidity and stability in the volatile crypto ecosystem. + Unlike fully-regulated stablecoins, Tether has faced scrutiny over its reserve transparency, though it claims to be backed by cash, cash equivalents, and other assets. + Despite regulatory concerns, USDT remains the most traded stablecoin and a dominant force in the digital asset space. +
+
+ Website +
+
+{{end}} + +{{define "ltcDocs"}} +
+
About Litecoin
+
+ Litecoin is a decentralized cryptocurrency created in 2011 by former Google engineer Charlie Lee as a faster and more lightweight alternative to Bitcoin. + It uses a proof-of-work (PoW) consensus mechanism with the Scrypt hashing algorithm, allowing for quicker block times (2.5 minutes vs. Bitcoin's 10 minutes) and lower transaction fees. + While often considered the "silver to Bitcoin's gold," Litecoin maintains strong network security and decentralization while offering improved scalability. + It has been a testing ground for innovations like Segregated Witness (SegWit) and the Lightning Network, which were later adopted by Bitcoin. + Despite facing competition from newer blockchains, Litecoin remains one of the most widely used and trusted cryptocurrencies. +
+
+ Website + Documentation +
+
+{{end}} + +{{define "dogeDocs"}} +
+
About Dogecoin
+
+ Dogecoin is a decentralized, open-source cryptocurrency created in 2013 by Billy Markus and Jackson Palmer as a lighthearted alternative to Bitcoin, featuring the Shiba Inu meme as its mascot. + Initially a joke, Dogecoin gained a strong community and became popular for tipping, charitable donations, and microtransactions due to its low fees and fast transaction speeds. + It operates on a proof-of-work (PoW) system similar to Litecoin, with an unlimited supply to encourage spending rather than hoarding. + Over time, Dogecoin has seen mainstream attention, including endorsements from figures like Elon Musk, and remains a widely recognized and actively traded cryptocurrency. +
+
+ Website + Documentation +
+
+{{end}} + +{{define "dashDocs"}} +
+
About Dash
+
+ Dash is a decentralized cryptocurrency launched in 2014 as a fork of Bitcoin, designed for fast, low-cost digital payments. + Dash features innovations like InstantSend, which enables near-instant transactions, and ChainLocks, which enhances security against 51% attacks. + It also has a unique two-tier network with masternodes that facilitate governance, private transactions (via PrivateSend), and network upgrades. + With a self-funding treasury system, Dash continues to evolve as a payment-focused cryptocurrency used for remittances and merchant adoption worldwide. +
+
+ Website + Documentation +
+
+{{end}} + +{{define "dgbDocs"}} +
+
About Digibyte
+
+ DigiByte is a decentralized, open-source cryptocurrency launched in 2014 by Jared Tate, designed to improve speed, security, and scalability compared to Bitcoin. + It features a multi-algorithm proof-of-work (PoW) consensus system, utilizing five different mining algorithms to enhance security and decentralization. + DigiByte also introduced DigiShield, an advanced difficulty adjustment mechanism that helps protect against mining fluctuations. + With 15-second block times, it offers significantly faster transactions than Bitcoin and many other cryptocurrencies. + Despite being a grassroots project without corporate backing, DigiByte has built a strong community and remains focused on decentralized digital payments and blockchain security applications. +
+
+ Website +
+
+{{end}} + +{{define "zecDocs"}} +
+
About Zcash
+
+ Zcash is a privacy-focused cryptocurrency launched in 2016, based on Bitcoin's code but with enhanced anonymity features. + It utilizes zero-knowledge proofs (zk-SNARKs) to enable fully private transactions while still being verifiable on the blockchain. + Users can choose between transparent and shielded addresses, allowing flexibility in privacy. + Zcash maintains a fixed supply of 21 million coins, similar to Bitcoin, and uses a proof-of-work (PoW) consensus mechanism for network security. + While it provides financial privacy, Zcash also supports compliance features for regulated institutions, making it one of the most advanced privacy coins in the cryptocurrency space. +
+
+ Website + Documentation +
+
+{{end}} + +{{define "firoDocs"}} +
+
About Firo
+
+ Firo, formerly known as Zcoin, is a privacy-focused cryptocurrency launched in 2016 that enhances transactional anonymity using advanced cryptographic techniques. + It originally implemented the Zerocoin protocol for privacy but later transitioned to Lelantus and Lelantus Spark, which provide trustless, on-chain privacy without the need for a trusted setup. + Firo uses a hybrid proof-of-work (PoW) and masternode system, which helps secure the network while enabling features like instant and private transactions. + With a strong emphasis on financial privacy and decentralization, Firo continues to be a leading privacy coin, balancing anonymity, usability, and security in the cryptocurrency ecosystem. +
+
+ Website +
+
+{{end}} + +{{define "bchDocs"}} +
+
About Bitcoin Cash
+
+ Bitcoin Cash is a decentralized cryptocurrency that emerged in 2017 as a hard fork of Bitcoin, aiming to improve scalability and transaction efficiency. + It increases the block size limit to 32MB, allowing for faster and cheaper transactions compared to Bitcoin's 1MB blocks. + Bitcoin Cash retains Bitcoin's proof-of-work (PoW) consensus mechanism but focuses on being a peer-to-peer electronic cash system, prioritizing usability for everyday payments. + Despite ideological and technical splits within its community—leading to further forks like Bitcoin SV (BSV)—BCH remains one of the most widely used cryptocurrencies for low-cost, fast transactions and merchant adoption. +
+
+ Website +
+
+{{end}} + +{{define "polDocs"}} +
+
About Polygon
+
+ Polygon is a layer-2 scaling solution for Ethereum that enhances transaction speed and reduces costs while maintaining security and decentralization. + Originally launched as Matic Network in 2017, it rebranded to Polygon in 2021, expanding its vision to become a multi-chain ecosystem supporting various scaling technologies, + including sidechains, rollups, and zero-knowledge (ZK) proofs. + Polygon enables developers to build and deploy scalable decentralized applications (dApps) with lower gas fees while leveraging Ethereum's robust security. + Its native token, POL, is used for governance, staking, and transaction fees. + With growing adoption in DeFi, gaming, and enterprise blockchain solutions, Polygon plays a key role in Ethereum's scalability and mass adoption. +
+
+ Website + Documentation +
+
+{{end}} diff --git a/client/webserver/site/src/html/forms.tmpl b/client/webserver/site/src/html/forms.tmpl index ed7f2a3433..48591d3514 100644 --- a/client/webserver/site/src/html/forms.tmpl +++ b/client/webserver/site/src/html/forms.tmpl @@ -60,10 +60,7 @@ {{define "newWalletForm"}}
-
- - [[[add_a_x_wallet]]] -
+
[[[add_a_x_wallet]]]
[[[Token on]]] @@ -91,54 +88,53 @@
- +
-
-
- [[[Synchronizing]]] . -
- -
- % -
-
- [[[wallet_wait_synced]]]. -
-
{{end}} {{define "depositAddress"}}
[[[Receive]]] - - + +
-
- [[[Token on]]] - - -
-
- -
-
-
- - - [[[copied]]] +
+
Select a Network
+
+
+
+
+ [[[Token on]]] + + +
+
+ +
+
+
+ + + [[[copied]]] +
+
-
-
-
-
- +
+
+
+
+ +
+
-
{{end}} {{define "certPicker"}} @@ -521,7 +517,7 @@
- +
{{end}} diff --git a/client/webserver/site/src/html/init.tmpl b/client/webserver/site/src/html/init.tmpl index c9df9e041e..e4324980e0 100644 --- a/client/webserver/site/src/html/init.tmpl +++ b/client/webserver/site/src/html/init.tmpl @@ -9,6 +9,7 @@ [[[Set App Password]]] +
[[[reg_set_app_pw_msg]]]
@@ -25,7 +26,7 @@ [[[Restoration Seed]]]
- +
@@ -55,7 +56,7 @@
- +
diff --git a/client/webserver/site/src/html/markets.tmpl b/client/webserver/site/src/html/markets.tmpl index 78915a9629..d990a1baab 100644 --- a/client/webserver/site/src/html/markets.tmpl +++ b/client/webserver/site/src/html/markets.tmpl @@ -414,12 +414,6 @@
-
- -
{{- /* REPUTATION */ -}} diff --git a/client/webserver/site/src/html/wallets.tmpl b/client/webserver/site/src/html/wallets.tmpl index 0ebc13ef2a..054a0db0cd 100644 --- a/client/webserver/site/src/html/wallets.tmpl +++ b/client/webserver/site/src/html/wallets.tmpl @@ -1,6 +1,6 @@ {{define "wallets"}} {{template "top" .}} -
+
@@ -58,201 +58,277 @@
{{- /* WALLET DETAILS */ -}} -
- +
{{- /* EXPORT WALLET AUTHORIZATION */ -}}
+
[[[export_wallet]]] @@ -848,7 +915,7 @@
- +
@@ -1146,6 +1213,59 @@ + + {{- /* TRANSACTION HISTORY */ -}} +
+
+
[[[asset_name tx_history]]]
+ + + + +
+ + + + + + + + + + + + + + + + + + + + + + +
[[[Type]]][[[ID]]][[[Age]]][[[Fees]]][[[Amount]]]
+ + + + + +
[[[Mempool]]]
+
+ + + +
+ +
+ [[[load_earlier_transactions]]] +
+
+ [[[no_tx_history]]] +
+
+
{{template "bottom"}} diff --git a/client/webserver/site/src/js/app.ts b/client/webserver/site/src/js/app.ts index 23002f9df5..d7902c7d9a 100644 --- a/client/webserver/site/src/js/app.ts +++ b/client/webserver/site/src/js/app.ts @@ -53,6 +53,7 @@ import { PageElement, ActionRequiredNote, ActionResolvedNote, + TipChangeNote, TransactionActionNote, CoreActionRequiredNote, RejectedRedemptionData, @@ -182,6 +183,10 @@ export default class Application { txHistoryMap: Record requiredActions: Record onionUrl: string + dynamicUnits: { + div: PageElement + rows: PageElement + } constructor () { this.notes = [] @@ -202,6 +207,13 @@ export default class Application { } document.body.classList.add('loaded') + const div = document.createElement('div') as PageElement + div.classList.add('position-absolute', 'p-3') + const rows = document.createElement('div') as PageElement + div.appendChild(rows) + rows.classList.add('body-bg', 'border') + this.dynamicUnits = { div, rows } + // Loggers can be enabled by setting a truthy value to the loggerID using // enableLogger. Settings are stored across sessions. See docstring for the // log method for more info. @@ -375,6 +387,8 @@ export default class Application { return } this.attachCommon(this.main) + // Nix annoying auto-typing of form buttons. + for (const bttn of Doc.applySelector(this.main, 'form button')) bttn.setAttribute('type', 'button') if (this.loadedPage) this.loadedPage.unload() const constructor = constructors[handlerID] if (constructor) this.loadedPage = new constructor(this.main, data) @@ -415,13 +429,8 @@ export default class Application { * display elements. The menu gives users an option to convert the value * to their preferred units. */ - bindUnits (main: PageElement) { - const div = document.createElement('div') as PageElement - div.classList.add('position-absolute', 'p-3') - // div.style.backgroundColor = 'yellow' - const rows = document.createElement('div') as PageElement - div.appendChild(rows) - rows.classList.add('body-bg', 'border') + bindUnits (ancestor: PageElement) { + const { div, rows } = this.dynamicUnits const addRow = (el: PageElement, unit: string, cFactor: number) => { const box = Doc.safeSelector(el, '[data-unit-box]') const atoms = parseInt(box.dataset.atoms as string) @@ -434,9 +443,9 @@ export default class Application { Doc.setText(el, '[data-unit]', unit) }) } - for (const el of Doc.applySelector(main, '[data-conversion-value]')) { + for (const el of Doc.applySelector(ancestor, '[data-conversion-value]')) { const box = Doc.safeSelector(el, '[data-unit-box]') - Doc.bind(box, 'mouseenter', () => { + Doc.bind(box, 'click', () => { Doc.empty(rows) box.appendChild(div) const lyt = Doc.layoutMetrics(box) @@ -1164,6 +1173,12 @@ export default class Application { } case 'actionResolved': { this.resolveAction(n.payload as ActionResolvedNote) + break + } + case 'tipChange': { + const note = n.payload as TipChangeNote + const w = this.assets[note.assetID].wallet + if (w) w.syncStatus.blocks = note.tip } } if (n.payload.route === 'transactionHistorySynced') { @@ -1619,22 +1634,6 @@ export default class Application { clearTxHistory (assetID: number) { delete this.txHistoryMap[assetID] } - - async needsCustomProvider (assetID: number): Promise { - const baseChainID = this.assets[assetID]?.token?.parentID ?? assetID - if (!baseChainID) return false - const w = this.walletMap[baseChainID] - if (!w) return false - const traitAccountLocker = 1 << 14 - if ((w.traits & traitAccountLocker) === 0) return false - const res = await postJSON('/api/walletsettings', { assetID: baseChainID }) - if (!this.checkResponse(res)) { - console.error(res.msg) - return false - } - const settings = res.map as Record - return !settings.providers - } } /* getSocketURI returns the websocket URI for the client. */ diff --git a/client/webserver/site/src/js/dexsettings.ts b/client/webserver/site/src/js/dexsettings.ts index 8151c5ef4d..60764653c4 100644 --- a/client/webserver/site/src/js/dexsettings.ts +++ b/client/webserver/site/src/js/dexsettings.ts @@ -180,7 +180,7 @@ export default class DexSettingsPage extends BasePage { }, this.host) // forms.bind(page.bondDetailsForm, page.updateBondOptionsConfirm, () => this.updateBondOptions()) - forms.bind(page.disableAccountForm, page.disableAccountConfirm, () => this.toggleAccountStatus(true)) + Doc.bind(page.disableAccountConfirm, 'click', () => this.toggleAccountStatus(true)) Doc.bind(page.forms, 'mousedown', (e: MouseEvent) => { if (!Doc.mouseInElement(e, this.currentForm)) { this.closePopups() } diff --git a/client/webserver/site/src/js/doc.ts b/client/webserver/site/src/js/doc.ts index 4934e526ca..9062932bf8 100644 --- a/client/webserver/site/src/js/doc.ts +++ b/client/webserver/site/src/js/doc.ts @@ -127,7 +127,7 @@ function convertToConventional (v: number, unitInfo?: UnitInfo) { * 1 - (-5) = 6, so the conversion that has the order closest to * bestDisplayOrder is the first one, 1,000 BTC. */ -const bestDisplayOrder = 1 // 10^1 => 1 +const bestDisplayOrder = 3 // 10^3 => 1000 /* * resolveUnitConversions creates a lookup object mapping unit -> conversion @@ -562,6 +562,10 @@ export default class Doc { return d } + static clone (node: HTMLElement): PageElement { + return node.cloneNode(true) as PageElement + } + /* * timeSince returns a string representation of the duration since the * specified unix timestamp (milliseconds). diff --git a/client/webserver/site/src/js/forms.ts b/client/webserver/site/src/js/forms.ts index d5b865a859..a4a8f5c190 100644 --- a/client/webserver/site/src/js/forms.ts +++ b/client/webserver/site/src/js/forms.ts @@ -45,7 +45,6 @@ interface ProgressPoint { interface CurrentAsset { asset: SupportedAsset - parentAsset?: SupportedAsset winfo: WalletInfo | Token // selectedDef is used in a strange way for tokens. If a token's parent wallet // already exists, then selectedDef is going to be the Token.definition. @@ -56,18 +55,13 @@ interface CurrentAsset { selectedDef: WalletDefinition } -interface WalletConfig { - assetID: number - config: Record - walletType: string -} - interface FormsConfig { closed?: (closedForm: PageElement | undefined) => void } export class Forms { formsDiv: PageElement + forms: PageElement[] currentForm: PageElement | undefined currentFormID: string | undefined keyup: (e: KeyboardEvent) => void @@ -75,6 +69,7 @@ export class Forms { constructor (formsDiv: PageElement, cfg?: FormsConfig) { this.formsDiv = formsDiv + this.forms = Array.from(formsDiv.children) as PageElement[] this.closed = cfg?.closed formsDiv.querySelectorAll('.form-closer').forEach(el => { @@ -98,14 +93,76 @@ export class Forms { async show (form: HTMLElement, id?: string): Promise { this.currentForm = form this.currentFormID = id - Doc.hide(...Array.from(this.formsDiv.children)) - form.style.right = '10000px' + Doc.hide(...this.forms) Doc.show(this.formsDiv, form) - const shift = (this.formsDiv.offsetWidth + form.offsetWidth) / 2 - await Doc.animate(animationLength, progress => { - form.style.right = `${(1 - progress) * shift}px` + await this.randomAnim(form) + } + + randomAnim (form: HTMLElement) { + switch (Math.floor(Math.random() * 4)) { + case 0: return this.slideIn(form) + case 1: return this.popIn(form) + case 2: return this.dropIn(form) + case 3: return this.sneakIn(form) + } + } + + async slideIn (form: HTMLElement) { + if (oneOrNegativeOne() > 0) return this.slideInUpDown(form) + return this.slideInLeftRight(form) + } + + async slideInUpDown (form: HTMLElement) { + const ud = oneOrNegativeOne() + const shift = ud * (this.formsDiv.offsetHeight + form.offsetHeight) / 2 + form.style.top = `${shift}px` + await Doc.animate(animationLength, p => { + form.style.top = `${(1 - p) * shift}px` + }, 'easeOutHard') + } + + async slideInLeftRight (form: HTMLElement) { + const lr = oneOrNegativeOne() + const shift = lr * (this.formsDiv.offsetWidth + form.offsetWidth) / 2 + form.style.right = `${shift}px` + await Doc.animate(animationLength, p => { + form.style.right = `${(1 - p) * shift}px` + }, 'easeOutHard') + } + + async popIn (form: HTMLElement) { + await Doc.animate(animationLength, p => { + form.style.scale = `${p}` + }, 'easeOutHard') + } + + async dropIn (form: HTMLElement) { + await Doc.animate(animationLength, p => { + form.style.opacity = String(p) + form.style.scale = `${(1 - p) * 2 + 1}` + }, 'easeOutHard') + } + + async sneakIn (form: HTMLElement) { + const lr = oneOrNegativeOne() + const ud = oneOrNegativeOne() + const xShift = lr * (this.formsDiv.offsetWidth + form.offsetWidth) / 2 + const yShift = ud * (this.formsDiv.offsetHeight + form.offsetHeight) / 2 + await Doc.animate(animationLength, p => { + form.style.opacity = String(p) + form.style.left = `${(1 - p) * xShift}px` + form.style.top = `${(1 - p) * yShift}px` + form.style.scale = `${p}` }, 'easeOutHard') - form.style.right = '0' + } + + async showSuccess (msg: string) { + Doc.hide(...this.forms) + const checkmarkForm = Doc.idel(this.formsDiv, 'checkmarkForm') + this.currentForm = checkmarkForm + Doc.show(this.formsDiv, checkmarkForm) + await animateCheckmark(checkmarkForm, msg).wait() + Doc.hide(this.formsDiv) } close (): void { @@ -121,6 +178,10 @@ export class Forms { } } +function oneOrNegativeOne (): number { + return Math.sign(Math.random() - 0.5) +} + /* * NewWalletForm should be used with the "newWalletForm" template. The enclosing *
element should be the first argument of the constructor. @@ -132,7 +193,6 @@ export class NewWalletForm { current: CurrentAsset subform: WalletConfigForm walletCfgGuide: PageElement - parentSyncer: null | ((w: WalletState) => void) createUpdater: null | ((note: WalletCreationNote) => void) constructor (form: HTMLElement, success: (assetID: number) => void, backFunc?: () => void) { @@ -153,25 +213,14 @@ export class NewWalletForm { this.walletCfgGuide = Doc.tmplElement(form, 'walletCfgGuide') - bind(form, page.submitAdd, () => this.submit()) - bind(form, page.oneBttn, () => this.submit()) + Doc.bind(page.submitAdd, 'click', () => this.submit()) + Doc.bind(page.oneBttn, 'click', () => this.submit()) app().registerNoteFeeder({ - walletstate: (note: WalletStateNote) => { this.reportWalletState(note.wallet) }, - walletsync: (note: WalletSyncNote) => { if (this.parentSyncer) this.parentSyncer(app().walletMap[note.assetID]) }, createwallet: (note: WalletCreationNote) => { this.reportCreationUpdate(note) } }) } - /* - * reportWalletState should be called when a 'walletstate' notification is - * received. - * TODO: Let form classes register for notifications. - */ - reportWalletState (w: WalletState): void { - if (this.parentSyncer) this.parentSyncer(w) - } - /* * reportWalletState should be called when a 'createwallet' notification is * received. @@ -180,13 +229,12 @@ export class NewWalletForm { if (this.createUpdater) this.createUpdater(note) } - async createWallet (assetID: number, walletType: string, parentForm?: WalletConfig) { + async createWallet (assetID: number, walletType: string) { const createForm = { assetID: assetID, pass: this.page.newWalletPass.value || '', config: this.subform.map(assetID), walletType: walletType, - parentForm: parentForm } const ani = new Wave(this.form, { backgroundColor: true }) @@ -200,88 +248,17 @@ export class NewWalletForm { const newWalletPass = page.newWalletPass as HTMLInputElement Doc.hide(page.newWalletErr) - const { asset, parentAsset } = this.current + const { asset } = this.current const selectedDef = this.current.selectedDef - let parentForm let walletType = selectedDef.type - if (parentAsset) { - walletType = (asset.token as Token).definition.type - parentForm = { - assetID: parentAsset.id, - config: this.subform.map(parentAsset.id), - walletType: selectedDef.type - } - } // Register the selected asset. - const res = await this.createWallet(asset.id, walletType, parentForm) + const res = await this.createWallet(asset.id, walletType) if (!app().checkResponse(res)) { this.setError(res.msg) return } newWalletPass.value = '' - if (parentAsset) await this.runParentSync() - else this.success(this.current.asset.id) - } - - /* - * runParentSync shows a syncing sub-dialog that tracks the parent asset's - * syncProgress and informs the user that the token wallet will be created - * after sync is complete. - */ - async runParentSync () { - const { page, current: { parentAsset, asset } } = this - if (!parentAsset) return - - page.parentSyncPct.textContent = '0' - page.parentName.textContent = parentAsset.name - page.parentLogo.src = Doc.logoPath(parentAsset.symbol) - page.childName.textContent = asset.name - page.childLogo.src = Doc.logoPath(asset.symbol) - Doc.hide(page.mainForm) - Doc.show(page.parentSyncing) - - try { - await this.syncParent(parentAsset) - this.success(this.current.asset.id) - } catch (error) { - this.setError(error.message || error) - } - Doc.show(page.mainForm) - Doc.hide(page.parentSyncing) - } - - /* - * syncParent monitors the sync progress of a token's parent asset, generating - * an Error if the token wallet creation does not complete successfully. - */ - syncParent (parentAsset: SupportedAsset): Promise { - const { page, current: { asset } } = this - return new Promise((resolve, reject) => { - // First, check if it's already synced. - const w = app().assets[parentAsset.id].wallet - if (w && w.synced) return resolve() - // Not synced, so create a syncer to update the parent sync pane. - this.parentSyncer = (w: WalletState) => { - if (w.assetID !== parentAsset.id) return - page.parentSyncPct.textContent = (w.syncProgress * 100).toFixed(1) - } - // Handle the async result. - this.createUpdater = (note: WalletCreationNote) => { - if (note.assetID !== asset.id) return - switch (note.topic) { - case 'QueuedCreationFailed': - reject(new Error(`${note.subject}: ${note.details}`)) - break - case 'QueuedCreationSuccess': - resolve() - break - default: - return - } - this.parentSyncer = null - this.createUpdater = null - } - }) + this.success(this.current.asset.id) } /* setAsset sets the current asset of the NewWalletForm */ @@ -289,21 +266,15 @@ export class NewWalletForm { if (!this.parseAsset(assetID)) return // nothing to change const page = this.page const tabs = page.walletTypeTabs - const { winfo, asset, parentAsset } = this.current + const { winfo, asset } = this.current page.assetName.textContent = winfo.name page.newWalletPass.value = '' Doc.empty(tabs) Doc.hide(tabs, page.newWalletErr, page.tokenMsgBox) this.page.assetLogo.src = Doc.logoPath(asset.symbol) - if (parentAsset) { - page.tokenParentLogo.src = Doc.logoPath(parentAsset.symbol) - page.tokenParentName.textContent = parentAsset.name - Doc.show(page.tokenMsgBox) - } - const pinfo = parentAsset ? parentAsset.info : null - const walletDefs = pinfo ? pinfo.availablewallets : (winfo as WalletInfo).availablewallets ? (winfo as WalletInfo).availablewallets : [(winfo as Token).definition] + const walletDefs = (winfo as WalletInfo).availablewallets ? (winfo as WalletInfo).availablewallets : [(winfo as Token).definition] if (walletDefs.length > 1) { Doc.show(tabs) @@ -323,8 +294,7 @@ export class NewWalletForm { first.classList.add('selected') } - await this.update(this.current.selectedDef) - if (asset.walletCreationPending) await this.runParentSync() + return this.update(this.current.selectedDef) } /* @@ -347,7 +317,7 @@ export class NewWalletForm { return true } if (!parentAsset.info) throw Error('this parent has no wallet info!') - this.current = { asset, parentAsset, winfo: token, selectedDef: parentAsset.info.availablewallets[0] } + this.current = { asset, winfo: token, selectedDef: parentAsset.info.availablewallets[0] } return true } @@ -375,7 +345,7 @@ export class NewWalletForm { break } } - const { asset, parentAsset, winfo } = this.current + const { asset } = this.current const displayCreateBtn = walletDef.seeded || Boolean(asset.token) if (displayCreateBtn && !containsRequired) { Doc.hide(page.walletSettingsHeader) @@ -390,20 +360,7 @@ export class NewWalletForm { page.submitAdd.textContent = intl.prep(intl.ID_ADD) } - if (parentAsset) { - const parentAndTokenOpts = JSON.parse(JSON.stringify(configOpts)) - // Add the regAsset field to the configurations so proper logos will be displayed - // next to them, and map can filter them out. The opts are copied here so the originals - // do not have the regAsset field added to them. - for (const opt of parentAndTokenOpts) opt.regAsset = parentAsset.id - const tokenOpts = (winfo as Token).definition.configopts || [] - if (tokenOpts.length > 0) { - const tokenOptsCopy = JSON.parse(JSON.stringify(tokenOpts)) - for (const opt of tokenOptsCopy) opt.regAsset = asset.id - parentAndTokenOpts.push(...tokenOptsCopy) - } - this.subform.update(asset.id, parentAndTokenOpts, false) - } else this.subform.update(asset.id, configOpts, false) + this.subform.update(asset.id, configOpts, false) this.setGuideLink(guideLink) // A seeded or token wallet is internal to Bison Wallet and as such does @@ -435,16 +392,11 @@ export class NewWalletForm { */ async loadDefaults () { // No default config files for seeded assets right now. - const { asset, parentAsset, selectedDef } = this.current + const { asset: { id: assetID }, selectedDef } = this.current if (!selectedDef.configpath) return - let configID = asset.id - if (parentAsset) { - if (selectedDef.seeded) return - configID = parentAsset.id - } const loaded = app().loading(this.form) const res = await postJSON('/api/defaultwalletcfg', { - assetID: configID, + assetID, type: selectedDef.type }) loaded() @@ -804,7 +756,7 @@ export class ConfirmRegistrationForm { this.certFile = '' Doc.bind(this.page.goBack, 'click', () => goBack()) - bind(form, this.page.submit, () => this.submitForm()) + Doc.bind(this.page.submit, 'click', () => this.submitForm()) } setExchange (xc: Exchange, certFile: string) { @@ -1220,7 +1172,6 @@ export class FeeAssetSelectionForm { */ function setReadyMessage (el: PageElement, asset: SupportedAsset) { if (asset.wallet) el.textContent = intl.prep(intl.ID_WALLET_READY) - else if (asset.walletCreationPending) el.textContent = intl.prep(intl.ID_WALLET_PENDING) else el.textContent = intl.prep(intl.ID_SETUP_NEEDED) el.classList.remove('readygreen', 'setuporange') el.classList.add(asset.wallet ? 'readygreen' : 'setuporange') @@ -1642,7 +1593,7 @@ export class DEXAddressForm { }) } - bind(form, page.submit, () => this.checkDEX()) + Doc.bind(page.submit, 'click', () => this.checkDEX()) if (dexToUpdate) { Doc.hide(page.addDexHdr, page.skipRegistrationBox) @@ -1753,7 +1704,7 @@ export class DiscoverAccountForm { const page = this.page = Doc.parseTemplate(form) page.dexHost.textContent = addr - bind(form, page.submit, () => this.checkDEX()) + Doc.bind(page.submit, 'click', () => this.checkDEX()) } /* Just a small size tweak and fade-in. */ @@ -1798,7 +1749,7 @@ export class LoginForm { this.success = success this.form = form const page = this.page = Doc.parseTemplate(form) - bind(form, page.submit, () => { this.submit() }) + Doc.bind(page.submit, 'click', () => { this.submit() }) app().registerNoteFeeder({ login: (note: CoreNote) => { this.handleLoginNote(note) } }) @@ -1864,24 +1815,53 @@ export class DepositAddress { form: PageElement page: Record assetID: number + netSelectBttnTmpl: PageElement constructor (form: PageElement) { this.form = form - const page = this.page = Doc.idDescendants(form) + this.netSelectBttnTmpl = Doc.tmplElement(form, 'netSelectBttnTmpl') + this.netSelectBttnTmpl.remove() + const page = this.page = Doc.parseTemplate(form) Doc.cleanTemplates(page.unifiedReceiverTmpl) Doc.bind(page.newDepAddrBttn, 'click', async () => { this.newDepositAddress() }) Doc.bind(page.copyAddressBtn, 'click', () => { this.copyAddress() }) } - /* Display a deposit address. */ + async setAssetSelect (assetIDs: number[]) { + if (assetIDs.length === 1) { + this.setAsset(assetIDs[0]) + return + } + const { page, netSelectBttnTmpl } = this + const assets = assetIDs.map((assetID: number) => app().assets[assetID]) + assets.sort((a: SupportedAsset) => a.token ? 1 : -1) + const { symbol, unitInfo } = assets[0] + page.depositLogo.src = Doc.logoPath(symbol) + page.depositName.textContent = unitInfo.conventional.unit + Doc.hide(page.mainForm) + Doc.show(page.netSelectForm) + Doc.empty(page.netSelectBox) + for (const { id: assetID, symbol, token, name } of assets) { + const bttn = Doc.clone(netSelectBttnTmpl) + page.netSelectBox.appendChild(bttn) + const tmpl = Doc.parseTemplate(bttn) + const chainSymbol = token ? app().assets[token.parentID].symbol : symbol + tmpl.logo.src = Doc.logoPath(chainSymbol) + const chainName = token ? app().assets[token.parentID].name : name + tmpl.chainName.textContent = chainName + Doc.bind(bttn, 'click', () => this.setAsset(assetID)) + } + } + async setAsset (assetID: number) { this.assetID = assetID const page = this.page - Doc.hide(page.depositErr, page.depositTokenMsgBox) + Doc.hide(page.depositErr, page.depositTokenMsgBox, page.netSelectForm) + Doc.show(page.mainForm) const asset = app().assets[assetID] page.depositLogo.src = Doc.logoPath(asset.symbol) - const wallet = app().walletMap[assetID] page.depositName.textContent = asset.unitInfo.conventional.unit + const wallet = app().walletMap[assetID] if (asset.token) { const parentAsset = app().assets[asset.token.parentID] page.depositTokenParentLogo.src = Doc.logoPath(parentAsset.symbol) @@ -1967,7 +1947,7 @@ export class AppPassResetForm { this.form = form this.success = success const page = this.page = Doc.idDescendants(form) - bind(form, page.resetAppPWSubmitBtn, () => this.resetAppPW()) + Doc.bind(page.resetAppPWSubmitBtn, 'click', () => this.resetAppPW()) } async resetAppPW () { @@ -2235,9 +2215,14 @@ export async function slideSwap (form1: HTMLElement, form2: HTMLElement) { } export function showSuccess (page: Record, msg: string) { - page.successMessage.textContent = msg Doc.show(page.forms, page.checkmarkForm) - page.checkmarkForm.style.right = '0' + return animateCheckmark(page.checkmarkForm, msg) +} + +function animateCheckmark (checkmarkForm: PageElement, msg: string) { + const page = Doc.idDescendants(checkmarkForm) + page.successMessage.textContent = msg + checkmarkForm.style.right = '0' page.checkmark.style.fontSize = '0px' const [startR, startG, startB] = State.isDark() ? [223, 226, 225] : [51, 51, 51] @@ -2250,19 +2235,6 @@ export function showSuccess (page: Record, msg: string) { }, 'easeOutElastic') } -/* - * bind binds the click and submit events and prevents page reloading on - * submission. - */ -export function bind (form: HTMLElement, submitBttn: HTMLElement, handler: (e: Event) => void) { - const wrapper = (e: Event) => { - if (e.preventDefault) e.preventDefault() - handler(e) - } - Doc.bind(submitBttn, 'click', wrapper) - Doc.bind(form, 'submit', wrapper) -} - // isTruthyString will be true if the provided string is recognized as a // value representing true. function isTruthyString (s: string) { diff --git a/client/webserver/site/src/js/init.ts b/client/webserver/site/src/js/init.ts index e04f14a6b7..5b2c7f7700 100644 --- a/client/webserver/site/src/js/init.ts +++ b/client/webserver/site/src/js/init.ts @@ -2,10 +2,7 @@ import Doc from './doc' import BasePage from './basepage' import { postJSON } from './http' import * as intl from './locales' -import { - bind as bindForm, - slideSwap -} from './forms' +import { slideSwap } from './forms' import { Wave } from './charts' import { app, @@ -90,8 +87,8 @@ class AppInitForm { this.form = form this.success = success const page = this.page = Doc.idDescendants(form) - bindForm(form, page.appPWSubmit, () => this.setAppPass()) - bindForm(form, page.toggleSeedInput, () => { + Doc.bind(page.appPWSubmit, 'click', () => this.setAppPass()) + Doc.bind(page.toggleSeedInput, 'click', () => { if (Doc.isHidden(page.seedInputBox)) { page.toggleSeedInputIcon.classList.remove('ico-plus') page.toggleSeedInputIcon.classList.add('ico-minus') @@ -172,8 +169,8 @@ class QuickConfigForm { this.success = success const page = this.page = Doc.idDescendants(form) Doc.cleanTemplates(page.qcServerTmpl, page.qcWalletTmpl) - bindForm(form, page.quickConfigSubmit, () => { this.submit() }) - bindForm(form, page.qcErrAck, () => { this.success() }) + Doc.bind(page.quickConfigSubmit, 'click', () => { this.submit() }) + Doc.bind(page.qcErrAck, 'click', () => { this.success() }) } async update (pw: string, hosts: string[]) { @@ -300,8 +297,8 @@ class SeedBackupForm { constructor (form: PageElement, success: () => void) { this.form = form const page = this.page = Doc.idDescendants(form) - bindForm(form, page.seedAck, () => success()) - bindForm(form, page.showSeed, () => this.showSeed()) + Doc.bind(page.seedAck, 'click', () => success()) + Doc.bind(page.showSeed, 'click', () => this.showSeed()) } update (mnemonic: string) { diff --git a/client/webserver/site/src/js/locales.ts b/client/webserver/site/src/js/locales.ts index ff7b927234..dda612856f 100644 --- a/client/webserver/site/src/js/locales.ts +++ b/client/webserver/site/src/js/locales.ts @@ -53,7 +53,6 @@ export const ID_SETUP_WALLET = 'SETUP_WALLET' export const ID_WALLET_READY = 'WALLET_READY' export const ID_CHANGE_WALLET_TYPE = 'CHANGE_WALLET_TYPE' export const ID_KEEP_WALLET_TYPE = 'KEEP_WALLET_TYPE' -export const ID_WALLET_PENDING = 'WALLET_PENDING' export const ID_SETUP_NEEDED = 'SETUP_NEEDED' export const ID_SEND_SUCCESS = 'SEND_SUCCESS' export const ID_RECONFIG_SUCCESS = 'RECONFIG_SUCCESS' diff --git a/client/webserver/site/src/js/markets.ts b/client/webserver/site/src/js/markets.ts index 689ea7771b..cb46d5f189 100644 --- a/client/webserver/site/src/js/markets.ts +++ b/client/webserver/site/src/js/markets.ts @@ -19,7 +19,6 @@ import { AccelerateOrderForm, DepositAddress, TokenApprovalForm, - bind as bindForm, Forms } from './forms' import * as OrderUtil from './orderutil' @@ -274,8 +273,6 @@ export default class MarketsPage extends BasePage { bind(wgt.quote.tmpl.newWalletBttn, 'click', () => { this.showCreate(this.market.quote) }) bind(wgt.base.tmpl.walletAddr, 'click', () => { this.showDeposit(this.market.base.id) }) bind(wgt.quote.tmpl.walletAddr, 'click', () => { this.showDeposit(this.market.quote.id) }) - bind(wgt.base.tmpl.wantProviders, 'click', () => { this.showCustomProviderDialog(this.market.base.id) }) - bind(wgt.quote.tmpl.wantProviders, 'click', () => { this.showCustomProviderDialog(this.market.quote.id) }) this.depositAddrForm = new DepositAddress(page.deposit) } @@ -291,7 +288,7 @@ export default class MarketsPage extends BasePage { this.reputationMeter = new ReputationMeter(page.reputationMeter) // Bind toggle wallet status form. - bindForm(page.toggleWalletStatusConfirm, page.toggleWalletStatusSubmit, async () => { this.toggleWalletStatus() }) + Doc.bind(page.toggleWalletStatusSubmit, 'click', async () => { this.toggleWalletStatus() }) // Prepare templates for the buy and sell tables and the user's order table. setOptionTemplates(page) @@ -375,11 +372,11 @@ export default class MarketsPage extends BasePage { // Create a wallet this.newWalletForm = new NewWalletForm(page.newWalletForm, async () => { this.createWallet() }) // Main order form. - bindForm(page.orderForm, page.submitBttn, async () => { this.stepSubmit() }) + Doc.bind(page.submitBttn, 'click', async () => { this.stepSubmit() }) // Order verification form. - bindForm(page.verifyForm, page.vSubmit, async () => { this.submitOrder() }) + Doc.bind(page.vSubmit, 'click', async () => { this.submitOrder() }) // Cancel order form. - bindForm(page.cancelForm, page.cancelSubmit, async () => { this.submitCancel() }) + Doc.bind(page.cancelSubmit, 'click', async () => { this.submitCancel() }) // Order detail view. Doc.bind(page.vFeeDetails, 'click', () => this.forms.show(page.vDetailPane)) Doc.bind(page.closeDetailPane, 'click', () => this.showVerifyForm()) @@ -2450,10 +2447,6 @@ export default class MarketsPage extends BasePage { this.forms.show(this.page.deposit) } - showCustomProviderDialog (assetID: number) { - app().loadPage('wallets', { promptProvider: assetID, goBack: 'markets' }) - } - /* * handlePriceUpdate is the handler for the 'spots' notification. */ @@ -3315,9 +3308,8 @@ class BalanceWidget { // Just hide everything to start. Doc.hide( tmpl.newWalletRow, tmpl.expired, tmpl.unsupported, tmpl.connect, tmpl.spinner, - tmpl.walletState, tmpl.balanceRows, tmpl.walletAddr, tmpl.wantProvidersBox + tmpl.walletState, tmpl.balanceRows, tmpl.walletAddr ) - this.checkNeedsProvider(assetID, tmpl.wantProvidersBox) tmpl.logo.src = Doc.logoPath(cfg.symbol) tmpl.addWalletSymbol.textContent = cfg.symbol.toUpperCase() Doc.empty(tmpl.symbol) @@ -3333,10 +3325,6 @@ class BalanceWidget { stateIcons.readWallet(wallet) // Handle no wallet configured. if (!wallet) { - if (asset.walletCreationPending) { - Doc.show(tmpl.spinner) - return - } Doc.show(tmpl.newWalletRow) return } @@ -3390,10 +3378,6 @@ class BalanceWidget { } else Doc.hide(tmpl.expired) } - async checkNeedsProvider (assetID: number, el: PageElement) { - Doc.setVis(await app().needsCustomProvider(assetID), el) - } - /* updateParent updates the side's parent asset balance. */ updateParent (side: BalanceWidgetElement) { const { wallet: { balance }, unitInfo } = app().assets[side.parentID] diff --git a/client/webserver/site/src/js/mmsettings.ts b/client/webserver/site/src/js/mmsettings.ts index 0a5e0f44e5..e9776e30ba 100644 --- a/client/webserver/site/src/js/mmsettings.ts +++ b/client/webserver/site/src/js/mmsettings.ts @@ -53,7 +53,7 @@ import { GapStrategyPercentPlus, feesAndCommit } from './mmutil' -import { Forms, bind as bindForm, NewWalletForm, TokenApprovalForm, DepositAddress, CEXConfigurationForm } from './forms' +import { Forms, NewWalletForm, TokenApprovalForm, DepositAddress, CEXConfigurationForm } from './forms' import * as intl from './locales' import * as OrderUtil from './orderutil' @@ -288,7 +288,7 @@ export default class MarketMakerSettingsPage extends BasePage { Doc.bind(page.updateButton, 'click', () => { this.saveSettings() }) Doc.bind(page.createButton, 'click', async () => { this.saveSettings() }) Doc.bind(page.deleteBttn, 'click', () => { this.delete() }) - bindForm(page.botTypeForm, page.botTypeSubmit, () => { this.submitBotType() }) + Doc.bind(page.botTypeSubmit, 'click', () => { this.submitBotType() }) Doc.bind(page.noMarketBttn, 'click', () => { this.showMarketSelectForm() }) Doc.bind(page.botTypeHeader, 'click', () => { this.reshowBotTypeForm() }) Doc.bind(page.botTypeChangeMarket, 'click', () => { this.showMarketSelectForm() }) diff --git a/client/webserver/site/src/js/order.ts b/client/webserver/site/src/js/order.ts index 47a56b71e0..696098e7c2 100644 --- a/client/webserver/site/src/js/order.ts +++ b/client/webserver/site/src/js/order.ts @@ -1,7 +1,7 @@ import Doc from './doc' import BasePage from './basepage' import * as OrderUtil from './orderutil' -import { bind as bindForm, AccelerateOrderForm } from './forms' +import { AccelerateOrderForm } from './forms' import { postJSON } from './http' import * as intl from './locales' import { @@ -100,7 +100,7 @@ export default class OrderPage extends BasePage { }) // Cancel order form - bindForm(page.cancelForm, page.cancelSubmit, async () => { this.submitCancel() }) + Doc.bind(page.cancelSubmit, 'click', async () => { this.submitCancel() }) this.secondTicker = window.setInterval(() => { setStamp() diff --git a/client/webserver/site/src/js/registry.ts b/client/webserver/site/src/js/registry.ts index a1e10b692f..39d39d714f 100644 --- a/client/webserver/site/src/js/registry.ts +++ b/client/webserver/site/src/js/registry.ts @@ -206,7 +206,6 @@ export interface SupportedAsset { info?: WalletInfo token?: Token unitInfo: UnitInfo - walletCreationPending: boolean } export interface Token { @@ -245,13 +244,13 @@ export interface WalletState { assetID: number version: number type: string + class: string traits: number open: boolean running: boolean disabled: boolean balance: WalletBalance address: string - units: string encrypted: boolean peerCount: number synced: boolean @@ -1323,7 +1322,7 @@ export interface Application { getWalletTx(assetID: number, txid: string): WalletTransaction | undefined clearTxHistory(assetID: number): void parentAsset(assetID: number): SupportedAsset - needsCustomProvider (assetID: number): Promise + bindUnits (ancestor: PageElement): void } // TODO: Define an interface for Application? diff --git a/client/webserver/site/src/js/settings.ts b/client/webserver/site/src/js/settings.ts index 59e6758446..dbb8085edf 100644 --- a/client/webserver/site/src/js/settings.ts +++ b/client/webserver/site/src/js/settings.ts @@ -136,10 +136,10 @@ export default class SettingsPage extends BasePage { }) Doc.bind(page.importAccount, 'click', () => this.prepareAccountImport(page.authorizeAccountImportForm)) - forms.bind(page.authorizeAccountImportForm, page.authorizeImportAccountConfirm, () => this.importAccount()) + Doc.bind(page.authorizeImportAccountConfirm, 'click', () => this.importAccount()) Doc.bind(page.changeAppPW, 'click', () => this.showForm(page.changeAppPWForm)) - forms.bind(page.changeAppPWForm, page.submitNewPW, () => this.changeAppPW()) + Doc.bind(page.submitNewPW, 'click', () => this.changeAppPW()) this.appPassResetForm = new forms.AppPassResetForm(page.resetAppPWForm, async () => { await app().loadPage('login') @@ -171,7 +171,7 @@ export default class SettingsPage extends BasePage { Doc.hide(page.exportSeedErr) this.showForm(page.exportSeedAuth) }) - forms.bind(page.exportSeedAuth, page.exportSeedSubmit, () => this.submitExportSeedReq()) + Doc.bind(page.exportSeedSubmit, 'click', () => this.submitExportSeedReq()) Doc.bind(page.gameCodeLink, 'click', () => this.showForm(page.gameCodeForm)) Doc.bind(page.gameCodeSubmit, 'click', () => this.submitGameCode()) diff --git a/client/webserver/site/src/js/wallets.ts b/client/webserver/site/src/js/wallets.ts index f78569fe31..e20d6a1306 100644 --- a/client/webserver/site/src/js/wallets.ts +++ b/client/webserver/site/src/js/wallets.ts @@ -5,8 +5,7 @@ import { NewWalletForm, WalletConfigForm, DepositAddress, - bind as bindForm, - showSuccess + Forms } from './forms' import State from './state' import * as intl from './locales' @@ -19,7 +18,6 @@ import { BalanceNote, WalletStateNote, WalletSyncNote, - RateNote, Order, OrderFilter, WalletCreationNote, @@ -41,8 +39,7 @@ import { TxHistoryResult, TransactionNote, WalletTransaction, - FeeState, - WalletBalance + WalletInfo } from './registry' import { CoinExplorers } from './coinexplorers' @@ -59,27 +56,141 @@ interface TicketPurchaseUpdate extends BaseWalletNote { stats?: TicketStats } -interface AssetState { +class ChainAsset { + assetID: number symbol: string + ui: UnitInfo + chainName: string + chainLogo: string ticker: string - bal: WalletBalance - xcRate: number + token?: { + parentMade: boolean + parentID: number + feeUI: UnitInfo + } + + constructor (a: SupportedAsset) { + const { id: assetID, symbol, name, token, unitInfo: ui, unitInfo: { conventional: { unit: ticker } } } = a + this.assetID = assetID + this.ticker = ticker + this.symbol = symbol + this.ui = ui + this.chainName = token ? app().assets[token.parentID].name : name + this.chainLogo = token ? Doc.logoPath(app().assets[token.parentID].symbol) : Doc.logoPath(symbol) + if (token) this.token = { parentID: token.parentID, feeUI: app().unitInfo(token.parentID), parentMade: Boolean(app().assets[token.parentID].wallet) } + } + + get bal () { + const w = app().assets[this.assetID].wallet + return w?.balance ?? { available: 0, locked: 0, immature: 0 } + } + + updateTokenParentMade () { + if (!this.token) return false + this.token.parentMade = Boolean(app().assets[this.token.parentID].wallet) + } } -interface TickerAsset { +class TickerAsset { ticker: string // normalized e.g. WETH -> ETH - hasTokens: boolean + hasWallets: boolean cFactor: number + bestID: number logoSymbol: string - bal: { - avail: number - immature: number - locked: number - total: number - } - xcRate: number - assets: AssetState[] - lookup: Record + name: string + chainAssets: ChainAsset[] + chainAssetLookup: Record + haveAllFiatRates: boolean + isMultiNet: boolean + hasTokens: boolean + ui: UnitInfo + + constructor (a: SupportedAsset) { + const { id: assetID, name, symbol, unitInfo: ui, unitInfo: { conventional: {conversionFactor: cFactor } } } = a + this.ticker = normalizedTicker(a) + this.cFactor = cFactor + this.chainAssets = [] + this.chainAssetLookup = {} + this.bestID = assetID + this.name = name + this.logoSymbol = symbol + this.ui = ui + this.addChainAsset(a) + } + + addChainAsset (a: SupportedAsset) { + const { id: assetID, symbol, name, wallet: w, token, unitInfo: ui } = a + const xcRate = app().fiatRatesMap[assetID] + if (!xcRate) this.haveAllFiatRates = false + this.hasTokens = this.hasTokens || Boolean(token) + if (!token) { // prefer the native asset data, e.g. weth.polygon -> eth} + this.bestID = assetID + this.logoSymbol = symbol + this.name = name + this.ui = ui + } + const ca = new ChainAsset(a) + this.hasWallets = this.hasWallets || Boolean(w) || Boolean(ca.token?.parentMade) + this.chainAssets.push(ca) + this.chainAssetLookup[a.id] = ca + this.chainAssets.sort((a: ChainAsset, b: ChainAsset) => { + if (a.token && !b.token) return 1 + if (!a.token && b.token) return -1 + return a.ticker.localeCompare(b.ticker) + }) + this.isMultiNet = this.chainAssets.length > 1 + } + + walletInfo (): WalletInfo | undefined { + for (const { assetID } of this.chainAssets) { + const { info } = app().assets[assetID] + if (info) return info + } + } + + updateHasWallets () { + for (const ta of this.chainAssets) { + ta.updateTokenParentMade() + const { assetID, token } = ta + if (app().walletMap[assetID] || token?.parentMade) { + this.hasWallets = true + return + } + } + } + + /* + * blockchainWallet returns the assetID and wallet for the blockchain for + * which this ticker is a native asset, if it exists. + */ + blockchainWallet () { + for (const { assetID } of this.chainAssets) { + const { wallet, token } = app().assets[assetID] + if (!token) return { assetID, wallet } + } + } + + get avail () { + return this.chainAssets.reduce((sum: number, ca: ChainAsset) => sum + ca.bal.available, 0) + } + + get immature () { + return this.chainAssets.reduce((sum: number, ca: ChainAsset) => sum + ca.bal.immature, 0) + } + + get locked () { + return this.chainAssets.reduce((sum: number, ca: ChainAsset) => sum + ca.bal.locked, 0) + } + + get total () { + return this.chainAssets.reduce((sum: number, { bal: { available, locked, immature } }: ChainAsset) => { + return sum + available + locked + immature + }, 0) + } + + get xcRate () { + return app().fiatRatesMap[this.bestID] + } } const animationLength = 300 @@ -216,11 +327,6 @@ interface WalletRestoration { instructions: string } -interface AssetButton { - tmpl: Record - bttn: PageElement -} - interface TicketPagination { number: number history: Ticket[] @@ -242,12 +348,15 @@ export default class WalletsPage extends BasePage { body: HTMLElement data?: WalletsPageData page: Record - - tickers: TickerAsset[] - haveAllFiatRates: boolean - totalUSD: number - - // assetButtons: Record + forms: Forms + selectedTicker: TickerAsset + tickerMap: Record + tickerList: TickerAsset[] + balTracker: Record + tickerTemplates: Record> + tickerButtons: Record + balanceDetails: Record + walletConfig: Record newWalletForm: NewWalletForm reconfigForm: WalletConfigForm walletCfgGuide: PageElement @@ -256,12 +365,12 @@ export default class WalletsPage extends BasePage { changeWalletPW: boolean displayed: HTMLElement animation: Animation - forms: PageElement[] + formsList: PageElement[] forceReq: RescanRecoveryRequest forceUrl: string currentForm: PageElement restoreInfoCard: HTMLElement - selectedAssetID: number + selectedWalletID: number stakeStatus: TicketStakingStatus maxSend: number unapprovingTokenVersion: number @@ -279,6 +388,15 @@ export default class WalletsPage extends BasePage { this.data = data const page = this.page = Doc.idDescendants(body) this.stampers = [] + this.balTracker = {} + this.tickerTemplates = {} + this.tickerButtons = {} + this.selectedWalletID = -1 + + this.balanceDetails = Doc.parseTemplate(page.balanceDetails) + this.walletConfig = Doc.parseTemplate(page.walletConfig) + this.walletConfig.div = page.walletConfig + net = app().user.net const setStamp = () => { @@ -292,117 +410,98 @@ export default class WalletsPage extends BasePage { setStamp() }, 10000) // update every 10 seconds - Doc.cleanTemplates(page.restoreInfoCard, page.connectedIconTmpl, page.disconnectedIconTmpl, page.removeIconTmpl, page.tickerBalsBox) + Doc.cleanTemplates( + page.restoreInfoCard, page.connectedIconTmpl, page.disconnectedIconTmpl, + page.removeIconTmpl, page.tickerBalsBox, page.blockchainBalanceTmpl, + page.multiNetTxFeeTmpl, page.multiNetFeeRateTmpl, page.netTxFeeTmpl, + page.netSelectBttnTmpl + ) this.restoreInfoCard = page.restoreInfoCard.cloneNode(true) as HTMLElement Doc.show(page.connectedIconTmpl, page.disconnectedIconTmpl, page.removeIconTmpl) - this.forms = Doc.applySelector(page.forms, ':scope > form') - page.forms.querySelectorAll('.form-closer').forEach(el => { - Doc.bind(el, 'click', () => { this.closePopups() }) - }) - Doc.bind(page.cancelForce, 'click', () => { this.closePopups() }) - - this.selectedAssetID = -1 Doc.cleanTemplates( - page.iconSelectTmpl, page.balanceDetailRow, page.recentOrderTmpl, page.vspRowTmpl, + page.iconSelectTmpl, page.recentOrderTmpl, page.vspRowTmpl, page.ticketHistoryRowTmpl, page.votingChoiceTmpl, page.votingAgendaTmpl, page.tspendTmpl, page.tkeyTmpl, page.txHistoryRowTmpl, page.txHistoryDateRowTmpl ) - Doc.bind(page.createWallet, 'click', () => this.showNewWallet(this.selectedAssetID)) - Doc.bind(page.connectBttn, 'click', () => this.doConnect(this.selectedAssetID)) - Doc.bind(page.send, 'click', () => this.showSendForm(this.selectedAssetID)) - Doc.bind(page.receive, 'click', () => this.showDeposit(this.selectedAssetID)) - Doc.bind(page.unlockBttn, 'click', () => this.openWallet(this.selectedAssetID)) - Doc.bind(page.lockBttn, 'click', () => this.lock(this.selectedAssetID)) - Doc.bind(page.reconfigureBttn, 'click', () => this.showReconfig(this.selectedAssetID)) - Doc.bind(page.needsProviderBttn, 'click', () => this.showReconfig(this.selectedAssetID)) - Doc.bind(page.rescanWallet, 'click', () => this.rescanWallet(this.selectedAssetID)) + Doc.bind(page.cancelForce, 'click', () => { this.forms.close() }) + Doc.bind(page.createWallet, 'click', () => this.showNewWallet(this.selectedWalletID)) + Doc.bind(page.connectBttn, 'click', () => this.doConnect(this.selectedWalletID)) + Doc.bind(page.send, 'click', () => this.showSendForm()) + Doc.bind(page.receive, 'click', () => this.showDeposit()) + Doc.bind(page.unlockBttn, 'click', () => this.openWallet(this.selectedWalletID)) + Doc.bind(page.lockBttn, 'click', () => this.lock(this.selectedWalletID)) + Doc.bind(page.reconfigureBttn, 'click', () => this.showReconfig(this.selectedWalletID)) + Doc.bind(page.rescanWallet, 'click', () => this.rescanWallet(this.selectedWalletID)) Doc.bind(page.earlierTxs, 'click', () => this.loadEarlierTxs()) - Doc.bind(page.copyTxIDBtn, 'click', () => { setupCopyBtn(this.currTx?.id || '', page.txDetailsID, page.copyTxIDBtn, '#1e7d11') }) Doc.bind(page.copyRecipientBtn, 'click', () => { setupCopyBtn(this.currTx?.recipient || '', page.txDetailsRecipient, page.copyRecipientBtn, '#1e7d11') }) Doc.bind(page.copyBondIDBtn, 'click', () => { setupCopyBtn(this.currTx?.bondInfo?.bondID || '', page.txDetailsBondID, page.copyBondIDBtn, '#1e7d11') }) Doc.bind(page.copyBondAccountIDBtn, 'click', () => { setupCopyBtn(this.currTx?.bondInfo?.accountID || '', page.txDetailsBondAccountID, page.copyBondAccountIDBtn, '#1e7d11') }) - Doc.bind(page.hideMixTxsCheckbox, 'change', () => { this.showTxHistory(this.selectedAssetID) }) + Doc.bind(page.hideMixTxsCheckbox, 'change', () => { this.showTxHistory(this.selectedWalletID) }) + + // Forms + this.forms = new Forms(page.forms) + this.keyup = (e: KeyboardEvent) => { + if (e.key === 'Escape') this.forms.close() + } + Doc.bind(document, 'keyup', this.keyup) - // Bind the new wallet form. - this.newWalletForm = new NewWalletForm(page.newWalletForm, (assetID: number) => { + this.newWalletForm = new NewWalletForm(page.newWalletForm, async (assetID: number) => { + await app().fetchUser() const fmtParams = { assetName: app().assets[assetID].name } this.assetUpdated(assetID, page.newWalletForm, intl.prep(intl.ID_NEW_WALLET_SUCCESS, fmtParams)) - this.sortAssetButtons() - this.updateTicketBuyer(assetID) - this.updatePrivacy(assetID) + for (const ta of this.tickerList) ta.updateHasWallets() + this.refreshBalances() + this.sortTickers() + this.updateGlobalBalance() + if (this.selectedTicker.chainAssetLookup[assetID]) this.updateDisplayedTicker() + this.updateTicketBuyer() + this.updatePrivacy() }) - // Bind the wallet reconfig form. this.reconfigForm = new WalletConfigForm(page.reconfigInputs, false) - this.walletCfgGuide = Doc.tmplElement(page.reconfigForm, 'walletCfgGuide') - - // Bind the send form. - bindForm(page.sendForm, page.submitSendForm, async () => { this.stepSend() }) - // Send confirmation form. - bindForm(page.vSendForm, page.vSend, async () => { this.send() }) - // Bind the wallet reconfiguration submission. - bindForm(page.reconfigForm, page.submitReconfig, () => this.reconfig()) - - page.forms.querySelectorAll('.form-closer').forEach(el => { - Doc.bind(el, 'click', () => this.closePopups()) - }) - - Doc.bind(page.forms, 'mousedown', (e: MouseEvent) => { - if (!Doc.mouseInElement(e, this.currentForm)) { this.closePopups() } - }) - + this.depositAddrForm = new DepositAddress(page.deposit) this.mixerToggle = new AniToggle(page.toggleMixer, page.mixingErr, false, (newState: boolean) => { return this.updateMixerState(newState) }) - this.keyup = (e: KeyboardEvent) => { - if (e.key === 'Escape') { - if (Doc.isDisplayed(this.page.forms)) this.closePopups() - } - } - Doc.bind(document, 'keyup', this.keyup) - + Doc.bind(page.submitSendForm, 'click', async () => { this.stepSend() }) + Doc.bind(page.vSend, 'click', async () => { this.send() }) + Doc.bind(page.submitReconfig, 'click', () => this.reconfig()) Doc.bind(page.downloadLogs, 'click', async () => { this.downloadLogs() }) Doc.bind(page.exportWallet, 'click', async () => { this.displayExportWalletAuth() }) Doc.bind(page.recoverWallet, 'click', async () => { this.showRecoverWallet() }) - bindForm(page.exportWalletAuth, page.exportWalletAuthSubmit, async () => { this.exportWalletAuthSubmit() }) - bindForm(page.recoverWalletConfirm, page.recoverWalletSubmit, () => { this.recoverWallet() }) - bindForm(page.confirmForce, page.confirmForceSubmit, async () => { this.confirmForceSubmit() }) + Doc.bind(page.exportWalletAuthSubmit, 'click', async () => { this.exportWalletAuthSubmit() }) + Doc.bind(page.recoverWalletSubmit, 'click', () => { this.recoverWallet() }) + Doc.bind(page.confirmForceSubmit, 'click', async () => { this.confirmForceSubmit() }) Doc.bind(page.disableWallet, 'click', async () => { this.showToggleWalletStatus(true) }) Doc.bind(page.enableWallet, 'click', async () => { this.showToggleWalletStatus(false) }) - bindForm(page.toggleWalletStatusConfirm, page.toggleWalletStatusSubmit, async () => { this.toggleWalletStatus() }) + Doc.bind(page.toggleWalletStatusSubmit, 'click', async () => { this.toggleWalletStatus() }) Doc.bind(page.managePeers, 'click', async () => { this.showManagePeersForm() }) Doc.bind(page.addPeerSubmit, 'click', async () => { this.submitAddPeer() }) Doc.bind(page.unapproveTokenAllowance, 'click', async () => { this.showUnapproveTokenAllowanceTableForm() }) Doc.bind(page.unapproveTokenSubmit, 'click', async () => { this.submitUnapproveTokenAllowance() }) Doc.bind(page.showVSPs, 'click', () => { this.showVSPPicker() }) Doc.bind(page.vspDisplay, 'click', () => { this.showVSPPicker() }) - bindForm(page.vspPicker, page.customVspSubmit, async () => { this.setCustomVSP() }) + Doc.bind(page.customVspSubmit, 'click', async () => { this.setCustomVSP() }) Doc.bind(page.purchaseTicketsBttn, 'click', () => { this.showPurchaseTicketsDialog() }) - bindForm(page.purchaseTicketsForm, page.purchaserSubmit, () => { this.purchaseTickets() }) + Doc.bind(page.purchaserSubmit, 'click', () => { this.purchaseTickets() }) Doc.bind(page.purchaserInput, 'change', () => { this.purchaserInputChanged() }) Doc.bind(page.ticketHistory, 'click', () => { this.showTicketHistory() }) Doc.bind(page.ticketHistoryNextPage, 'click', () => { this.nextTicketPage() }) Doc.bind(page.ticketHistoryPrevPage, 'click', () => { this.prevTicketPage() }) Doc.bind(page.setVotes, 'click', () => { this.showSetVotesDialog() }) Doc.bind(page.purchaseTicketsErrCloser, 'click', () => { Doc.hide(page.purchaseTicketsErrBox) }) - Doc.bind(page.privacyInfoBttn, 'click', () => { this.showForm(page.mixingInfo) }) - - // New deposit address button. - this.depositAddrForm = new DepositAddress(page.deposit) - - // Clicking on the available amount on the Send form populates the - // amount field. + Doc.bind(page.privacyInfoBttn, 'click', () => { this.forms.show(page.mixingInfo) }) Doc.bind(page.walletBal, 'click', () => { this.populateMaxSend() }) // Display fiat value for current send amount. Doc.bind(page.sendAmt, 'input', () => { - const { unitInfo: ui } = app().assets[this.selectedAssetID] + const { unitInfo: ui } = app().assets[this.selectedWalletID] const amt = parseFloatDefault(page.sendAmt.value) const conversionFactor = ui.conventional.conversionFactor - Doc.showFiatValue(page.sendValue, amt * conversionFactor, app().fiatRatesMap[this.selectedAssetID], ui) + Doc.showFiatValue(page.sendValue, amt * conversionFactor, app().fiatRatesMap[this.selectedWalletID], ui) }) // Clicking on maxSend on the send form should populate the amount field. @@ -410,7 +509,7 @@ export default class WalletsPage extends BasePage { // Validate send address on input. Doc.bind(page.sendAddr, 'input', async () => { - const asset = app().assets[this.selectedAssetID] + const asset = app().assets[this.selectedWalletID] page.sendAddr.classList.remove('border-danger', 'border-success') const addr = page.sendAddr.value || '' if (!asset || addr === '') return @@ -434,25 +533,30 @@ export default class WalletsPage extends BasePage { Doc.show(page.changeWalletType, page.changeTypeHideIcon) Doc.hide(page.changeTypeShowIcon) page.changeTypeMsg.textContent = intl.prep(intl.ID_KEEP_WALLET_TYPE) - } else this.showReconfig(this.selectedAssetID, { skipAnimation: true }) + } else this.showReconfig(this.selectedWalletID, { skipAnimation: true }) }) app().registerNoteFeeder({ - fiatrateupdate: (note: RateNote) => { this.handleRatesNote(note) }, + fiatrateupdate: () => { this.handleRatesNote() }, balance: (note: BalanceNote) => { this.handleBalanceNote(note) }, walletstate: (note: WalletStateNote) => { this.handleWalletStateNote(note) }, walletconfig: (note: WalletStateNote) => { this.handleWalletStateNote(note) }, - walletsync: (note: WalletSyncNote) => { this.updateSyncAndPeers(note.assetID) }, + walletsync: (note: WalletSyncNote) => { + if (note.assetID === this.selectedWalletID) this.updateSyncAndPeers() + }, createwallet: (note: WalletCreationNote) => { this.handleCreateWalletNote(note) }, walletnote: (note: WalletNote) => { this.handleCustomWalletNote(note) } }) this.prepareTickerAssets() - const firstAsset = this.sortAssetButtons() - let selectedAsset = firstAsset.id - const assetIDStr = State.fetchLocal(State.selectedAssetLK) - if (assetIDStr) selectedAsset = Number(assetIDStr) - this.setSelectedAsset(selectedAsset) + this.setTickerButtons() + this.refreshBalances() + this.updateGlobalBalance() + // let selectedAsset = firstAsset.id + let lastTicker = State.fetchLocal(State.selectedAssetLK) + if (!lastTicker || !this.tickerMap[lastTicker]) lastTicker = 'DCR' + // if (assetIDStr) selectedAsset = Number(assetIDStr) + this.start(lastTicker) setInterval(() => { for (const row of this.page.txHistoryTableBody.children) { @@ -462,16 +566,17 @@ export default class WalletsPage extends BasePage { }, 5000) } - closePopups () { - Doc.hide(this.page.forms) - this.currTx = undefined - if (this.animation) this.animation.stop() + async start (firstTicker: string) { + await this.setSelectedTicker(firstTicker) + this.page.walletDetailsBox.classList.remove('invisible') + this.page.assetSelect.classList.remove('invisible') + this.page.secondColumn.classList.remove('invisible') } async safePost (path: string, args: any): Promise { - const assetID = this.selectedAssetID + const assetID = this.selectedWalletID const res = await postJSON(path, args) - if (assetID !== this.selectedAssetID) throw Error('asset changed during request. aborting') + if (assetID !== this.selectedWalletID) throw Error('asset changed during request. aborting') return res } @@ -561,14 +666,14 @@ export default class WalletsPage extends BasePage { } } Doc.hide(page.sendForm) - await this.showForm(page.vSendForm) + await this.forms.show(page.vSendForm) } // cancelSend displays the send form if user wants to make modification. async cancelSend () { const page = this.page Doc.hide(page.vSendForm, page.sendErr) - await this.showForm(page.sendForm) + await this.forms.show(page.sendForm) } /* @@ -600,7 +705,7 @@ export default class WalletsPage extends BasePage { * currently selected asset to the DEXes that use that version. */ assetVersionUsedByDEXes (): Record { - const assetID = this.selectedAssetID + const assetID = this.selectedWalletID const versionToDEXes = {} as Record const exchanges = app().exchanges @@ -625,7 +730,7 @@ export default class WalletsPage extends BasePage { const page = this.page const path = '/api/unapprovetoken' const res = await postJSON(path, { - assetID: this.selectedAssetID, + assetID: this.selectedWalletID, version: this.unapprovingTokenVersion }) if (!app().checkResponse(res)) { @@ -634,7 +739,7 @@ export default class WalletsPage extends BasePage { return } - const assetExplorer = CoinExplorers[this.selectedAssetID] + const assetExplorer = CoinExplorers[this.selectedWalletID] if (assetExplorer && assetExplorer[net]) { page.unapproveTokenTxID.href = assetExplorer[net](res.txID) } @@ -652,7 +757,7 @@ export default class WalletsPage extends BasePage { this.unapprovingTokenVersion = version Doc.show(page.unapproveTokenSubmissionElements) Doc.hide(page.unapproveTokenTxMsg, page.unapproveTokenErr) - const asset = app().assets[this.selectedAssetID] + const asset = app().assets[this.selectedWalletID] if (!asset || !asset.token) return const parentAsset = app().assets[asset.token.parentID] if (!parentAsset) return @@ -662,7 +767,7 @@ export default class WalletsPage extends BasePage { const path = '/api/approvetokenfee' const res = await postJSON(path, { - assetID: this.selectedAssetID, + assetID: this.selectedWalletID, version: version, approving: false }) @@ -677,7 +782,7 @@ export default class WalletsPage extends BasePage { } page.unapprovalFeeEstimate.textContent = feeText } - this.showForm(page.unapproveTokenForm) + this.forms.show(page.unapproveTokenForm) } /* @@ -687,7 +792,7 @@ export default class WalletsPage extends BasePage { */ async showUnapproveTokenAllowanceTableForm () { const page = this.page - const asset = app().assets[this.selectedAssetID] + const asset = app().assets[this.selectedWalletID] if (!asset || !asset.wallet || !asset.wallet.approved) return while (page.tokenVersionBody.firstChild) { page.tokenVersionBody.removeChild(page.tokenVersionBody.firstChild) @@ -718,7 +823,7 @@ export default class WalletsPage extends BasePage { } Doc.setVis(showTable, page.tokenVersionTable) Doc.setVis(!showTable, page.tokenVersionNone) - this.showForm(page.unapproveTokenTableForm) + this.forms.show(page.unapproveTokenTableForm) } /* @@ -731,7 +836,7 @@ export default class WalletsPage extends BasePage { Doc.hide(page.peerSpinner) const res = await postJSON('/api/getwalletpeers', { - assetID: this.selectedAssetID + assetID: this.selectedWalletID }) if (!app().checkResponse(res)) { page.managePeersErr.textContent = res.msg @@ -783,7 +888,7 @@ export default class WalletsPage extends BasePage { Doc.bind(removeIcon, 'click', async () => { Doc.hide(page.managePeersErr) const res = await postJSON('/api/removewalletpeer', { - assetID: this.selectedAssetID, + assetID: this.selectedWalletID, addr: peer.addr }) if (!app().checkResponse(res)) { @@ -805,7 +910,7 @@ export default class WalletsPage extends BasePage { const page = this.page await this.updateWalletPeersTable() Doc.hide(page.managePeersErr) - this.showForm(page.managePeersForm) + this.forms.show(page.managePeersForm) } // submitAddPeers sends a request for the the wallet to connect to a new @@ -814,7 +919,7 @@ export default class WalletsPage extends BasePage { const page = this.page Doc.hide(page.managePeersErr) const res = await postJSON('/api/addwalletpeer', { - assetID: this.selectedAssetID, + assetID: this.selectedWalletID, addr: page.addPeerInput.value }) if (!app().checkResponse(res)) { @@ -850,7 +955,7 @@ export default class WalletsPage extends BasePage { Doc.hide(page.toggleWalletStatusErr, page.walletStatusDisable, page.disableWalletMsg, page.walletStatusEnable, page.enableWalletMsg) if (disable) Doc.show(page.walletStatusDisable, page.disableWalletMsg) else Doc.show(page.walletStatusEnable, page.enableWalletMsg) - this.showForm(page.toggleWalletStatusConfirm) + this.forms.show(page.toggleWalletStatusConfirm) } /* @@ -860,11 +965,11 @@ export default class WalletsPage extends BasePage { const page = this.page Doc.hide(page.toggleWalletStatusErr) - const asset = app().assets[this.selectedAssetID] + const asset = app().assets[this.selectedWalletID] const disable = !asset.wallet.disabled const url = '/api/togglewalletstatus' const req = { - assetID: this.selectedAssetID, + assetID: this.selectedWalletID, disable: disable } @@ -881,7 +986,7 @@ export default class WalletsPage extends BasePage { let successMsg = intl.prep(intl.ID_WALLET_DISABLED_MSG, fmtParams) if (!disable) successMsg = intl.prep(intl.ID_WALLET_ENABLED_MSG, fmtParams) - this.assetUpdated(this.selectedAssetID, page.toggleWalletStatusConfirm, successMsg) + this.assetUpdated(this.selectedWalletID, page.toggleWalletStatusConfirm, successMsg) } /* @@ -898,85 +1003,42 @@ export default class WalletsPage extends BasePage { this.displayed = box } - /* showForm shows a modal form with a little animation. */ - async showForm (form: PageElement) { - const page = this.page - this.currentForm = form - this.forms.forEach(form => Doc.hide(form)) - form.style.right = '10000px' - Doc.show(page.forms, form) - const shift = (page.forms.offsetWidth + form.offsetWidth) / 2 - await Doc.animate(animationLength, progress => { - form.style.right = `${(1 - progress) * shift}px` - }, 'easeOutHard') - form.style.right = '0' - } - - async showSuccess (msg: string) { - this.forms.forEach(form => Doc.hide(form)) - this.currentForm = this.page.checkmarkForm - this.animation = showSuccess(this.page, msg) - await this.animation.wait() - this.animation = new Animation(1500, () => { /* pass */ }, '', () => { - if (this.currentForm === this.page.checkmarkForm) this.closePopups() - }) - } - /* Show the new wallet form. */ async showNewWallet (assetID: number) { const page = this.page const box = page.newWalletForm this.newWalletForm.setAsset(assetID) const defaultsLoaded = this.newWalletForm.loadDefaults() - await this.showForm(box) + await this.forms.show(box) await defaultsLoaded } prepareTickerAssets () { - const fiats = app().fiatRatesMap - let [haveAllFiatRates, totalUSD] = [true, 0] - const tickers: TickerAsset[] = [] - const getTickerAsset = (a: SupportedAsset, xcRate: number): TickerAsset => { - const { symbol, token, unitInfo: { conventional: {conversionFactor: cFactor, unit: ticker } } } = a - const normedTicker = ticker === 'WETH' ? 'ETH' : ticker === 'WBTC' ? 'BTC' : ticker - const i = tickers.findIndex((ta: TickerAsset) => ta.ticker === ticker, tickers) - if (i === -1) { - const ta: TickerAsset = { - ticker: normedTicker, cFactor, assets: [], lookup: {}, hasTokens: false, - logoSymbol: symbol, bal: { avail: 0, immature: 0, locked: 0, total: 0 }, xcRate - } - tickers.push(ta) - return ta + const tickerList: TickerAsset[] = [] + const tickerMap: Record = {} + + for (const a of Object.values(app().user.assets)) { + const normedTicker = normalizedTicker(a) + let ta = tickerMap[normedTicker] + if (ta) { + ta.addChainAsset(a) + continue } - const ta = tickers[i] - if (!token) ta.logoSymbol = symbol // Prefer the native asset symbol for e.g, eth/weth - return tickers[i] - } - - for (const [assetID, a] of Object.entries(app().user.assets)) { - const { symbol, wallet: w, info: token, unitInfo: { conventional: { conversionFactor, unit: ticker } } } = a - const xcRate = fiats[Number(assetID)] - const ta = getTickerAsset(a, xcRate) - if (!w) continue - const { balance: { available, locked, immature } } = w - const totalBal = available + locked + immature - if (xcRate) totalUSD += totalBal / conversionFactor * xcRate - else haveAllFiatRates = false - if (!ta.logoSymbol || !token) ta.logoSymbol = symbol - ta.hasTokens = ta.hasTokens && Boolean(token) - ta.bal.avail += available - ta.bal.locked += locked - ta.bal.immature += immature - ta.bal.total += available + locked + immature - const state: AssetState = { symbol, ticker, bal: w.balance, xcRate } - ta.assets.push(state) - ta.lookup[Number(assetID)] = state - } - tickers.sort((a: TickerAsset, b: TickerAsset) => { - if (a.assets.length && !b.assets.length) return -1 - if (!a.assets.length && b.assets.length) return 1 - if (!a.assets.length && !b.assets.length) return a.ticker === 'DCR' ? -1 : 1 - const [aTotal, bTotal] = [a.bal.total, b.bal.total] + ta = new TickerAsset(a) + tickerList.push(ta) + tickerMap[normedTicker] = ta + } + this.tickerList = tickerList + this.tickerMap = tickerMap + } + + sortTickers () { + const { page, tickerList, tickerButtons } = this + tickerList.sort((a: TickerAsset, b: TickerAsset) => { + if (a.hasWallets && !b.hasWallets) return -1 + if (!a.hasWallets && b.hasWallets) return 1 + if (!a.hasWallets && !b.hasWallets) return a.ticker === 'DCR' ? -1 : 1 + const [aTotal, bTotal] = [a.total, b.total] if (aTotal === 0 && bTotal === 0) return a.ticker.localeCompare(b.ticker) else if (aTotal === 0) return 1 else if (aTotal === 0) return -1 @@ -985,227 +1047,290 @@ export default class WalletsPage extends BasePage { if (!aFiat && bFiat) return 1 return bFiat * bTotal - aFiat * aTotal }) - this.totalUSD = totalUSD - this.haveAllFiatRates = haveAllFiatRates - this.tickers = tickers - } - - // sortAssetButtons displays supported assets, sorted. Returns first asset in the - // list. - sortAssetButtons (): SupportedAsset { - const { page, tickers, totalUSD } = this - // const fiats = app().fiatRatesMap - // this.assetButtons = {} - // Global balance always goes first. + Doc.empty(page.tickerBalsBox) + for (const { ticker } of tickerList) page.tickerBalsBox.appendChild(tickerButtons[ticker]) + } + + refreshBalances () { + const { balTracker, tickerList, tickerTemplates, updateTickerButtonTemplate } = this + for (const ta of tickerList) { + const { ticker, total, xcRate, cFactor } = ta + balTracker[ticker] = total / cFactor * xcRate + updateTickerButtonTemplate(ta, tickerTemplates[ticker]) + } + } + + setTickerButtons () { + const { page, tickerList } = this Doc.empty(page.assetSelect) page.assetSelect.appendChild(page.globalBalanceBox) page.assetSelect.appendChild(page.tickerBalsBox) Doc.empty(page.tickerBalsBox) - for (const { ticker, bal, cFactor, xcRate, logoSymbol, assets } of tickers) { + for (const ta of tickerList) { + const { ticker, logoSymbol } = ta const div = page.tickerBalTmpl.cloneNode(true) as PageElement - page.tickerBalsBox.appendChild(div) + this.tickerButtons[ticker] = div + Doc.bind(div, 'click', () => this.setSelectedTicker(ticker)) const tmpl = Doc.parseTemplate(div) + this.tickerTemplates[ticker] = tmpl tmpl.logo.src = Doc.logoPath(logoSymbol) tmpl.ticker.textContent = ticker - if (assets.length) { - tmpl.bal.textContent = Doc.formatFourSigFigs(bal.total / cFactor) - tmpl.fiatBal.textContent = Doc.formatFourSigFigs(bal.total / cFactor * xcRate, 2) - } else { - Doc.hide(tmpl.fiatBox) - tmpl.logo.classList.add('greyscale', 'faded') - tmpl.ticker.classList.add('grey') + } + this.sortTickers() + } + + updateTickerButtonTemplate (ta: TickerAsset, tmpl: Record) { + const { total, cFactor, hasWallets, xcRate } = ta + Doc.setVis(hasWallets && xcRate, tmpl.fiatBox) + if (hasWallets) { + tmpl.bal.textContent = Doc.formatFourSigFigs(total / cFactor) + tmpl.fiatBal.textContent = Doc.formatFourSigFigs(total / cFactor * xcRate, 2) + tmpl.logo.classList.remove('greyscale', 'faded') + tmpl.ticker.classList.remove('grey') + } else { + tmpl.logo.classList.add('greyscale', 'faded') + tmpl.ticker.classList.add('grey') + } + } + + updateGlobalBalance () { + const totalUSD = Object.values(this.balTracker).reduce((total, fiatBal) => total + fiatBal, 0) + this.page.globalBalance.textContent = Doc.formatFourSigFigs(totalUSD, 2) + } + + updateAssetBalance (assetID: number) { + const ticker = normalizedTicker(app().assets[assetID]) + const ta = this.tickerMap[ticker] + const { total, xcRate, cFactor } = ta + this.balTracker[ticker] = total / cFactor * xcRate + this.updateTickerButtonTemplate(ta, this.tickerTemplates[ticker]) + this.updateGlobalBalance() + } + + async setSelectedTicker (ticker: string) { + const ta = this.selectedTicker = this.tickerMap[ticker] + this.selectedWalletID = ta.blockchainWallet()?.assetID ?? -1 + const { page } = this + const { logoSymbol, name, isMultiNet, hasTokens } = ta + Doc.setText(page.walletDetailsBox, '[data-ticker]', ticker) + Doc.setText(page.walletDetailsBox, '[data-asset-name]', name) + Doc.setSrc(page.walletDetailsBox, '[data-logo]', Doc.logoPath(logoSymbol)) + page.walletDetailsBox.classList.toggle('multinet', isMultiNet) + page.walletDetailsBox.classList.toggle('token', hasTokens) + for (const div of Array.from(page.docs.children) as PageElement[]) Doc.setVis(div.dataset.docTicker === ticker, div) + this.updateDisplayedTicker() + + // const { assetSelect } = this.page + // for (const b of assetSelect.children) b.classList.remove('selected') + // // this.assetButtons[assetID].bttn.classList.add('selected') + // this.selectedWalletID = assetID + // this.page.hideMixTxsCheckbox.checked = true + // this.updateDisplayedAsset(assetID) + this.showAvailableMarkets() + + // const a = this.showRecentActivity(assetID) + // const b = this.showTxHistory(assetID) + for (const p of [this.updateTicketBuyer(), this.updatePrivacy(), State.storeLocal(State.selectedAssetLK, ticker)]) await p + } + + updateDisplayedTicker () { + const { page, selectedTicker: ta } = this + const chainWallet = ta.blockchainWallet() + Doc.setVis(chainWallet && !chainWallet.wallet, page.createWalletBox) + Doc.setVis(ta.hasWallets, page.sendReceiveBox) + const w = chainWallet?.wallet + Doc.setVis(w, page.walletConfig) + + if (w) { + Doc.show(page.walletConfig) + page.blockchainClass.textContent = w.class + const walletDef = app().walletDefinition(w.assetID, w.type) + page.walletType.textContent = walletDef.tab + this.updateSyncAndPeers() + } + + this.updateDisplayedTickerBalance() + this.updateFeeState() + } + + updateDisplayedTickerBalance (): void { + const { page, selectedTicker: ta, balanceDetails: { balance, fiatBalance, fiatBalanceBox } } = this + const { ui, total, cFactor, xcRate } = ta + balance.textContent = Doc.formatFourSigFigs(total / cFactor) + Doc.setVis(xcRate, fiatBalanceBox) + if (xcRate) fiatBalance.textContent = Doc.formatFourSigFigs(total / cFactor * xcRate, 2) + const chainWallet = ta.blockchainWallet() + // Only show balance breakdown if this is multi-chain or if this is unichain + // and has a wallet + const showBalanceBreakdown = Boolean(chainWallet?.wallet) || ta.isMultiNet + Doc.setVis(showBalanceBreakdown, page.balanceBreakdownBox) + Doc.setVis(total > 0, page.send) + if (!showBalanceBreakdown) return + + Doc.empty(page.balanceBreakdown) + for (const { assetID, chainName, chainLogo, bal: { available, locked, immature }, token } of ta.chainAssets) { + const { wallet: w } = app().assets[assetID] + const tr = Doc.clone(page.blockchainBalanceTmpl) + page.balanceBreakdown.appendChild(tr) + const tmpl = Doc.parseTemplate(tr) + tmpl.chainLogo.src = chainLogo + tmpl.chainName.textContent = chainName + const usable = w || token?.parentMade + if (usable) { + if (immature > 0) Doc.formatCoinValue((immature), ui) + if (locked > 0) Doc.formatCoinValue((locked), ui) + tmpl.avail.textContent = Doc.formatCoinValue(available, ui) + tmpl.allocation.textContent = String(total ? Math.round((available + locked + immature) / total * 100) : 0) + '%' } + Doc.bind(tmpl.txsBttn, 'click', () => this.showTxHistory(assetID)) + Doc.bind(tmpl.createWalletBttn, 'click', () => this.showNewWallet(token?.parentID ?? assetID)) + + Doc.setVis(usable, tmpl.txsBttn) + Doc.setVis(!usable, tmpl.createWalletBttn) } - page.globalBalance.textContent = Doc.formatFourSigFigs(totalUSD, 2) - - - // const sortedAssets = [...Object.values(app().assets)] - // sortedAssets.sort((a: SupportedAsset, b: SupportedAsset) => { - // if (a.wallet && !b.wallet) return -1 - // if (!a.wallet && b.wallet) return 1 - // if (!a.wallet && !b.wallet) return a.symbol === 'dcr' ? -1 : 1 - // const [aBal, bBal] = [a.wallet.balance, b.wallet.balance] - // const [aTotal, bTotal] = [aBal.available + aBal.immature + aBal.locked, bBal.available + bBal.immature + bBal.locked] - // if (aTotal === 0 && bTotal === 0) return a.symbol.localeCompare(b.symbol) - // else if (aTotal === 0) return 1 - // else if (aTotal === 0) return -1 - // const [aFiat, bFiat] = [fiats[a.id], fiats[b.id]] - // if (aFiat && !bFiat) return -1 - // if (!aFiat && bFiat) return 1 - // return bFiat * bTotal - aFiat * aTotal - // }) - // for (const a of sortedAssets) { - // const bttn = page.iconSelectTmpl.cloneNode(true) as HTMLElement - // page.assetSelect.appendChild(bttn) - // const tmpl = Doc.parseTemplate(bttn) - // this.assetButtons[a.id] = { tmpl, bttn } - // this.updateAssetButton(a.id) - // Doc.bind(bttn, 'click', () => { - // this.setSelectedAsset(a.id) - // State.storeLocal(State.selectedAssetLK, String(a.id)) - // }) - // } - // page.assetSelect.classList.remove('invisible') - // return sortedAssets[0] - - - } - - updateAssetButton (assetID: number) { - const a = app().assets[assetID] - const { bttn, tmpl } = this.assetButtons[assetID] - Doc.hide(tmpl.fiatBox, tmpl.noWallet) - bttn.classList.add('nowallet') - tmpl.img.src ||= Doc.logoPath(a.symbol) // don't initiate GET if already set (e.g. update on some notification) - const symbolParts = a.symbol.split('.') - if (symbolParts.length === 2) { - const parentSymbol = symbolParts[1] - tmpl.parentImg.classList.remove('d-hide') - tmpl.parentImg.src ||= Doc.logoPath(parentSymbol) - } - if (this.selectedAssetID === assetID) bttn.classList.add('selected') - tmpl.name.textContent = a.name - if (a.wallet) { - bttn.classList.remove('nowallet') - const { wallet: { balance: b }, unitInfo: ui } = a - const totalBalance = b.available + b.locked + b.immature - const [s, unit] = Doc.formatBestUnitsFourSigFigs(totalBalance, ui) - tmpl.balance.textContent = s - tmpl.unit.textContent = unit - Doc.show(tmpl.balanceBox) - const fiatRate = app().fiatRatesMap[a.id] - if (fiatRate) { - Doc.show(tmpl.fiatBox) - tmpl.fiat.textContent = Doc.formatFourSigFigs(totalBalance / ui.conventional.conversionFactor * fiatRate) + + + // TODO: handle reserves deficit with a notification. + // if (bal.reservesDeficit > 0) addPrimaryBalance(intl.prep(intl.ID_RESERVES_DEFICIT), bal.reservesDeficit, intl.prep(intl.ID_RESERVES_DEFICIT_MSG)) + + // page.purchaserBal.textContent = Doc.formatFourSigFigs(bal.available / ui.conventional.conversionFactor) + // app().bindTooltips(page.balanceDetailBox) + } + + updateSyncAndPeers () { + const { page, selectedWalletID: assetID } = this + const w = app().walletMap[assetID] + const { peerCount, syncProgress, syncStatus, encrypted, open: unlocked, running, disabled } = w + + Doc.hide(page.txSyncBox, page.txFindingAddrs, page.txProgress) + if (running) { + page.peerCount.textContent = String(peerCount) + page.syncProgress.textContent = `${(syncProgress * 100).toFixed(1)}%` + page.syncHeight.textContent = String(syncStatus.blocks) + if (syncStatus.txs !== undefined) { + Doc.show(page.txSyncBox) + if (syncStatus.txs === 0 && syncStatus.blocks >= syncStatus.targetHeight) Doc.show(page.txFindingAddrs) + else { + Doc.show(page.txProgress) + const prog = syncStatus.txs / syncStatus.targetHeight + page.txProgress.textContent = `${(prog * 100).toFixed(1)}%` + } } - } else Doc.show(tmpl.noWallet) - } - - async setSelectedAsset (assetID: number) { - const { assetSelect } = this.page - for (const b of assetSelect.children) b.classList.remove('selected') - this.assetButtons[assetID].bttn.classList.add('selected') - this.selectedAssetID = assetID - this.page.hideMixTxsCheckbox.checked = true - this.updateDisplayedAsset(assetID) - this.showAvailableMarkets(assetID) - const a = this.showRecentActivity(assetID) - const b = this.showTxHistory(assetID) - const c = this.updateTicketBuyer(assetID) - const d = this.updatePrivacy(assetID) - for (const p of [a, b, c, d]) await p - } - - updateDisplayedAsset (assetID: number) { - if (assetID !== this.selectedAssetID) return - const { symbol, wallet, name, token, unitInfo } = app().assets[assetID] - const { page, body } = this - Doc.setText(body, '[data-asset-name]', name) - Doc.setText(body, '[data-ticker]', unitInfo.conventional.unit) - page.assetLogo.src = Doc.logoPath(symbol) + } else { + page.peerCount.textContent = '—' + page.syncProgress.textContent = '—' + page.syncHeight.textContent = '—' + } + Doc.hide( - page.balanceBox, page.fiatBalanceBox, page.createWallet, page.walletDetails, - page.sendReceive, page.connectBttnBox, page.statusLocked, page.statusReady, - page.statusOff, page.unlockBttnBox, page.lockBttnBox, page.connectBttnBox, - page.peerCountBox, page.syncProgressBox, page.statusDisabled, page.tokenInfoBox, - page.needsProviderBox, page.feeStateBox, page.txSyncBox, page.txProgress, - page.txFindingAddrs + page.statusReady, page.statusLocked, page.statusOff, page.statusDisabled, page.statusSyncing, + page.connectBttn, page.lockBttn, page.unlockBttn ) - this.checkNeedsProvider(assetID) - if (token) { - const parentAsset = app().assets[token.parentID] - page.tokenParentLogo.src = Doc.logoPath(parentAsset.symbol) - page.tokenParentName.textContent = parentAsset.name - Doc.show(page.tokenInfoBox) - } - if (wallet) { - this.updateDisplayedAssetBalance() - const { feeState, running, disabled, type: walletType } = wallet - const walletDef = app().walletDefinition(assetID, walletType) - page.walletType.textContent = walletDef.tab - if (feeState) this.updateFeeState(feeState) - if (disabled) Doc.show(page.statusDisabled) // wallet is disabled - else if (running) { - this.updateSyncAndPeers(wallet.assetID) - } else Doc.show(page.statusOff, page.connectBttnBox) // wallet not running - } else Doc.show(page.createWallet) // no wallet - - page.walletDetailsBox.classList.remove('invisible') - } - - updateSyncAndPeers (assetID: number) { - const { page, selectedAssetID } = this - if (assetID !== selectedAssetID) return - const { peerCount, syncProgress, syncStatus, encrypted, open, running } = app().walletMap[assetID] - if (!running) return - Doc.show(page.sendReceive, page.peerCountBox, page.syncProgressBox) - page.peerCount.textContent = String(peerCount) - page.syncProgress.textContent = `${(syncProgress * 100).toFixed(1)}%` - if (open) { - Doc.show(page.statusReady) - if (!app().haveActiveOrders(assetID) && encrypted) Doc.show(page.lockBttnBox) - } else Doc.show(page.statusLocked, page.unlockBttnBox) // wallet not unlocked - Doc.setVis(syncStatus.txs !== undefined, page.txSyncBox) - if (syncStatus.txs !== undefined) { - Doc.hide(page.txProgress, page.txFindingAddrs) - if (syncStatus.txs === 0 && syncStatus.blocks >= syncStatus.targetHeight) Doc.show(page.txFindingAddrs) - else { - Doc.show(page.txProgress) - const prog = syncStatus.txs / syncStatus.targetHeight - page.txProgress.textContent = `${(prog * 100).toFixed(1)}%` + if (disabled) return Doc.show(page.statusDisabled) + if (!running) return Doc.show(page.connectBttn, page.statusLocked) + const syncing = syncProgress < 1 || syncStatus.txs !== undefined + if (syncing) return Doc.show(page.statusSyncing) + Doc.show(page.statusReady) + const hasActiveOrders = app().haveActiveOrders(assetID) + const lockable = unlocked && encrypted && !hasActiveOrders + const unlockable = encrypted && !unlocked + Doc.setVis(unlockable, page.unlockBttn) + Doc.setVis(lockable, page.lockBttn) + if (unlockable) Doc.show(page.unlockBttn) + else if (lockable) Doc.show(page.lockBttn) + } + + updateFeeState () { + const { page, selectedTicker: ta } = this + const { ui, xcRate, chainAssets } = ta + + page.feeStateXcRate.textContent = Doc.formatFourSigFigs(xcRate) + + const formatUSD = (el: PageElement, v: number, feeUI: UnitInfo, feeFiatRate: number) => { + const tmpl = Doc.parseTemplate(el) + const fv = v / feeUI.conventional.conversionFactor * feeFiatRate + Doc.setVis(fv <= 0.001, tmpl.lessThan) + tmpl.value.textContent = Doc.formatFourSigFigs(Math.max(fv, 0.001), fv >= 0.1 ? 2 : 3) + } + + const feeAssetStuff = (ca: ChainAsset): [number, UnitInfo, number] => { + const { assetID, token } = ca + const feeAssetID = token ? token.parentID : assetID + const feeUI = token?.feeUI ?? ui + const feeFiatRate = app().fiatRatesMap[feeAssetID] + return [feeAssetID, feeUI, feeFiatRate] + } + + if (ta.isMultiNet) { + Doc.empty(page.netTxFees) + for (const ca of chainAssets) { + const { assetID, chainName, chainLogo } = ca + const [feeAssetID, feeUI, feeFiatRate] = feeAssetStuff(ca) + const tr = Doc.clone(page.netTxFeeTmpl) + page.netTxFees.appendChild(tr) + const tmpl = Doc.parseTemplate(tr) + tmpl.chainLogo.src = chainLogo + tmpl.chainName.textContent = chainName + const w = app().walletMap[assetID] + if (!w?.feeState) continue + // remove dummies + for (const dummy of Array.from(tr.children).slice(1)) tr.removeChild(dummy) + const { send, swap, redeem, rate } = w.feeState + + const addTD = (v: number) => { + const td = Doc.clone(page.multiNetTxFeeTmpl) + tr.appendChild(td) + const tdTmpl = Doc.parseTemplate(td) + Doc.formatBestValueElement(tdTmpl.chainUnits, feeAssetID, v, feeUI) + formatUSD(tdTmpl.fiatUnits, v, feeUI, feeFiatRate) + } + + addTD(send) + addTD(redeem) // buy + addTD(swap) // sell + // Rate + const td = Doc.clone(page.multiNetFeeRateTmpl) + tr.appendChild(td) + Doc.formatBestRateElement(td, feeAssetID, rate, feeUI) } + app().bindUnits(page.netTxFees) + } else { + const [feeAssetID, feeUI, feeFiatRate] = feeAssetStuff(chainAssets[0]) + const w = app().walletMap[feeAssetID] + if (!w?.feeState) { + Doc.hide(page.txFeesBox) + return + } + const { rate, send, swap, redeem } = w.feeState + Doc.formatBestRateElement(page.networkFeeRate, feeAssetID, rate, feeUI) + Doc.formatBestValueElement(page.feeStateSendFees, feeAssetID, send, feeUI) + Doc.formatBestValueElement(page.feeStateSellFees, feeAssetID, swap, feeUI) + Doc.formatBestValueElement(page.feeStateBuyFees, feeAssetID, redeem, feeUI) + formatUSD(page.feeStateSendFiat, send, feeUI, feeFiatRate) + formatUSD(page.feeStateSellFiat, swap, feeUI, feeFiatRate) + formatUSD(page.feeStateBuyFiat, redeem, feeUI, feeFiatRate) } + } - updateFeeState (feeState: FeeState) { - const { page, selectedAssetID: assetID } = this - Doc.hide(page.feeStateBox) - const { unitInfo: ui, token } = app().assets[assetID] - const fiatRate = app().fiatRatesMap[assetID] - if (!fiatRate) return - const feeAssetID = token ? token.parentID : assetID - const feeFiatRate = app().fiatRatesMap[feeAssetID] - if (token && !feeFiatRate) return - Doc.show(page.feeStateBox) - const feeUI = token ? app().assets[token.parentID].unitInfo : ui - Doc.formatBestRateElement(page.feeStateNetRate, feeAssetID, feeState.rate, feeUI) - Doc.formatBestValueElement(page.feeStateSendFees, feeAssetID, feeState.send, feeUI) - Doc.formatBestValueElement(page.feeStateSwapFees, feeAssetID, feeState.swap, feeUI) - Doc.formatBestValueElement(page.feeStateRedeemFees, feeAssetID, feeState.redeem, feeUI) - page.feeStateXcRate.textContent = Doc.formatFourSigFigs(fiatRate) - const sendFiat = feeState.send / feeUI.conventional.conversionFactor * feeFiatRate - page.feeStateSendFiat.textContent = Doc.formatFourSigFigs(sendFiat) - const swapFiat = feeState.swap / feeUI.conventional.conversionFactor * feeFiatRate - page.feeStateSwapFiat.textContent = Doc.formatFourSigFigs(swapFiat) - const redeemFiat = feeState.redeem / feeUI.conventional.conversionFactor * feeFiatRate - page.feeStateRedeemFiat.textContent = Doc.formatFourSigFigs(redeemFiat) - Doc.show(page.feeStateBox) - } - - async checkNeedsProvider (assetID: number) { - const needs = await app().needsCustomProvider(assetID) - const { page: { needsProviderBox: box, needsProviderBttn: bttn } } = this - Doc.setVis(needs, box) - if (!needs) return - Doc.blink(bttn) - } - - async updateTicketBuyer (assetID: number) { - this.ticketPage = { - number: 0, - history: [], - scanned: false - } + async updateTicketBuyer () { + const { page, selectedWalletID: assetID } = this + if (assetID === -1) return Doc.hide(page.stakingBox) const { wallet, unitInfo: ui } = app().assets[assetID] - const page = this.page Doc.hide( - page.stakingBox, page.pickVSP, page.stakingSummary, page.stakingErr, + page.pickVSP, page.stakingSummary, page.stakingErr, page.vspDisplayBox, page.ticketPriceBox, page.purchaseTicketsBox, page.stakingRpcSpvMsg, page.ticketsDisabled ) - if (!wallet?.running || (wallet.traits & traitTicketBuyer) === 0) return - Doc.show(page.stakingBox) + const showStakingBox = wallet?.running && Boolean(wallet.traits & traitTicketBuyer) + Doc.setVis(showStakingBox, page.stakingBox) + if (!showStakingBox) return + this.ticketPage = { + number: 0, + history: [], + scanned: false + } const loaded = app().loading(page.stakingBox) const res = await this.safePost('/api/stakestatus', assetID) loaded() @@ -1228,7 +1353,7 @@ export default class WalletsPage extends BasePage { page.purchaserBal.textContent = Doc.formatCoinValue(wallet.balance.available, ui) this.updateTicketStats(stakeStatus.stats, ui, stakeStatus.ticketPrice, stakeStatus.votingSubsidy) // If this is an extension wallet, we'll might to disable all controls. - const disableStaking = app().extensionWallet(this.selectedAssetID)?.disableStaking + const disableStaking = app().extensionWallet(this.selectedWalletID)?.disableStaking if (disableStaking) { Doc.hide(page.setVotes, page.showVSPs) Doc.show(page.ticketsDisabled) @@ -1272,9 +1397,9 @@ export default class WalletsPage extends BasePage { } async showVSPPicker () { - const assetID = this.selectedAssetID + const assetID = this.selectedWalletID const page = this.page - this.showForm(page.vspPicker) + this.forms.show(page.vspPicker) Doc.empty(page.vspPickerList) Doc.hide(page.stakingErr) const loaded = app().loading(page.vspPicker) @@ -1304,7 +1429,7 @@ export default class WalletsPage extends BasePage { const page = this.page page.purchaserInput.value = '' Doc.hide(page.purchaserErr) - this.showForm(this.page.purchaseTicketsForm) + this.forms.show(this.page.purchaseTicketsForm) page.purchaserInput.focus() } @@ -1319,7 +1444,7 @@ export default class WalletsPage extends BasePage { } async purchaseTickets () { - const { page, selectedAssetID: assetID } = this + const { page, selectedWalletID: assetID } = this // DRAFT NOTE: The user will get an actual ticket count somewhere in the // range 1 <= tickets_purchased <= n. See notes in // (*spvWallet).PurchaseTickets. @@ -1336,14 +1461,14 @@ export default class WalletsPage extends BasePage { Doc.show(page.purchaserErr) return } - this.showSuccess(intl.prep(intl.ID_TICKETS_PURCHASED, { n: n.toLocaleString(Doc.languages()) })) + this.forms.showSuccess(intl.prep(intl.ID_TICKETS_PURCHASED, { n: n.toLocaleString(Doc.languages()) })) } processTicketPurchaseUpdate (walletNote: CustomWalletNote) { - const { stakeStatus, selectedAssetID, page } = this + const { stakeStatus, selectedWalletID, page } = this const { assetID } = walletNote const { err, remaining, tickets, stats } = walletNote.payload as TicketPurchaseUpdate - if (assetID !== selectedAssetID) return + if (assetID !== selectedWalletID) return if (err) { Doc.show(page.purchaseTicketsErrBox) page.purchaseTicketsErr.textContent = err @@ -1358,7 +1483,7 @@ export default class WalletsPage extends BasePage { } async setVSP (assetID: number, vsp: VotingServiceProvider) { - this.closePopups() + this.forms.close() const page = this.page const loaded = app().loading(page.stakingBox) const res = await this.safePost('/api/setvsp', { assetID, url: vsp.url }) @@ -1372,7 +1497,7 @@ export default class WalletsPage extends BasePage { } setCustomVSP () { - const assetID = this.selectedAssetID + const assetID = this.selectedWalletID const vsp = { url: this.page.customVspUrl.value } as VotingServiceProvider this.setVSP(assetID, vsp) } @@ -1395,7 +1520,7 @@ export default class WalletsPage extends BasePage { } displayTicketPage (pageNumber: number, pageOfTickets: Ticket[]) { - const { page, selectedAssetID: assetID } = this + const { page, selectedWalletID: assetID } = this const ui = app().unitInfo(assetID) const coinLink = CoinExplorers[assetID][app().user.net] Doc.empty(page.ticketHistoryRows) @@ -1415,7 +1540,7 @@ export default class WalletsPage extends BasePage { } async ticketPageN (pageNumber: number) { - const { page, stakeStatus, ticketPage, selectedAssetID: assetID } = this + const { page, stakeStatus, ticketPage, selectedWalletID: assetID } = this const pageOfTickets = this.pageOfTickets(pageNumber) if (pageOfTickets.length < ticketPageSize && !ticketPage.scanned) { const n = ticketPageSize - pageOfTickets.length @@ -1454,7 +1579,7 @@ export default class WalletsPage extends BasePage { } async showTicketHistory () { - this.showForm(this.page.ticketHistoryForm) + this.forms.show(this.page.ticketHistoryForm) await this.ticketPageN(this.ticketPage.number) } @@ -1467,7 +1592,7 @@ export default class WalletsPage extends BasePage { } showSetVotesDialog () { - const { page, stakeStatus, selectedAssetID: assetID } = this + const { page, stakeStatus, selectedWalletID: assetID } = this const ui = app().unitInfo(assetID) Doc.hide(page.votingFormErr) const coinLink = CoinExplorers[assetID][app().user.net] @@ -1559,19 +1684,22 @@ export default class WalletsPage extends BasePage { tmpl.key.textContent = keyPolicy.key } - this.showForm(page.votingForm) + this.forms.show(page.votingForm) } - async updatePrivacy (assetID: number) { - const disablePrivacy = app().extensionWallet(assetID)?.disablePrivacy + async updatePrivacy () { + const { page, selectedWalletID: assetID } = this this.mixing = false - const { wallet } = app().assets[assetID] - const page = this.page - Doc.hide(page.mixingBox, page.mixerOff, page.mixerOn) + if (assetID === -1) return Doc.hide(page.mixingBox) + const disablePrivacy = app().extensionWallet(assetID)?.disablePrivacy + const { wallet: w } = app().assets[assetID] + const showMixingBox = !disablePrivacy && w?.running && Boolean(w.traits & traitFundsMixer) + Doc.setVis(showMixingBox, page.mixingBox) + if (!showMixingBox) return + Doc.hide(page.mixerOff, page.mixerOn) // TODO: Show special messaging if the asset supports mixing but not this // wallet type. - if (disablePrivacy || !wallet?.running || (wallet.traits & traitFundsMixer) === 0) return - Doc.show(page.mixingBox, page.mixerLoading) + Doc.show(page.mixerLoading) const res = await this.safePost('/api/mixingstats', { assetID }) Doc.hide(page.mixerLoading) if (!app().checkResponse(res)) { @@ -1590,7 +1718,7 @@ export default class WalletsPage extends BasePage { const page = this.page Doc.hide(page.mixingErr) const loaded = app().loading(page.mixingBox) - const res = await postJSON('/api/configuremixer', { assetID: this.selectedAssetID, enabled: on }) + const res = await postJSON('/api/configuremixer', { assetID: this.selectedWalletID, enabled: on }) loaded() if (!app().checkResponse(res)) { page.mixingErr.textContent = intl.prep(intl.ID_API_ERROR, { msg: res.msg }) @@ -1602,87 +1730,87 @@ export default class WalletsPage extends BasePage { this.mixerToggle.setState(on) } - updateDisplayedAssetBalance (): void { - const page = this.page - const asset = app().assets[this.selectedAssetID] - const { wallet, unitInfo: ui, id: assetID } = asset - const bal = wallet.balance - Doc.show(page.balanceBox, page.walletDetails) - const totalLocked = bal.locked + bal.contractlocked + bal.bondlocked - const totalBalance = bal.available + totalLocked + bal.immature - page.balance.textContent = Doc.formatCoinValue(totalBalance, ui) - page.balanceUnit.textContent = ui.conventional.unit - const rate = app().fiatRatesMap[assetID] - if (rate) { - Doc.show(page.fiatBalanceBox) - page.fiatBalance.textContent = Doc.formatFiatConversion(totalBalance, rate, ui) - } - Doc.empty(page.balanceDetailBox) - - const addBalanceRow = (cat: string, bal: number, tooltipMsg?: string) => { - const row = page.balanceDetailRow.cloneNode(true) as PageElement - page.balanceDetailBox.appendChild(row) - const tmpl = Doc.parseTemplate(row) - tmpl.name.textContent = cat - if (tooltipMsg) { - tmpl.tooltipMsg.dataset.tooltip = tooltipMsg - Doc.show(tmpl.tooltipMsg) - } - tmpl.balance.textContent = Doc.formatCoinValue(bal, ui) - return row - } - - let lastSubLockedRow: PageElement | undefined - let lastPrimaryRow: PageElement | undefined - const addPrimaryBalance = (cat: string, bal: number, tooltipMsg?: string) => { - lastSubLockedRow = undefined - lastPrimaryRow = addBalanceRow(cat, bal, tooltipMsg) - } - const addSubBalance = (cat: string, bal: number, tooltipMsg?: string) => { - lastSubLockedRow = addBalanceRow(cat, bal, tooltipMsg) - lastSubLockedRow.classList.add('sub') - } - const setRowClasses = () => { - if (!lastSubLockedRow) return - (lastPrimaryRow as PageElement).classList.add('itemized') - lastSubLockedRow.classList.add('last') - } - - addPrimaryBalance(intl.prep(intl.ID_AVAILABLE_TITLE), bal.available, '') - if (bal.other?.Shielded !== undefined) { - const transparent = bal.available - bal.other.Shielded.amt - addSubBalance(intl.prep(intl.ID_TRANSPARENT), transparent) - addSubBalance(intl.prep(intl.ID_SHIELDED), bal.other.Shielded.amt) - } - setRowClasses() - - addPrimaryBalance(intl.prep(intl.ID_LOCKED_TITLE), totalLocked, intl.prep(intl.ID_LOCKED_BAL_MSG)) - if (bal.orderlocked > 0) addSubBalance(intl.prep(intl.ID_ORDER), bal.orderlocked, intl.prep(intl.ID_LOCKED_ORDER_BAL_MSG)) - if (bal.contractlocked > 0) addSubBalance(intl.prep(intl.ID_SWAPPING), bal.contractlocked, intl.prep(intl.ID_LOCKED_SWAPPING_BAL_MSG)) - if (bal.bondlocked > 0) addSubBalance(intl.prep(intl.ID_BONDED), bal.bondlocked, intl.prep(intl.ID_LOCKED_BOND_BAL_MSG)) - if (bal.bondReserves > 0) addSubBalance(intl.prep(intl.ID_BOND_RESERVES), bal.bondReserves, intl.prep(intl.ID_BOND_RESERVES_MSG)) - if (bal?.other?.Staked !== undefined) addSubBalance('Staked', bal.other.Staked.amt) - setRowClasses() - - if (bal.immature) addPrimaryBalance(intl.prep(intl.ID_IMMATURE_TITLE), bal.immature, intl.prep(intl.ID_IMMATURE_BAL_MSG)) - if (bal?.other?.Unmixed !== undefined) addSubBalance('Unmixed', bal.other.Unmixed.amt) - setRowClasses() - - // TODO: handle reserves deficit with a notification. - // if (bal.reservesDeficit > 0) addPrimaryBalance(intl.prep(intl.ID_RESERVES_DEFICIT), bal.reservesDeficit, intl.prep(intl.ID_RESERVES_DEFICIT_MSG)) - - page.purchaserBal.textContent = Doc.formatFourSigFigs(bal.available / ui.conventional.conversionFactor) - app().bindTooltips(page.balanceDetailBox) - } - - showAvailableMarkets (assetID: number) { - const page = this.page + // updateDisplayedAssetBalance (): void { + // const page = this.page + // const asset = app().assets[this.selectedWalletID] + // const { wallet, unitInfo: ui, id: assetID } = asset + // const bal = wallet.balance + // Doc.show(page.balanceBox, page.walletDetails) + // const totalLocked = bal.locked + bal.contractlocked + bal.bondlocked + // const totalBalance = bal.available + totalLocked + bal.immature + // page.balance.textContent = Doc.formatCoinValue(totalBalance, ui) + // page.balanceUnit.textContent = ui.conventional.unit + // const rate = app().fiatRatesMap[assetID] + // if (rate) { + // Doc.show(page.fiatBalanceBox) + // page.fiatBalance.textContent = Doc.formatFiatConversion(totalBalance, rate, ui) + // } + // Doc.empty(page.balanceDetailBox) + + // const addBalanceRow = (cat: string, bal: number, tooltipMsg?: string) => { + // const row = page.balanceDetailRow.cloneNode(true) as PageElement + // page.balanceDetailBox.appendChild(row) + // const tmpl = Doc.parseTemplate(row) + // tmpl.name.textContent = cat + // if (tooltipMsg) { + // tmpl.tooltipMsg.dataset.tooltip = tooltipMsg + // Doc.show(tmpl.tooltipMsg) + // } + // tmpl.balance.textContent = Doc.formatCoinValue(bal, ui) + // return row + // } + + // let lastSubLockedRow: PageElement | undefined + // let lastPrimaryRow: PageElement | undefined + // const addPrimaryBalance = (cat: string, bal: number, tooltipMsg?: string) => { + // lastSubLockedRow = undefined + // lastPrimaryRow = addBalanceRow(cat, bal, tooltipMsg) + // } + // const addSubBalance = (cat: string, bal: number, tooltipMsg?: string) => { + // lastSubLockedRow = addBalanceRow(cat, bal, tooltipMsg) + // lastSubLockedRow.classList.add('sub') + // } + // const setRowClasses = () => { + // if (!lastSubLockedRow) return + // (lastPrimaryRow as PageElement).classList.add('itemized') + // lastSubLockedRow.classList.add('last') + // } + + // addPrimaryBalance(intl.prep(intl.ID_AVAILABLE_TITLE), bal.available, '') + // if (bal.other?.Shielded !== undefined) { + // const transparent = bal.available - bal.other.Shielded.amt + // addSubBalance(intl.prep(intl.ID_TRANSPARENT), transparent) + // addSubBalance(intl.prep(intl.ID_SHIELDED), bal.other.Shielded.amt) + // } + // setRowClasses() + + // addPrimaryBalance(intl.prep(intl.ID_LOCKED_TITLE), totalLocked, intl.prep(intl.ID_LOCKED_BAL_MSG)) + // if (bal.orderlocked > 0) addSubBalance(intl.prep(intl.ID_ORDER), bal.orderlocked, intl.prep(intl.ID_LOCKED_ORDER_BAL_MSG)) + // if (bal.contractlocked > 0) addSubBalance(intl.prep(intl.ID_SWAPPING), bal.contractlocked, intl.prep(intl.ID_LOCKED_SWAPPING_BAL_MSG)) + // if (bal.bondlocked > 0) addSubBalance(intl.prep(intl.ID_BONDED), bal.bondlocked, intl.prep(intl.ID_LOCKED_BOND_BAL_MSG)) + // if (bal.bondReserves > 0) addSubBalance(intl.prep(intl.ID_BOND_RESERVES), bal.bondReserves, intl.prep(intl.ID_BOND_RESERVES_MSG)) + // if (bal?.other?.Staked !== undefined) addSubBalance('Staked', bal.other.Staked.amt) + // setRowClasses() + + // if (bal.immature) addPrimaryBalance(intl.prep(intl.ID_IMMATURE_TITLE), bal.immature, intl.prep(intl.ID_IMMATURE_BAL_MSG)) + // if (bal?.other?.Unmixed !== undefined) addSubBalance('Unmixed', bal.other.Unmixed.amt) + // setRowClasses() + + // // TODO: handle reserves deficit with a notification. + // // if (bal.reservesDeficit > 0) addPrimaryBalance(intl.prep(intl.ID_RESERVES_DEFICIT), bal.reservesDeficit, intl.prep(intl.ID_RESERVES_DEFICIT_MSG)) + + // page.purchaserBal.textContent = Doc.formatFourSigFigs(bal.available / ui.conventional.conversionFactor) + // app().bindTooltips(page.balanceDetailBox) + // } + + showAvailableMarkets () { + const { page, selectedTicker: { chainAssetLookup } } = this const exchanges = app().user.exchanges - const markets: [string, Exchange, Market][] = [] + const markets: [string, Exchange, Market, ChainAsset][] = [] for (const xc of Object.values(exchanges)) { - if (!xc.markets) continue - for (const mkt of Object.values(xc.markets)) { - if (mkt.baseid === assetID || mkt.quoteid === assetID) markets.push([xc.host, xc, mkt]) + for (const mkt of Object.values(xc.markets ?? [])) { + if (chainAssetLookup[mkt.baseid]) markets.push([xc.host, xc, mkt, chainAssetLookup[mkt.baseid]]) + else if (chainAssetLookup[mkt.quoteid]) markets.push([xc.host, xc, mkt, chainAssetLookup[mkt.quoteid]]) } } @@ -1694,15 +1822,15 @@ export default class WalletsPage extends BasePage { return volume / conversionFactor } - markets.sort((a: [string, Exchange, Market], b: [string, Exchange, Market]): number => { - const [hostA,, mktA] = a - const [hostB,, mktB] = b + markets.sort((a: [string, Exchange, Market, ChainAsset], b: [string, Exchange, Market, ChainAsset]): number => { + const [hostA,, mktA, caA] = a + const [hostB,, mktB, caB] = b if (!mktA.spot && !mktB.spot) return hostA.localeCompare(hostB) - return spotVolume(assetID, mktB) - spotVolume(assetID, mktA) + return spotVolume(caA.assetID, mktB) - spotVolume(caB.assetID, mktA) }) Doc.empty(page.availableMarkets) - for (const [host, xc, mkt] of markets) { + for (const [host, xc, mkt, ca] of markets) { const { spot, baseid, basesymbol, quoteid, quotesymbol } = mkt const row = page.marketRow.cloneNode(true) as PageElement page.availableMarkets.appendChild(row) @@ -1720,12 +1848,11 @@ export default class WalletsPage extends BasePage { const fmtSymbol = (s: string) => s.split('.')[0].toUpperCase() tmpl.priceQuoteUnit.textContent = fmtSymbol(quotesymbol) tmpl.priceBaseUnit.textContent = fmtSymbol(basesymbol) - tmpl.volume.textContent = Doc.formatFourSigFigs(spotVolume(assetID, mkt)) - tmpl.volumeUnit.textContent = assetID === baseid ? fmtSymbol(basesymbol) : fmtSymbol(quotesymbol) + tmpl.volume.textContent = Doc.formatFourSigFigs(spotVolume(ca.assetID, mkt)) + tmpl.volumeUnit.textContent = ca.assetID === baseid ? fmtSymbol(basesymbol) : fmtSymbol(quotesymbol) } else Doc.hide(tmpl.priceBox, tmpl.volumeBox) Doc.bind(row, 'click', () => app().loadPage('markets', { host, baseID: baseid, quoteID: quoteid })) } - page.marketsOverviewBox.classList.remove('invisible') } async showRecentActivity (assetID: number) { @@ -1742,7 +1869,6 @@ export default class WalletsPage extends BasePage { Doc.hide(page.noActivity, page.orderActivity) if (!res.orders || res.orders.length === 0) { Doc.show(page.noActivity) - page.orderActivityBox.classList.remove('invisible') return } Doc.show(page.orderActivity) @@ -1780,7 +1906,6 @@ export default class WalletsPage extends BasePage { tmpl.link.href = `order/${ord.id}` app().bindInternalNavigation(row) } - page.orderActivityBox.classList.remove('invisible') } updateTxHistoryRow (row: PageElement, tx: WalletTransaction, assetID: number) { @@ -1844,14 +1969,14 @@ export default class WalletsPage extends BasePage { const page = this.page // Block explorer - const assetExplorer = CoinExplorers[this.selectedAssetID] + const assetExplorer = CoinExplorers[this.selectedWalletID] if (assetExplorer && assetExplorer[net]) { page.txViewBlockExplorer.href = assetExplorer[net](tx.id) } // Tx type let txType = txTypeString(tx.type) - if (tx.tokenID && tx.tokenID !== this.selectedAssetID) { + if (tx.tokenID && tx.tokenID !== this.selectedWalletID) { const tokenSymbol = app().assets[tx.tokenID].symbol.split('.')[0].toUpperCase() txType = `${tokenSymbol} ${txType}` } @@ -1863,7 +1988,7 @@ export default class WalletsPage extends BasePage { if (noAmtTxTypes.includes(tx.type)) { Doc.hide(page.txDetailsAmtSection) } else { - let assetID = this.selectedAssetID + let assetID = this.selectedWalletID if (tx.tokenID) assetID = tx.tokenID Doc.show(page.txDetailsAmtSection) const ui = app().unitInfo(assetID) @@ -1874,7 +1999,7 @@ export default class WalletsPage extends BasePage { } // Fee - let feeAsset = this.selectedAssetID + let feeAsset = this.selectedWalletID if (tx.tokenID !== undefined) { const asset = app().assets[tx.tokenID] if (asset.token) { @@ -1935,14 +2060,14 @@ export default class WalletsPage extends BasePage { } showTxDetailsPopup (id: string) { - const tx = app().getWalletTx(this.selectedAssetID, id) + const tx = app().getWalletTx(this.selectedWalletID, id) if (!tx) { console.error(`wallet transaction ${id} not found`) return } this.currTx = tx this.setTxDetailsPopupElements(tx) - this.showForm(this.page.txDetails) + this.forms.show(this.page.txDetails) } txHistoryTableNewestDate () : string { @@ -1961,28 +2086,30 @@ export default class WalletsPage extends BasePage { } handleTxNote (tx: WalletTransaction, newTx: boolean) { - const w = app().assets[this.selectedAssetID].wallet - const hideMixing = (w.traits & traitFundsMixer) !== 0 && !!this.page.hideMixTxs.checked + const { page, selectedWalletID } = this + if (!Doc.isDisplayed(page.txHistoryForm)) return + const w = app().assets[selectedWalletID].wallet + const hideMixing = (w.traits & traitFundsMixer) !== 0 && !!page.hideMixTxs.checked if (hideMixing && tx.type === txTypeMixing) return if (newTx) { if (!this.oldestTx) { - Doc.show(this.page.txHistoryTable) - Doc.hide(this.page.noTxHistory) - this.page.txHistoryTableBody.appendChild(this.txHistoryDateRow(this.txDate(tx))) - this.page.txHistoryTableBody.appendChild(this.txHistoryRow(tx, this.selectedAssetID)) + Doc.show(page.txHistoryTable) + Doc.hide(page.noTxHistory) + page.txHistoryTableBody.appendChild(this.txHistoryDateRow(this.txDate(tx))) + page.txHistoryTableBody.appendChild(this.txHistoryRow(tx, selectedWalletID)) this.oldestTx = tx } else if (this.txDate(tx) !== this.txHistoryTableNewestDate()) { - this.page.txHistoryTableBody.insertBefore(this.txHistoryRow(tx, this.selectedAssetID), this.page.txHistoryTableBody.children[0]) - this.page.txHistoryTableBody.insertBefore(this.txHistoryDateRow(this.txDate(tx)), this.page.txHistoryTableBody.children[0]) + page.txHistoryTableBody.insertBefore(this.txHistoryRow(tx, selectedWalletID), page.txHistoryTableBody.children[0]) + page.txHistoryTableBody.insertBefore(this.txHistoryDateRow(this.txDate(tx)), page.txHistoryTableBody.children[0]) } else { - this.page.txHistoryTableBody.insertBefore(this.txHistoryRow(tx, this.selectedAssetID), this.page.txHistoryTableBody.children[1]) + page.txHistoryTableBody.insertBefore(this.txHistoryRow(tx, selectedWalletID), page.txHistoryTableBody.children[1]) } return } - for (const row of this.page.txHistoryTableBody.children) { + for (const row of page.txHistoryTableBody.children) { const peRow = row as PageElement if (peRow.dataset.txid === tx.id) { - this.updateTxHistoryRow(peRow, tx, this.selectedAssetID) + this.updateTxHistoryRow(peRow, tx, selectedWalletID) break } } @@ -2022,7 +2149,7 @@ export default class WalletsPage extends BasePage { async showTxHistory (assetID: number) { const page = this.page let txRes : TxHistoryResult - Doc.hide(page.txHistoryTable, page.txHistoryBox, page.noTxHistory, page.earlierTxs, page.txHistoryNotAvailable, page.hideMixTxs) + Doc.hide(page.txHistoryTable, page.noTxHistory, page.earlierTxs, page.txHistoryNotAvailable, page.hideMixTxs) Doc.empty(page.txHistoryTableBody) const w = app().assets[assetID].wallet if (!w || w.disabled || (w.traits & traitHistorian) === 0) { @@ -2034,7 +2161,7 @@ export default class WalletsPage extends BasePage { const isMixing = (w.traits & traitFundsMixer) !== 0 Doc.setVis(isMixing, page.hideMixTxs) - Doc.show(page.txHistoryBox) + this.forms.show(page.txHistoryForm) try { const hideMixing = isMixing && !!page.hideMixTxsCheckbox.checked @@ -2068,10 +2195,10 @@ export default class WalletsPage extends BasePage { if (!this.oldestTx) return const page = this.page let txRes : TxHistoryResult - const w = app().assets[this.selectedAssetID].wallet + const w = app().assets[this.selectedWalletID].wallet const hideMixing = (w.traits & traitFundsMixer) !== 0 && !!page.hideMixTxsCheckbox.checked try { - txRes = await this.getTxHistory(this.selectedAssetID, hideMixing, this.oldestTx.id) + txRes = await this.getTxHistory(this.selectedWalletID, hideMixing, this.oldestTx.id) } catch (err) { console.error(err) return @@ -2083,7 +2210,7 @@ export default class WalletsPage extends BasePage { oldestDate = date page.txHistoryTableBody.appendChild(this.txHistoryDateRow(date)) } - const row = this.txHistoryRow(tx, this.selectedAssetID) + const row = this.txHistoryRow(tx, this.selectedWalletID) page.txHistoryTableBody.appendChild(row) } Doc.setVis(!txRes.lastTx, page.earlierTxs) @@ -2117,12 +2244,12 @@ export default class WalletsPage extends BasePage { showConfirmForce () { Doc.hide(this.page.confirmForceErr) - this.showForm(this.page.confirmForce) + this.forms.show(this.page.confirmForce) } showRecoverWallet () { Doc.hide(this.page.recoverWalletErr) - this.showForm(this.page.recoverWalletConfirm) + this.forms.show(this.page.recoverWalletConfirm) } /* Show the open wallet form if the password is not cached, and otherwise @@ -2189,7 +2316,7 @@ export default class WalletsPage extends BasePage { page.recfgAssetLogo.src = Doc.logoPath(asset.symbol) page.recfgAssetName.textContent = asset.name - if (!cfg?.skipAnimation) this.showForm(page.reconfigForm) + if (!cfg?.skipAnimation) this.forms.show(page.reconfigForm) const loaded = app().loading(page.reconfigForm) const res = await postJSON('/api/walletsettings', { assetID }) loaded() @@ -2226,10 +2353,10 @@ export default class WalletsPage extends BasePage { changeWalletType () { const page = this.page const walletType = page.changeWalletTypeSelect.value || '' - const walletDef = app().walletDefinition(this.selectedAssetID, walletType) - this.reconfigForm.update(this.selectedAssetID, walletDef.configopts || [], false) - const wallet = app().walletMap[this.selectedAssetID] - const currentDef = app().currentWalletDefinition(this.selectedAssetID) + const walletDef = app().walletDefinition(this.selectedWalletID, walletType) + this.reconfigForm.update(this.selectedWalletID, walletDef.configopts || [], false) + const wallet = app().walletMap[this.selectedWalletID] + const currentDef = app().currentWalletDefinition(this.selectedWalletID) if (walletDef.type !== currentDef.type) this.setRecoverySupportMsgViz(false, wallet.symbol) else this.showOrHideRecoverySupportMsg(wallet, walletDef.seeded) this.setGuideLink(walletDef.guidelink) @@ -2245,7 +2372,7 @@ export default class WalletsPage extends BasePage { } updateDisplayedReconfigFields (walletDef: WalletDefinition) { - const disablePassword = app().extensionWallet(this.selectedAssetID)?.disablePassword + const disablePassword = app().extensionWallet(this.selectedWalletID)?.disablePassword if (walletDef.seeded || walletDef.type === 'token' || disablePassword) { Doc.hide(this.page.showChangePW, this.reconfigForm.fileSelector) this.changeWalletPW = false @@ -2254,14 +2381,33 @@ export default class WalletsPage extends BasePage { } /* Display a deposit address. */ - async showDeposit (assetID: number) { - this.depositAddrForm.setAsset(assetID) - this.showForm(this.page.deposit) - } - - /* Show the form to either send or withdraw funds. */ - async showSendForm (assetID: number) { - const page = this.page + async showDeposit () { + const { page, selectedTicker: { chainAssets } } = this + const assetIDs = chainAssets.map(({ assetID }: ChainAsset) => assetID) + this.depositAddrForm.setAssetSelect(assetIDs) + this.forms.show(page.deposit) + } + + async showSendForm () { + const { page, selectedTicker: { chainAssets } } = this + const fundedAssets: ChainAsset[] = [] + for (const ca of chainAssets) if (ca.bal.available > 0) fundedAssets.push(ca) + if (fundedAssets.length === 1) return this.showSendAssetForm(fundedAssets[0].assetID) + Doc.empty(page.netSelectBox) + for (const { assetID, chainLogo, chainName, bal, ui } of chainAssets ) { + const bttn = Doc.clone(page.netSelectBttnTmpl) + page.netSelectBox.appendChild(bttn) + const tmpl = Doc.parseTemplate(bttn) + tmpl.logo.src = chainLogo + tmpl.chainName.textContent = chainName + tmpl.bal.textContent = Doc.formatCoinValue(bal.available, ui) + Doc.bind(bttn, 'click', () => {this.showSendAssetForm(assetID)}) + } + this.forms.show(page.sendChainSelectForm) + } + + async showSendAssetForm (assetID: number) { + const { page } = this const box = page.sendForm const { wallet, unitInfo: ui, symbol, token } = app().assets[assetID] Doc.hide(page.toggleSubtract) @@ -2326,7 +2472,7 @@ export default class WalletsPage extends BasePage { Doc.showFiatValue(page.sendValue, 0, xcRate, ui) page.walletBal.textContent = Doc.formatFullPrecision(wallet.balance.available, ui) box.dataset.assetID = String(assetID) - this.showForm(box) + this.forms.show(box) } /* doConnect connects to a wallet via the connectwallet API route. */ @@ -2338,17 +2484,17 @@ export default class WalletsPage extends BasePage { const { symbol } = app().assets[assetID] const page = this.page page.errorModalMsg.textContent = intl.prep(intl.ID_CONNECT_WALLET_ERR_MSG, { assetName: symbol, errMsg: res.msg }) - this.showForm(page.errorModal) + this.forms.show(page.errorModal) } - this.updateDisplayedAsset(assetID) + this.updateSyncAndPeers() } assetUpdated (assetID: number, oldForm?: PageElement, successMsg?: string) { - if (assetID !== this.selectedAssetID) return - this.updateDisplayedAsset(assetID) - if (oldForm && Object.is(this.currentForm, oldForm)) { - if (successMsg) this.showSuccess(successMsg) - else this.closePopups() + if (this.selectedTicker.chainAssetLookup[assetID]) this.updateDisplayedTicker() + this.updateAssetBalance(assetID) + if (oldForm && Object.is(this.forms.currentForm, oldForm)) { + if (successMsg) this.forms.showSuccess(successMsg) + else this.forms.close() } } @@ -2358,7 +2504,7 @@ export default class WalletsPage extends BasePage { */ async populateMaxSend () { const page = this.page - const { id: assetID, unitInfo: ui, wallet } = app().assets[this.selectedAssetID] + const { id: assetID, unitInfo: ui, wallet } = app().assets[this.selectedWalletID] // Populate send amount with max send value and ensure we don't check // subtract checkbox for assets that don't have a withdraw method. const xcRate = app().fiatRatesMap[assetID] @@ -2407,7 +2553,7 @@ export default class WalletsPage extends BasePage { /* update wallet configuration */ async reconfig (): Promise { const page = this.page - const assetID = this.selectedAssetID + const assetID = this.selectedWalletID Doc.hide(page.reconfigErr) let walletType = app().currentWalletDefinition(assetID).type if (!Doc.isHidden(page.changeWalletType)) { @@ -2433,11 +2579,10 @@ export default class WalletsPage extends BasePage { return } this.assetUpdated(assetID, page.reconfigForm, intl.prep(intl.ID_RECONFIG_SUCCESS)) - this.updateTicketBuyer(assetID) + this.updateTicketBuyer() app().clearTxHistory(assetID) - this.showTxHistory(assetID) - this.updatePrivacy(assetID) - this.checkNeedsProvider(assetID) + // this.showTxHistory(assetID) + this.updatePrivacy() } /* lock instructs the API to lock the wallet. */ @@ -2447,13 +2592,13 @@ export default class WalletsPage extends BasePage { const res = await postJSON('/api/closewallet', { assetID: assetID }) loaded() if (!app().checkResponse(res)) return - this.updateDisplayedAsset(assetID) - this.updatePrivacy(assetID) + this.updateSyncAndPeers() + this.updatePrivacy() } async downloadLogs (): Promise { const search = new URLSearchParams('') - search.append('assetid', `${this.selectedAssetID}`) + search.append('assetid', `${this.selectedWalletID}`) const url = new URL(window.location.href) url.search = search.toString() url.pathname = '/wallets/logfile' @@ -2466,7 +2611,7 @@ export default class WalletsPage extends BasePage { const page = this.page Doc.hide(page.exportWalletErr) page.exportWalletPW.value = '' - this.showForm(page.exportWalletAuth) + this.forms.show(page.exportWalletAuth) } // exportWalletAuthSubmit is called after the user enters their password to @@ -2475,7 +2620,7 @@ export default class WalletsPage extends BasePage { async exportWalletAuthSubmit (): Promise { const page = this.page const req = { - assetID: this.selectedAssetID, + assetID: this.selectedWalletID, pass: page.exportWalletPW.value } const url = '/api/restorewalletinfo' @@ -2504,14 +2649,14 @@ export default class WalletsPage extends BasePage { tmpl.instructions.textContent = wr.instructions page.restoreInfoCardsList.appendChild(card) } - this.showForm(page.restoreWalletInfo) + this.forms.show(page.restoreWalletInfo) } async recoverWallet (): Promise { const page = this.page Doc.hide(page.recoverWalletErr) const req = { - assetID: this.selectedAssetID + assetID: this.selectedWalletID } const url = '/api/recoverwallet' const loaded = app().loading(page.forms) @@ -2522,7 +2667,7 @@ export default class WalletsPage extends BasePage { this.forceReq = req this.showConfirmForce() } else if (app().checkResponse(res)) { - this.closePopups() + this.forms.close() } else { Doc.showFormError(page.recoverWalletErr, res.msg) } @@ -2539,7 +2684,7 @@ export default class WalletsPage extends BasePage { const loaded = app().loading(page.forms) const res = await postJSON(this.forceUrl, this.forceReq) loaded() - if (app().checkResponse(res)) this.closePopups() + if (app().checkResponse(res)) this.forms.close() else { Doc.showFormError(page.confirmForceErr, res.msg) } @@ -2549,19 +2694,18 @@ export default class WalletsPage extends BasePage { value in default fiat rate. . */ handleBalanceNote (note: BalanceNote): void { - this.updateAssetButton(note.assetID) - if (note.assetID === this.selectedAssetID) this.updateDisplayedAssetBalance() + this.updateAssetBalance(note.assetID) + if (this.selectedTicker.chainAssetLookup[note.assetID]) this.updateDisplayedTickerBalance() } /* handleRatesNote handles fiat rate notifications, updating the fiat value of * all supported assets. */ - handleRatesNote (note: RateNote): void { - this.updateAssetButton(this.selectedAssetID) - if (!note.fiatRates[this.selectedAssetID]) return - this.updateDisplayedAssetBalance() - const { feeState } = app().walletMap[this.selectedAssetID] - if (feeState) this.updateFeeState(feeState) + handleRatesNote (): void { + this.updateDisplayedTickerBalance() + this.updateFeeState() + this.refreshBalances() + this.updateGlobalBalance() } /* @@ -2569,24 +2713,21 @@ export default class WalletsPage extends BasePage { * 'walletconfig' notifications. */ handleWalletStateNote (note: WalletStateNote): void { - const { assetID, feeState } = note.wallet - this.updateAssetButton(assetID) - this.assetUpdated(assetID) + const { assetID } = note.wallet + if (this.selectedTicker.chainAssetLookup[assetID]) this.updateDisplayedTicker() + if (assetID === this.selectedWalletID) this.updateFeeState() if (note.topic === 'WalletPeersUpdate' && - assetID === this.selectedAssetID && + assetID === this.selectedWalletID && Doc.isDisplayed(this.page.managePeersForm)) { this.updateWalletPeersTable() } - if (feeState && assetID === this.selectedAssetID) this.updateFeeState(feeState) } /* * handleCreateWalletNote is a handler for 'createwallet' notifications. */ handleCreateWalletNote (note: WalletCreationNote) { - this.updateAssetButton(note.assetID) - this.assetUpdated(note.assetID) - this.showTxHistory(note.assetID) + if (this.selectedTicker.chainAssetLookup[note.assetID]) this.updateDisplayedTicker() } handleCustomWalletNote (note: WalletNote) { @@ -2594,6 +2735,7 @@ export default class WalletsPage extends BasePage { switch (walletNote.route) { case 'tipChange': { const n = walletNote as TipChangeNote + if (n.assetID === this.selectedWalletID) this.page.syncHeight.textContent = String(n.tip) switch (n.assetID) { case 42: { // dcr if (!this.stakeStatus) return @@ -2613,14 +2755,14 @@ export default class WalletsPage extends BasePage { } case 'transaction': { const n = walletNote as TransactionNote - if (n.assetID === this.selectedAssetID) this.handleTxNote(n.transaction, n.new) - break - } - case 'transactionHistorySynced' : { - const n = walletNote - if (n.assetID === this.selectedAssetID) this.showTxHistory(n.assetID) + if (n.assetID === this.selectedWalletID) this.handleTxNote(n.transaction, n.new) break } + // case 'transactionHistorySynced' : { + // const n = walletNote + // if (n.assetID === this.selectedWalletID) this.showTxHistory(n.assetID) + // break + // } } } @@ -2638,3 +2780,8 @@ function trimStringWithEllipsis (str: string, maxLen: number): string { if (str.length <= maxLen) return str return `${str.substring(0, maxLen / 2)}...${str.substring(str.length - maxLen / 2)}` } + +function normalizedTicker (a: SupportedAsset): string { + const ticker = a.unitInfo.conventional.unit + return ticker === 'WETH' ? 'ETH' : ticker === 'WBTC' ? 'BTC' : ticker +} \ No newline at end of file diff --git a/client/webserver/types.go b/client/webserver/types.go index f5b8e48be4..6125d5d656 100644 --- a/client/webserver/types.go +++ b/client/webserver/types.go @@ -87,9 +87,8 @@ type walletConfig struct { // newWalletForm is information necessary to create a new wallet. type newWalletForm struct { walletConfig - Pass encode.PassBytes `json:"pass"` - AppPW encode.PassBytes `json:"appPass"` - ParentForm *walletConfig `json:"parentForm"` + Pass encode.PassBytes `json:"pass"` + AppPW encode.PassBytes `json:"appPass"` } // openWalletForm is information necessary to open a wallet. diff --git a/client/webserver/webserver.go b/client/webserver/webserver.go index eac1bb6b51..91691aeb22 100644 --- a/client/webserver/webserver.go +++ b/client/webserver/webserver.go @@ -653,7 +653,7 @@ func (s *WebServer) buildTemplates(lang string) error { addTemplate("login", bb, "forms"). addTemplate("register", bb, "forms"). addTemplate("markets", bb, "forms"). - addTemplate("wallets", bb, "forms"). + addTemplate("wallets", bb, "forms", "docs"). addTemplate("settings", bb, "forms"). addTemplate("orders", bb). addTemplate("order", bb, "forms"). diff --git a/dex/testing/loadbot/mantle.go b/dex/testing/loadbot/mantle.go index 0e388d355d..5917c53369 100644 --- a/dex/testing/loadbot/mantle.go +++ b/dex/testing/loadbot/mantle.go @@ -521,15 +521,6 @@ func (m *Mantle) createWallet(symbol string, minFunds, maxFunds uint64, numCoins } var err error - if w.parentForm != nil { - // Create the parent asset - if w.parentAddress, err = createWallet(walletPass, w.parentForm, 1); err != nil { - m.fatalError("error creating parent asset wallet: %v", err) - return - } - walletPass = nil - } - if w.address, err = createWallet(walletPass, w.form, numCoins); err != nil { m.fatalError(err.Error()) return @@ -730,7 +721,6 @@ func randomToken() string { // keep the Core wallet's balance within allowable range. type botWallet struct { form *core.WalletForm - parentForm *core.WalletForm minFunds uint64 maxFunds uint64 name string @@ -749,7 +739,7 @@ type botWallet struct { // Set numCoins to at least twice the the maximum number of (booked + epoch) // orders the wallet is expected to support. func newBotWallet(symbol, node, name string, port string, pass []byte, minFunds, maxFunds uint64, numCoins int) *botWallet { - var form, parentForm *core.WalletForm + var form *core.WalletForm switch symbol { case dcr: form = &core.WalletForm{ @@ -875,14 +865,6 @@ func newBotWallet(symbol, node, name string, port string, pass []byte, minFunds, "providers": rpcProvider, }, } - if symbol == usdc { - parentForm = form - form = &core.WalletForm{ - Type: "token", - AssetID: usdcID, - ParentForm: form, - } - } case polygon, usdcp: rpcProvider := filepath.Join(dextestDir, "polygon", "alpha", "bor", "bor.ipc") if node == beta { @@ -895,26 +877,17 @@ func newBotWallet(symbol, node, name string, port string, pass []byte, minFunds, "providers": rpcProvider, }, } - if symbol == usdcp { - parentForm = form - form = &core.WalletForm{ - Type: "token", - AssetID: usdcpID, - ParentForm: form, - } - } } return &botWallet{ - form: form, - parentForm: parentForm, - name: name, - node: node, - symbol: symbol, - pass: pass, - assetID: form.AssetID, - numCoins: numCoins, - minFunds: minFunds, - maxFunds: maxFunds, + form: form, + name: name, + node: node, + symbol: symbol, + pass: pass, + assetID: form.AssetID, + numCoins: numCoins, + minFunds: minFunds, + maxFunds: maxFunds, } } diff --git a/server/cmd/dexadm/main.go b/server/cmd/dexadm/main.go index 5fbe2616f3..3648365833 100644 --- a/server/cmd/dexadm/main.go +++ b/server/cmd/dexadm/main.go @@ -221,7 +221,7 @@ func configure() (*Config, error) { } if cfg.AdminSrvCertPath == "" { - return nil, fmt.Errorf("no adminsrvaddr argument in file or by command-line") + return nil, fmt.Errorf("no adminsrvcertpath argument in file or by command-line") } if cfg.AdminSrvPassword == "" {