Improved account transfer UI, added config-based api switch.
This commit is contained in:
		
							parent
							
								
									506a474819
								
							
						
					
					
						commit
						db2bcb576f
					
				| 
						 | 
				
			
			@ -1,7 +1,10 @@
 | 
			
		|||
package nl.andrewl.coyotecredit.ctl.api;
 | 
			
		||||
 | 
			
		||||
import lombok.RequiredArgsConstructor;
 | 
			
		||||
import nl.andrewl.coyotecredit.ctl.exchange.dto.PublicAccountData;
 | 
			
		||||
import nl.andrewl.coyotecredit.model.User;
 | 
			
		||||
import nl.andrewl.coyotecredit.service.ExchangeService;
 | 
			
		||||
import org.springframework.security.core.annotation.AuthenticationPrincipal;
 | 
			
		||||
import org.springframework.web.bind.annotation.GetMapping;
 | 
			
		||||
import org.springframework.web.bind.annotation.PathVariable;
 | 
			
		||||
import org.springframework.web.bind.annotation.RequestMapping;
 | 
			
		||||
| 
						 | 
				
			
			@ -16,7 +19,12 @@ public class ExchangeApiController {
 | 
			
		|||
	private final ExchangeService exchangeService;
 | 
			
		||||
 | 
			
		||||
	@GetMapping(path = "/tradeables")
 | 
			
		||||
	public Map<Long, String> getCurrentTradeables(@PathVariable long exchangeId) {
 | 
			
		||||
		return exchangeService.getCurrentTradeables(exchangeId);
 | 
			
		||||
	public Map<Long, String> getCurrentTradeables(@PathVariable long exchangeId, @AuthenticationPrincipal User user) {
 | 
			
		||||
		return exchangeService.getCurrentTradeables(exchangeId, user);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@GetMapping(path = "/accounts/{number}")
 | 
			
		||||
	public PublicAccountData getAccountData(@PathVariable long exchangeId, @PathVariable String number, @AuthenticationPrincipal User user) {
 | 
			
		||||
		return exchangeService.getPublicAccountData(exchangeId, number, user);
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,7 +1,7 @@
 | 
			
		|||
package nl.andrewl.coyotecredit.ctl;
 | 
			
		||||
package nl.andrewl.coyotecredit.ctl.exchange;
 | 
			
		||||
 | 
			
		||||
import lombok.RequiredArgsConstructor;
 | 
			
		||||
import nl.andrewl.coyotecredit.ctl.dto.TransferPayload;
 | 
			
		||||
import nl.andrewl.coyotecredit.ctl.exchange.dto.TransferPayload;
 | 
			
		||||
import nl.andrewl.coyotecredit.model.User;
 | 
			
		||||
import nl.andrewl.coyotecredit.service.AccountService;
 | 
			
		||||
import org.springframework.http.HttpStatus;
 | 
			
		||||
| 
						 | 
				
			
			@ -45,7 +45,7 @@ public class AccountPage {
 | 
			
		|||
 | 
			
		||||
	@GetMapping(path = "/transfer")
 | 
			
		||||
	public String getTransferPage(Model model, @PathVariable long accountId, @AuthenticationPrincipal User user) {
 | 
			
		||||
		model.addAttribute("balances", accountService.getTransferData(accountId, user));
 | 
			
		||||
		model.addAttribute("data", accountService.getTransferData(accountId, user));
 | 
			
		||||
		return "account/transfer";
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -1,7 +1,7 @@
 | 
			
		|||
package nl.andrewl.coyotecredit.ctl;
 | 
			
		||||
package nl.andrewl.coyotecredit.ctl.exchange;
 | 
			
		||||
 | 
			
		||||
import lombok.RequiredArgsConstructor;
 | 
			
		||||
import nl.andrewl.coyotecredit.ctl.dto.TradePayload;
 | 
			
		||||
import nl.andrewl.coyotecredit.ctl.exchange.dto.TradePayload;
 | 
			
		||||
import nl.andrewl.coyotecredit.model.User;
 | 
			
		||||
import nl.andrewl.coyotecredit.service.ExchangeService;
 | 
			
		||||
import nl.andrewl.coyotecredit.service.TradeService;
 | 
			
		||||
| 
						 | 
				
			
			@ -26,7 +26,7 @@ public class TradePage {
 | 
			
		|||
			@AuthenticationPrincipal User user
 | 
			
		||||
	) {
 | 
			
		||||
		model.addAttribute("data", tradeService.getTradeData(accountId, user));
 | 
			
		||||
		return "trade";
 | 
			
		||||
		return "account/trade";
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@PostMapping
 | 
			
		||||
| 
						 | 
				
			
			@ -1,4 +1,4 @@
 | 
			
		|||
package nl.andrewl.coyotecredit.ctl.dto;
 | 
			
		||||
package nl.andrewl.coyotecredit.ctl.exchange.dto;
 | 
			
		||||
 | 
			
		||||
public record BalanceData(
 | 
			
		||||
	long id,
 | 
			
		||||
| 
						 | 
				
			
			@ -1,7 +1,5 @@
 | 
			
		|||
package nl.andrewl.coyotecredit.ctl.exchange.dto;
 | 
			
		||||
 | 
			
		||||
import nl.andrewl.coyotecredit.ctl.dto.SimpleAccountData;
 | 
			
		||||
 | 
			
		||||
public record ExchangeAccountData(
 | 
			
		||||
		ExchangeData exchange,
 | 
			
		||||
		SimpleAccountData account
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,6 +1,4 @@
 | 
			
		|||
package nl.andrewl.coyotecredit.ctl.dto;
 | 
			
		||||
 | 
			
		||||
import nl.andrewl.coyotecredit.ctl.exchange.dto.ExchangeData;
 | 
			
		||||
package nl.andrewl.coyotecredit.ctl.exchange.dto;
 | 
			
		||||
 | 
			
		||||
import java.util.List;
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,10 @@
 | 
			
		|||
package nl.andrewl.coyotecredit.ctl.exchange.dto;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Account data that can be publicly seen by any member of an exchange.
 | 
			
		||||
 */
 | 
			
		||||
public record PublicAccountData(
 | 
			
		||||
		long id,
 | 
			
		||||
		String number,
 | 
			
		||||
		String name
 | 
			
		||||
) {}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,4 +1,4 @@
 | 
			
		|||
package nl.andrewl.coyotecredit.ctl.dto;
 | 
			
		||||
package nl.andrewl.coyotecredit.ctl.exchange.dto;
 | 
			
		||||
 | 
			
		||||
public record SimpleAccountData (
 | 
			
		||||
		long id,
 | 
			
		||||
| 
						 | 
				
			
			@ -1,4 +1,6 @@
 | 
			
		|||
package nl.andrewl.coyotecredit.ctl.dto;
 | 
			
		||||
package nl.andrewl.coyotecredit.ctl.exchange.dto;
 | 
			
		||||
 | 
			
		||||
import nl.andrewl.coyotecredit.ctl.dto.TradeableData;
 | 
			
		||||
 | 
			
		||||
import java.math.BigDecimal;
 | 
			
		||||
import java.util.List;
 | 
			
		||||
| 
						 | 
				
			
			@ -1,4 +1,4 @@
 | 
			
		|||
package nl.andrewl.coyotecredit.ctl.dto;
 | 
			
		||||
package nl.andrewl.coyotecredit.ctl.exchange.dto;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * The payload that's sent when a user performs a trade with the market. This
 | 
			
		||||
| 
						 | 
				
			
			@ -1,5 +1,6 @@
 | 
			
		|||
package nl.andrewl.coyotecredit.ctl.dto;
 | 
			
		||||
package nl.andrewl.coyotecredit.ctl.exchange.dto;
 | 
			
		||||
 | 
			
		||||
import nl.andrewl.coyotecredit.ctl.dto.TradeableData;
 | 
			
		||||
import nl.andrewl.coyotecredit.model.Transaction;
 | 
			
		||||
 | 
			
		||||
import java.time.format.DateTimeFormatter;
 | 
			
		||||
| 
						 | 
				
			
			@ -1,5 +1,6 @@
 | 
			
		|||
package nl.andrewl.coyotecredit.ctl.dto;
 | 
			
		||||
package nl.andrewl.coyotecredit.ctl.exchange.dto;
 | 
			
		||||
 | 
			
		||||
import nl.andrewl.coyotecredit.ctl.dto.TradeableData;
 | 
			
		||||
import nl.andrewl.coyotecredit.model.Transfer;
 | 
			
		||||
 | 
			
		||||
import java.time.format.DateTimeFormatter;
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,9 @@
 | 
			
		|||
package nl.andrewl.coyotecredit.ctl.exchange.dto;
 | 
			
		||||
 | 
			
		||||
import java.util.List;
 | 
			
		||||
 | 
			
		||||
public record TransferPageData(
 | 
			
		||||
		long exchangeId,
 | 
			
		||||
		List<BalanceData> balances
 | 
			
		||||
) {
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,4 +1,4 @@
 | 
			
		|||
package nl.andrewl.coyotecredit.ctl.dto;
 | 
			
		||||
package nl.andrewl.coyotecredit.ctl.exchange.dto;
 | 
			
		||||
 | 
			
		||||
public record TransferPayload (
 | 
			
		||||
		String recipientNumber,
 | 
			
		||||
| 
						 | 
				
			
			@ -14,6 +14,7 @@ public interface AccountRepository extends JpaRepository<Account, Long> {
 | 
			
		|||
	List<Account> findAllByUser(User user);
 | 
			
		||||
	Optional<Account> findByNumber(String number);
 | 
			
		||||
	Optional<Account> findByUserAndExchange(User user, Exchange exchange);
 | 
			
		||||
	Optional<Account> findByNumberAndExchange(String number, Exchange exchange);
 | 
			
		||||
	boolean existsByUserAndExchange(User user, Exchange exchange);
 | 
			
		||||
 | 
			
		||||
	List<Account> findAllByExchange(Exchange e);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -2,7 +2,7 @@ package nl.andrewl.coyotecredit.service;
 | 
			
		|||
 | 
			
		||||
import lombok.RequiredArgsConstructor;
 | 
			
		||||
import nl.andrewl.coyotecredit.ctl.dto.*;
 | 
			
		||||
import nl.andrewl.coyotecredit.ctl.exchange.dto.ExchangeData;
 | 
			
		||||
import nl.andrewl.coyotecredit.ctl.exchange.dto.*;
 | 
			
		||||
import nl.andrewl.coyotecredit.dao.*;
 | 
			
		||||
import nl.andrewl.coyotecredit.model.*;
 | 
			
		||||
import nl.andrewl.coyotecredit.util.AccountNumberUtils;
 | 
			
		||||
| 
						 | 
				
			
			@ -34,13 +34,13 @@ public class AccountService {
 | 
			
		|||
	private final UserNotificationRepository notificationRepository;
 | 
			
		||||
 | 
			
		||||
	@Transactional(readOnly = true)
 | 
			
		||||
	public List<BalanceData> getTransferData(long accountId, User user) {
 | 
			
		||||
	public TransferPageData getTransferData(long accountId, User user) {
 | 
			
		||||
		Account account = accountRepository.findById(accountId)
 | 
			
		||||
				.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
 | 
			
		||||
		if (!account.getUser().getId().equals(user.getId())) {
 | 
			
		||||
			throw new ResponseStatusException(HttpStatus.NOT_FOUND);
 | 
			
		||||
		}
 | 
			
		||||
		return account.getBalances().stream()
 | 
			
		||||
		List<BalanceData> balances = account.getBalances().stream()
 | 
			
		||||
				.filter(b -> b.getAmount().compareTo(BigDecimal.ZERO) > 0)
 | 
			
		||||
				.map(b -> new BalanceData(
 | 
			
		||||
						b.getTradeable().getId(),
 | 
			
		||||
| 
						 | 
				
			
			@ -50,6 +50,7 @@ public class AccountService {
 | 
			
		|||
				))
 | 
			
		||||
				.sorted(Comparator.comparing(BalanceData::symbol))
 | 
			
		||||
				.toList();
 | 
			
		||||
		return new TransferPageData(account.getExchange().getId(), balances);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Transactional
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -103,6 +103,18 @@ public class ExchangeService {
 | 
			
		|||
				.toList();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Transactional(readOnly = true)
 | 
			
		||||
	public PublicAccountData getPublicAccountData(long exchangeId, String number, User user) {
 | 
			
		||||
		Exchange exchange = exchangeRepository.findById(exchangeId)
 | 
			
		||||
				.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
 | 
			
		||||
		if (!accountRepository.existsByUserAndExchange(user, exchange)) {
 | 
			
		||||
			throw new ResponseStatusException(HttpStatus.NOT_FOUND);
 | 
			
		||||
		}
 | 
			
		||||
		Account account = accountRepository.findByNumberAndExchange(number, exchange)
 | 
			
		||||
				.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
 | 
			
		||||
		return new PublicAccountData(account.getId(), account.getNumber(), account.getName());
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Transactional(readOnly = true)
 | 
			
		||||
	public void ensureAdminAccount(long exchangeId, User user) {
 | 
			
		||||
		getExchangeIfAdmin(exchangeId, user);
 | 
			
		||||
| 
						 | 
				
			
			@ -168,9 +180,12 @@ public class ExchangeService {
 | 
			
		|||
	}
 | 
			
		||||
 | 
			
		||||
	@Transactional(readOnly = true)
 | 
			
		||||
	public Map<Long, String> getCurrentTradeables(long exchangeId) {
 | 
			
		||||
	public Map<Long, String> getCurrentTradeables(long exchangeId, User user) {
 | 
			
		||||
		Exchange e = exchangeRepository.findById(exchangeId)
 | 
			
		||||
				.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
 | 
			
		||||
		if (!accountRepository.existsByUserAndExchange(user, e)) {
 | 
			
		||||
			throw new ResponseStatusException(HttpStatus.NOT_FOUND);
 | 
			
		||||
		}
 | 
			
		||||
		Map<Long, String> tradeables = new HashMap<>();
 | 
			
		||||
		for (var t : e.getAllTradeables()) {
 | 
			
		||||
			tradeables.put(t.getId(), t.getMarketPriceUsd().toPlainString());
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,7 +1,7 @@
 | 
			
		|||
package nl.andrewl.coyotecredit.service;
 | 
			
		||||
 | 
			
		||||
import lombok.RequiredArgsConstructor;
 | 
			
		||||
import nl.andrewl.coyotecredit.ctl.dto.TradeData;
 | 
			
		||||
import nl.andrewl.coyotecredit.ctl.exchange.dto.TradeData;
 | 
			
		||||
import nl.andrewl.coyotecredit.ctl.dto.TradeableData;
 | 
			
		||||
import nl.andrewl.coyotecredit.dao.AccountRepository;
 | 
			
		||||
import nl.andrewl.coyotecredit.model.Account;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -9,6 +9,7 @@ import nl.andrewl.coyotecredit.dao.TradeableRepository;
 | 
			
		|||
import nl.andrewl.coyotecredit.model.Tradeable;
 | 
			
		||||
import nl.andrewl.coyotecredit.model.TradeableType;
 | 
			
		||||
import org.springframework.beans.factory.annotation.Value;
 | 
			
		||||
import org.springframework.core.env.Environment;
 | 
			
		||||
import org.springframework.scheduling.annotation.Scheduled;
 | 
			
		||||
import org.springframework.stereotype.Service;
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -32,6 +33,7 @@ import java.util.concurrent.TimeUnit;
 | 
			
		|||
@Slf4j
 | 
			
		||||
public class TradeableUpdateService {
 | 
			
		||||
	private final TradeableRepository tradeableRepository;
 | 
			
		||||
	private final Environment environment;
 | 
			
		||||
 | 
			
		||||
	private final HttpClient httpClient = HttpClient.newHttpClient();
 | 
			
		||||
	private final ObjectMapper objectMapper = new ObjectMapper();
 | 
			
		||||
| 
						 | 
				
			
			@ -57,7 +59,11 @@ public class TradeableUpdateService {
 | 
			
		|||
		for (var tradeable : publicTradeables) {
 | 
			
		||||
			// Special case of ignoring USD as the universal transfer currency.
 | 
			
		||||
			if (tradeable.getSymbol().equals("USD")) continue;
 | 
			
		||||
			if (environment.getProperty("coyote-credit.enable-tradeable-updates", Boolean.TYPE, true)) {
 | 
			
		||||
				executorService.schedule(() -> updateTradeable(tradeable), delay, TimeUnit.SECONDS);
 | 
			
		||||
			} else {
 | 
			
		||||
				log.info("Tradeable update skipped for {}.", tradeable.getSymbol());
 | 
			
		||||
			}
 | 
			
		||||
			delay += POLYGON_API_TIMEOUT;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -10,3 +10,4 @@ spring.mail.host=127.0.0.1
 | 
			
		|||
spring.mail.port=1025
 | 
			
		||||
 | 
			
		||||
coyote-credit.base-url=http://localhost:8080
 | 
			
		||||
coyote-credit.enable-tradeable-updates=false
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,7 +1,14 @@
 | 
			
		|||
const tradeableSelect = document.getElementById("tradeableSelect");
 | 
			
		||||
const valueInput = document.getElementById("amountInput");
 | 
			
		||||
const recipientNumberInput = document.getElementById("recipientNumberInput");
 | 
			
		||||
const recipientNumberNote = document.getElementById("recipientNumberNote");
 | 
			
		||||
 | 
			
		||||
const exchangeId = document.getElementById("exchangeIdInput").value;
 | 
			
		||||
const accountNumberRegex = new RegExp("\\d{4}-\\d{4}-\\d{4}-\\d{4}");
 | 
			
		||||
 | 
			
		||||
tradeableSelect.addEventListener("change", onSelectChanged);
 | 
			
		||||
recipientNumberInput.addEventListener("change", onAccountNumberChanged);
 | 
			
		||||
recipientNumberInput.addEventListener("keyup", onAccountNumberChanged);
 | 
			
		||||
 | 
			
		||||
function onSelectChanged() {
 | 
			
		||||
    valueInput.value = null;
 | 
			
		||||
| 
						 | 
				
			
			@ -17,3 +24,52 @@ function onSelectChanged() {
 | 
			
		|||
        valueInput.setAttribute("min", 0.0000000001);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Check that the account number that was entered is valid.
 | 
			
		||||
 */
 | 
			
		||||
function onAccountNumberChanged() {
 | 
			
		||||
    const currentNumber = recipientNumberInput.value;
 | 
			
		||||
    console.log("Account number changed to " + currentNumber);
 | 
			
		||||
    if (currentNumber !== undefined && currentNumber.length > 0) {
 | 
			
		||||
        if (accountNumberRegex.test(currentNumber)) {
 | 
			
		||||
            recipientNumberNote.innerText = "Valid account number.";
 | 
			
		||||
            fetch("/api/exchanges/" + exchangeId + "/accounts/" + currentNumber)
 | 
			
		||||
                .then(response => {
 | 
			
		||||
                    if (response.status === 200) {
 | 
			
		||||
                        response.json()
 | 
			
		||||
                            .then(data => {
 | 
			
		||||
                                console.log(data);
 | 
			
		||||
                                showRecipientNumberNote("info", "Account number is valid. Will transfer to <span class='fw-bold'>" + data.name + "</span>");
 | 
			
		||||
                            })
 | 
			
		||||
                            .catch(() => {
 | 
			
		||||
                                showRecipientNumberNote("warning", "Could not find an account with that number.");
 | 
			
		||||
                            });
 | 
			
		||||
                    } else if (response.status === 404) {
 | 
			
		||||
                        showRecipientNumberNote("warning", "Could not find an account with that number.");
 | 
			
		||||
                    } else {
 | 
			
		||||
                        showRecipientNumberNote("warning", "Error: Couldn't read API response.");
 | 
			
		||||
                    }
 | 
			
		||||
                })
 | 
			
		||||
                .catch(error => {
 | 
			
		||||
                    console.log(error);
 | 
			
		||||
                    showRecipientNumberNote("warning", "API error occurred. Couldn't fetch account information.");
 | 
			
		||||
                });
 | 
			
		||||
        } else {
 | 
			
		||||
            showRecipientNumberNote("warning", "Invalid account number. Format: <span class='monospace'>1234-1234-1234-1234</span>");
 | 
			
		||||
        }
 | 
			
		||||
    } else {
 | 
			
		||||
        showRecipientNumberNote("info", "Enter an account number.");
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function showRecipientNumberNote(type, text) {
 | 
			
		||||
    recipientNumberNote.innerHTML = text;
 | 
			
		||||
    if (type === "warning") {
 | 
			
		||||
        recipientNumberNote.classList.add("text-danger");
 | 
			
		||||
        recipientNumberNote.classList.remove("text-muted");
 | 
			
		||||
    } else {
 | 
			
		||||
        recipientNumberNote.classList.remove("text-danger");
 | 
			
		||||
        recipientNumberNote.classList.add("text-muted");
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -10,11 +10,12 @@
 | 
			
		|||
        Transfer funds to other accounts.
 | 
			
		||||
    </p>
 | 
			
		||||
    <form th:action="@{/accounts/{aId}/transfer(aId=${accountId})}" th:method="post">
 | 
			
		||||
        <input type="hidden" id="exchangeIdInput" th:value="${data.exchangeId()}"/>
 | 
			
		||||
        <div class="mb-3">
 | 
			
		||||
            <label for="recipientNumberInput" class="form-label">Recipient Account Number</label>
 | 
			
		||||
            <input type="text" name="recipientNumber" class="form-control" id="recipientNumberInput" required/>
 | 
			
		||||
            <small class="text-muted">
 | 
			
		||||
                Format: <span class="monospace">1234-1234-1234-1234</span><br>
 | 
			
		||||
            <input type="text" name="recipientNumber" class="form-control monospace" id="recipientNumberInput" required/>
 | 
			
		||||
            <small class="text-muted text-danger" id="recipientNumberNote">
 | 
			
		||||
                Enter an account number.
 | 
			
		||||
            </small>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="mb-3">
 | 
			
		||||
| 
						 | 
				
			
			@ -22,7 +23,7 @@
 | 
			
		|||
            <select class="form-select" id="tradeableSelect" name="tradeableId" required>
 | 
			
		||||
                <option value="" selected disabled hidden>Choose something to send</option>
 | 
			
		||||
                <option
 | 
			
		||||
                        th:each="b : ${balances}"
 | 
			
		||||
                        th:each="b : ${data.balances()}"
 | 
			
		||||
                        th:text="${b.symbol() + ' - Balance ' + b.amount()}"
 | 
			
		||||
                        th:value="${b.id()}"
 | 
			
		||||
                        th:data-amount="${b.amount()}"
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue