Added working first stuff.
This commit is contained in:
parent
5ffe9b3a84
commit
0538334df4
|
@ -0,0 +1,19 @@
|
||||||
|
package nl.andrewl.coyotecredit.config;
|
||||||
|
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.http.CacheControl;
|
||||||
|
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
|
||||||
|
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||||
|
|
||||||
|
import java.time.Duration;
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
public class WebMvcConfig implements WebMvcConfigurer {
|
||||||
|
@Override
|
||||||
|
public void addResourceHandlers(ResourceHandlerRegistry registry) {
|
||||||
|
registry.addResourceHandler("/static/**")
|
||||||
|
.addResourceLocations("classpath:/static/")
|
||||||
|
.setOptimizeLocations(true)
|
||||||
|
.setCacheControl(CacheControl.maxAge(Duration.ofHours(1)));
|
||||||
|
}
|
||||||
|
}
|
|
@ -26,7 +26,7 @@ public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
|
||||||
http
|
http
|
||||||
.authorizeRequests()
|
.authorizeRequests()
|
||||||
.antMatchers(
|
.antMatchers(
|
||||||
"/login", "/login/processing"
|
"/login", "/login/processing", "/static/**"
|
||||||
).permitAll()
|
).permitAll()
|
||||||
.and()
|
.and()
|
||||||
.authorizeRequests().anyRequest().authenticated()
|
.authorizeRequests().anyRequest().authenticated()
|
||||||
|
|
|
@ -1,14 +1,18 @@
|
||||||
package nl.andrewl.coyotecredit.ctl;
|
package nl.andrewl.coyotecredit.ctl;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import nl.andrewl.coyotecredit.model.User;
|
import nl.andrewl.coyotecredit.model.User;
|
||||||
import nl.andrewl.coyotecredit.service.AccountService;
|
import nl.andrewl.coyotecredit.service.AccountService;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||||
import org.springframework.stereotype.Controller;
|
import org.springframework.stereotype.Controller;
|
||||||
import org.springframework.ui.Model;
|
import org.springframework.ui.Model;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.util.MultiValueMap;
|
||||||
import org.springframework.web.bind.annotation.PathVariable;
|
import org.springframework.web.bind.annotation.*;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.server.ResponseStatusException;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
@Controller
|
@Controller
|
||||||
@RequestMapping(path = "/accounts/{accountId}")
|
@RequestMapping(path = "/accounts/{accountId}")
|
||||||
|
@ -26,4 +30,18 @@ public class AccountPage {
|
||||||
model.addAttribute("account", data);
|
model.addAttribute("account", data);
|
||||||
return "account";
|
return "account";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GetMapping(path = "/editBalances")
|
||||||
|
public String getEditBalancesPage(Model model, @PathVariable long accountId, @AuthenticationPrincipal User user) {
|
||||||
|
var data = accountService.getAccountData(user, accountId);
|
||||||
|
if (!data.userAdmin()) throw new ResponseStatusException(HttpStatus.NOT_FOUND);
|
||||||
|
model.addAttribute("account", data);
|
||||||
|
return "account/edit_balances";
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping(path = "/editBalances")
|
||||||
|
public String postEditBalances(@PathVariable long accountId, @AuthenticationPrincipal User user, @RequestParam MultiValueMap<String, String> paramMap) {
|
||||||
|
accountService.editBalances(accountId, user, paramMap);
|
||||||
|
return "redirect:/accounts/" + accountId;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,53 @@
|
||||||
|
package nl.andrewl.coyotecredit.ctl;
|
||||||
|
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import nl.andrewl.coyotecredit.ctl.dto.AddAccountPayload;
|
||||||
|
import nl.andrewl.coyotecredit.model.User;
|
||||||
|
import nl.andrewl.coyotecredit.service.ExchangeService;
|
||||||
|
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||||
|
import org.springframework.stereotype.Controller;
|
||||||
|
import org.springframework.ui.Model;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
@Controller
|
||||||
|
@RequestMapping(path = "/exchanges/{exchangeId}")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class ExchangePage {
|
||||||
|
private final ExchangeService exchangeService;
|
||||||
|
|
||||||
|
@GetMapping
|
||||||
|
public String get(Model model, @PathVariable long exchangeId, @AuthenticationPrincipal User user) {
|
||||||
|
model.addAttribute("exchange", exchangeService.getData(exchangeId, user));
|
||||||
|
return "exchange";
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping(path = "/accounts")
|
||||||
|
public String getAccountsPage(Model model, @PathVariable long exchangeId, @AuthenticationPrincipal User user) {
|
||||||
|
model.addAttribute("accounts", exchangeService.getAccounts(exchangeId, user));
|
||||||
|
return "exchange/accounts";
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping(path = "/addAccount")
|
||||||
|
public String getAddAccountPage(@PathVariable long exchangeId, @AuthenticationPrincipal User user) {
|
||||||
|
exchangeService.ensureAdminAccount(exchangeId, user);
|
||||||
|
return "exchange/addAccount";
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping(path = "/addAccount")
|
||||||
|
public String postAddAcount(@PathVariable long exchangeId, @AuthenticationPrincipal User user, @ModelAttribute AddAccountPayload payload) {
|
||||||
|
long accountId = exchangeService.addAccount(exchangeId, user, payload);
|
||||||
|
return "redirect:/accounts/" + accountId;
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping(path = "/removeAccount/{accountId}")
|
||||||
|
public String getRemoveAccountPage(@PathVariable long exchangeId, @PathVariable long accountId, @AuthenticationPrincipal User user) {
|
||||||
|
exchangeService.ensureAdminAccount(exchangeId, user);
|
||||||
|
return "exchange/removeAccount";
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping(path = "/removeAccount/{accountId}")
|
||||||
|
public String postRemoveAccount(@PathVariable long exchangeId, @PathVariable long accountId, @AuthenticationPrincipal User user) {
|
||||||
|
exchangeService.removeAccount(exchangeId, accountId, user);
|
||||||
|
return "redirect:/exchanges/" + exchangeId + "/accounts";
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,41 @@
|
||||||
|
package nl.andrewl.coyotecredit.ctl;
|
||||||
|
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import nl.andrewl.coyotecredit.ctl.dto.TradePayload;
|
||||||
|
import nl.andrewl.coyotecredit.model.User;
|
||||||
|
import nl.andrewl.coyotecredit.service.ExchangeService;
|
||||||
|
import nl.andrewl.coyotecredit.service.TradeService;
|
||||||
|
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||||
|
import org.springframework.stereotype.Controller;
|
||||||
|
import org.springframework.ui.Model;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import javax.validation.Valid;
|
||||||
|
|
||||||
|
@Controller
|
||||||
|
@RequestMapping(path = "/trade/{accountId}")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class TradePage {
|
||||||
|
private final TradeService tradeService;
|
||||||
|
private final ExchangeService exchangeService;
|
||||||
|
|
||||||
|
@GetMapping
|
||||||
|
public String get(
|
||||||
|
Model model,
|
||||||
|
@PathVariable long accountId,
|
||||||
|
@AuthenticationPrincipal User user
|
||||||
|
) {
|
||||||
|
model.addAttribute("data", tradeService.getTradeData(accountId, user));
|
||||||
|
return "trade";
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping
|
||||||
|
public String doTrade(
|
||||||
|
@PathVariable long accountId,
|
||||||
|
@AuthenticationPrincipal User user,
|
||||||
|
@ModelAttribute @Valid TradePayload payload
|
||||||
|
) {
|
||||||
|
exchangeService.doTrade(accountId, payload, user);
|
||||||
|
return "redirect:/accounts/" + accountId;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,24 @@
|
||||||
|
package nl.andrewl.coyotecredit.ctl;
|
||||||
|
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import nl.andrewl.coyotecredit.model.User;
|
||||||
|
import nl.andrewl.coyotecredit.service.UserService;
|
||||||
|
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||||
|
import org.springframework.stereotype.Controller;
|
||||||
|
import org.springframework.ui.Model;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
|
||||||
|
@Controller
|
||||||
|
@RequestMapping(path = "/users/{userId}")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class UserPage {
|
||||||
|
private final UserService userService;
|
||||||
|
|
||||||
|
@GetMapping
|
||||||
|
public String get(Model model, @PathVariable long userId, @AuthenticationPrincipal User user) {
|
||||||
|
model.addAttribute("user", userService.getUser(userId, user));
|
||||||
|
return "user";
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,22 @@
|
||||||
|
package nl.andrewl.coyotecredit.ctl.api;
|
||||||
|
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import nl.andrewl.coyotecredit.service.ExchangeService;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/exchanges/{exchangeId}")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class ExchangeApiController {
|
||||||
|
private final ExchangeService exchangeService;
|
||||||
|
|
||||||
|
@GetMapping(path = "/tradeables")
|
||||||
|
public Map<Long, String> getCurrentTradeables(@PathVariable long exchangeId) {
|
||||||
|
return exchangeService.getCurrentTradeables(exchangeId);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
package nl.andrewl.coyotecredit.ctl.dto;
|
||||||
|
|
||||||
|
public record AddAccountPayload(
|
||||||
|
String name,
|
||||||
|
String username,
|
||||||
|
String password
|
||||||
|
) {}
|
|
@ -0,0 +1,7 @@
|
||||||
|
package nl.andrewl.coyotecredit.ctl.dto;
|
||||||
|
|
||||||
|
public record BalanceData(
|
||||||
|
long id,
|
||||||
|
String symbol,
|
||||||
|
String amount
|
||||||
|
) {}
|
|
@ -0,0 +1,8 @@
|
||||||
|
package nl.andrewl.coyotecredit.ctl.dto;
|
||||||
|
|
||||||
|
public record ExchangeData(
|
||||||
|
long id,
|
||||||
|
String name,
|
||||||
|
String primaryTradeable
|
||||||
|
) {
|
||||||
|
}
|
|
@ -0,0 +1,19 @@
|
||||||
|
package nl.andrewl.coyotecredit.ctl.dto;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The data that's needed for displaying a user's account page.
|
||||||
|
*/
|
||||||
|
public record FullAccountData (
|
||||||
|
long id,
|
||||||
|
String number,
|
||||||
|
String name,
|
||||||
|
boolean admin,
|
||||||
|
boolean userAdmin,
|
||||||
|
ExchangeData exchange,
|
||||||
|
List<BalanceData> balances,
|
||||||
|
String totalBalance,
|
||||||
|
List<TransactionData> recentTransactions
|
||||||
|
) {
|
||||||
|
}
|
|
@ -0,0 +1,16 @@
|
||||||
|
package nl.andrewl.coyotecredit.ctl.dto;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The data that's used on the exchange page.
|
||||||
|
*/
|
||||||
|
public record FullExchangeData (
|
||||||
|
long id,
|
||||||
|
String name,
|
||||||
|
TradeableData primaryTradeable,
|
||||||
|
List<TradeableData> supportedTradeables,
|
||||||
|
// Account info
|
||||||
|
boolean accountAdmin
|
||||||
|
) {
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
package nl.andrewl.coyotecredit.ctl.dto;
|
||||||
|
|
||||||
|
public record SimpleAccountData (
|
||||||
|
long id,
|
||||||
|
String number,
|
||||||
|
String name,
|
||||||
|
boolean admin,
|
||||||
|
String totalBalance
|
||||||
|
) {
|
||||||
|
}
|
|
@ -0,0 +1,14 @@
|
||||||
|
package nl.andrewl.coyotecredit.ctl.dto;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public record TradeData(
|
||||||
|
long accountId,
|
||||||
|
String accountNumber,
|
||||||
|
long exchangeId,
|
||||||
|
List<TradeableData> tradeablesToSell,
|
||||||
|
Map<String, BigDecimal> accountBalances,
|
||||||
|
List<TradeableData> tradeablesToBuy
|
||||||
|
) {}
|
|
@ -0,0 +1,23 @@
|
||||||
|
package nl.andrewl.coyotecredit.ctl.dto;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The payload that's sent when a user performs a trade with the market. This
|
||||||
|
* can be either a SELL or a BUY trade.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* In a SELL trade, the user indicates an amount of their selling tradeable
|
||||||
|
* to sell in exchange for the market-equivalent value of the buying
|
||||||
|
* tradeable.
|
||||||
|
* </p>
|
||||||
|
* <p>
|
||||||
|
* In a BUY trade, the opposite happens, where the user indicates how much
|
||||||
|
* of the buying tradeable they want to acquire, and will have a market-
|
||||||
|
* equivalent value deducted from their selling tradeable.
|
||||||
|
* </p>
|
||||||
|
*/
|
||||||
|
public record TradePayload(
|
||||||
|
String type, // SELL or BUY
|
||||||
|
long sellTradeableId,
|
||||||
|
long buyTradeableId,
|
||||||
|
String value
|
||||||
|
) {}
|
|
@ -0,0 +1,23 @@
|
||||||
|
package nl.andrewl.coyotecredit.ctl.dto;
|
||||||
|
|
||||||
|
import nl.andrewl.coyotecredit.model.Tradeable;
|
||||||
|
|
||||||
|
public record TradeableData(
|
||||||
|
long id,
|
||||||
|
String symbol,
|
||||||
|
String type,
|
||||||
|
String marketPriceUsd,
|
||||||
|
String name,
|
||||||
|
String description
|
||||||
|
) {
|
||||||
|
public TradeableData(Tradeable t) {
|
||||||
|
this(
|
||||||
|
t.getId(),
|
||||||
|
t.getSymbol(),
|
||||||
|
t.getType().name(),
|
||||||
|
t.getMarketPriceUsd().toPlainString(),
|
||||||
|
t.getName(),
|
||||||
|
t.getDescription()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,25 @@
|
||||||
|
package nl.andrewl.coyotecredit.ctl.dto;
|
||||||
|
|
||||||
|
import nl.andrewl.coyotecredit.model.Transaction;
|
||||||
|
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
|
||||||
|
public record TransactionData(
|
||||||
|
long id,
|
||||||
|
TradeableData from,
|
||||||
|
String fromAmount,
|
||||||
|
TradeableData to,
|
||||||
|
String toAmount,
|
||||||
|
String timestamp
|
||||||
|
) {
|
||||||
|
public TransactionData(Transaction t) {
|
||||||
|
this(
|
||||||
|
t.getId(),
|
||||||
|
new TradeableData(t.getFrom()),
|
||||||
|
t.getFromAmount().toPlainString(),
|
||||||
|
new TradeableData(t.getTo()),
|
||||||
|
t.getToAmount().toPlainString(),
|
||||||
|
t.getTimestamp().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
package nl.andrewl.coyotecredit.ctl.dto;
|
||||||
|
|
||||||
|
public record UserData (
|
||||||
|
long id,
|
||||||
|
String username
|
||||||
|
) {}
|
|
@ -1,13 +1,17 @@
|
||||||
package nl.andrewl.coyotecredit.dao;
|
package nl.andrewl.coyotecredit.dao;
|
||||||
|
|
||||||
import nl.andrewl.coyotecredit.model.Account;
|
import nl.andrewl.coyotecredit.model.Account;
|
||||||
|
import nl.andrewl.coyotecredit.model.Exchange;
|
||||||
import nl.andrewl.coyotecredit.model.User;
|
import nl.andrewl.coyotecredit.model.User;
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
import org.springframework.stereotype.Repository;
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
@Repository
|
@Repository
|
||||||
public interface AccountRepository extends JpaRepository<Account, Long> {
|
public interface AccountRepository extends JpaRepository<Account, Long> {
|
||||||
List<Account> findAllByUser(User user);
|
List<Account> findAllByUser(User user);
|
||||||
|
Optional<Account> findByNumber(String number);
|
||||||
|
Optional<Account> findByUserAndExchange(User user, Exchange exchange);
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
package nl.andrewl.coyotecredit.dao;
|
||||||
|
|
||||||
|
import nl.andrewl.coyotecredit.model.Exchange;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
public interface ExchangeRepository extends JpaRepository<Exchange, Long> {
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
package nl.andrewl.coyotecredit.dao;
|
||||||
|
|
||||||
|
import nl.andrewl.coyotecredit.model.Tradeable;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
public interface TradeableRepository extends JpaRepository<Tradeable, Long> {
|
||||||
|
}
|
|
@ -0,0 +1,12 @@
|
||||||
|
package nl.andrewl.coyotecredit.dao;
|
||||||
|
|
||||||
|
import nl.andrewl.coyotecredit.model.Transaction;
|
||||||
|
import org.springframework.data.domain.Page;
|
||||||
|
import org.springframework.data.domain.Pageable;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
public interface TransactionRepository extends JpaRepository<Transaction, Long> {
|
||||||
|
Page<Transaction> findAllByAccountNumberOrderByTimestampDesc(String accountNumber, Pageable pageable);
|
||||||
|
}
|
|
@ -6,7 +6,9 @@ import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
import javax.persistence.*;
|
import javax.persistence.*;
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
|
import java.math.RoundingMode;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
|
@ -22,23 +24,72 @@ public class Account {
|
||||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
private Long id;
|
private Long id;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The unique account number.
|
||||||
|
*/
|
||||||
@Column(nullable = false, unique = true)
|
@Column(nullable = false, unique = true)
|
||||||
private String number;
|
private String number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The user that this account belongs to.
|
||||||
|
*/
|
||||||
@ManyToOne(fetch = FetchType.LAZY, optional = false)
|
@ManyToOne(fetch = FetchType.LAZY, optional = false)
|
||||||
private User user;
|
private User user;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The name on this account.
|
||||||
|
*/
|
||||||
|
@Column
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The exchange that this account belongs to.
|
||||||
|
*/
|
||||||
@ManyToOne(fetch = FetchType.LAZY, optional = false)
|
@ManyToOne(fetch = FetchType.LAZY, optional = false)
|
||||||
private Exchange exchange;
|
private Exchange exchange;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether this account is an administrator in the exchange it's linked to.
|
||||||
|
* Administrators have special permissions to add and remove other accounts,
|
||||||
|
* custom tradeables, exchange rates, and more.
|
||||||
|
*/
|
||||||
|
@Column(nullable = false)
|
||||||
|
private boolean admin;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The set of tradeable balances that this account has.
|
||||||
|
*/
|
||||||
@OneToMany(mappedBy = "account", fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true)
|
@OneToMany(mappedBy = "account", fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true)
|
||||||
private Set<Balance> balances;
|
private Set<Balance> balances;
|
||||||
|
|
||||||
public Map<Currency, BigDecimal> getMappedBalances() {
|
public Account(String number, User user, String name, Exchange exchange) {
|
||||||
Map<Currency, BigDecimal> b = new HashMap<>();
|
this.number = number;
|
||||||
|
this.user = user;
|
||||||
|
this.name = name;
|
||||||
|
this.exchange = exchange;
|
||||||
|
this.balances = new HashSet<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<Tradeable, BigDecimal> getMappedBalances() {
|
||||||
|
Map<Tradeable, BigDecimal> b = new HashMap<>();
|
||||||
for (var bal : getBalances()) {
|
for (var bal : getBalances()) {
|
||||||
b.put(bal.getCurrency(), bal.getAmount());
|
b.put(bal.getTradeable(), bal.getAmount());
|
||||||
}
|
}
|
||||||
return b;
|
return b;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Balance getBalanceForTradeable(Tradeable t) {
|
||||||
|
for (var bal : getBalances()) {
|
||||||
|
if (bal.getTradeable().equals(t)) return bal;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public BigDecimal getTotalBalance() {
|
||||||
|
BigDecimal totalUsd = new BigDecimal(0);
|
||||||
|
for (var bal : getBalances()) {
|
||||||
|
totalUsd = totalUsd.add(bal.getTradeable().getMarketPriceUsd().multiply(bal.getAmount()));
|
||||||
|
}
|
||||||
|
return totalUsd.divide(getExchange().getPrimaryTradeable().getMarketPriceUsd(), RoundingMode.HALF_UP);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,24 +1,43 @@
|
||||||
package nl.andrewl.coyotecredit.model;
|
package nl.andrewl.coyotecredit.model;
|
||||||
|
|
||||||
|
import lombok.AccessLevel;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import lombok.Setter;
|
||||||
|
|
||||||
import javax.persistence.*;
|
import javax.persistence.*;
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents an account's balance for a certain amount of tradeables in their
|
||||||
|
* exchange.
|
||||||
|
*/
|
||||||
@Entity
|
@Entity
|
||||||
|
@NoArgsConstructor(access = AccessLevel.PROTECTED)
|
||||||
@Table(name = "account_balance")
|
@Table(name = "account_balance")
|
||||||
@Getter
|
@Getter
|
||||||
public class Balance {
|
public class Balance {
|
||||||
@Id
|
@EmbeddedId
|
||||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
private BalanceId balanceId;
|
||||||
private Long id;
|
|
||||||
|
|
||||||
|
@MapsId("accountId")
|
||||||
@ManyToOne(optional = false, fetch = FetchType.LAZY)
|
@ManyToOne(optional = false, fetch = FetchType.LAZY)
|
||||||
|
@JoinColumn(name = "account_id")
|
||||||
private Account account;
|
private Account account;
|
||||||
|
|
||||||
|
@MapsId("tradeableId")
|
||||||
@ManyToOne(optional = false, fetch = FetchType.EAGER)
|
@ManyToOne(optional = false, fetch = FetchType.EAGER)
|
||||||
private Currency currency;
|
@JoinColumn(name = "tradeable_id")
|
||||||
|
private Tradeable tradeable;
|
||||||
|
|
||||||
@Column(nullable = false, precision = 24, scale = 10)
|
@Column(nullable = false, precision = 24, scale = 10)
|
||||||
|
@Setter
|
||||||
private BigDecimal amount;
|
private BigDecimal amount;
|
||||||
|
|
||||||
|
public Balance(Account account, Tradeable tradeable, BigDecimal amount) {
|
||||||
|
this.balanceId = new BalanceId(tradeable.getId(), account.getId());
|
||||||
|
this.account = account;
|
||||||
|
this.tradeable = tradeable;
|
||||||
|
this.amount = amount;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,17 @@
|
||||||
|
package nl.andrewl.coyotecredit.model;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
import javax.persistence.Embeddable;
|
||||||
|
import java.io.Serializable;
|
||||||
|
|
||||||
|
@Embeddable
|
||||||
|
@EqualsAndHashCode
|
||||||
|
@AllArgsConstructor
|
||||||
|
@NoArgsConstructor
|
||||||
|
public class BalanceId implements Serializable {
|
||||||
|
private Long tradeableId;
|
||||||
|
private Long accountId;
|
||||||
|
}
|
|
@ -1,48 +0,0 @@
|
||||||
package nl.andrewl.coyotecredit.model;
|
|
||||||
|
|
||||||
import lombok.Getter;
|
|
||||||
|
|
||||||
import javax.persistence.*;
|
|
||||||
import java.util.Objects;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Represents a type of currency. This can be an actual fiat currency, or
|
|
||||||
* perhaps a cryptocurrency, or stocks, or really anything tradeable on the
|
|
||||||
* exchange.
|
|
||||||
*/
|
|
||||||
@Entity
|
|
||||||
@Table(uniqueConstraints = @UniqueConstraint(columnNames = {"identifier", "type"}))
|
|
||||||
@Getter
|
|
||||||
public class Currency {
|
|
||||||
@Id
|
|
||||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
|
||||||
private Long id;
|
|
||||||
|
|
||||||
@Column(nullable = false)
|
|
||||||
private String identifier;
|
|
||||||
|
|
||||||
@Enumerated(EnumType.STRING)
|
|
||||||
private CurrencyType type;
|
|
||||||
|
|
||||||
@Column(nullable = false)
|
|
||||||
private String name;
|
|
||||||
|
|
||||||
@Column
|
|
||||||
private String description;
|
|
||||||
|
|
||||||
@Column(nullable = false)
|
|
||||||
private float minDenomination = 0.01f;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean equals(Object other) {
|
|
||||||
if (!(other instanceof Currency c)) return false;
|
|
||||||
if (c.getId() != null && this.getId() != null) return this.getId().equals(c.getId());
|
|
||||||
return this.identifier.equals(c.getIdentifier()) &&
|
|
||||||
this.type.equals(c.getType());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int hashCode() {
|
|
||||||
return Objects.hash(this.identifier, this.type);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,20 @@
|
||||||
|
package nl.andrewl.coyotecredit.model;
|
||||||
|
|
||||||
|
import lombok.AccessLevel;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
import javax.persistence.Entity;
|
||||||
|
import javax.persistence.FetchType;
|
||||||
|
import javax.persistence.ManyToOne;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@NoArgsConstructor(access = AccessLevel.PROTECTED)
|
||||||
|
@Getter
|
||||||
|
public class CustomTradeable extends Tradeable {
|
||||||
|
/**
|
||||||
|
* The exchange that the tradeable belongs to.
|
||||||
|
*/
|
||||||
|
@ManyToOne(optional = false, fetch = FetchType.LAZY)
|
||||||
|
private Exchange exchange;
|
||||||
|
}
|
|
@ -1,15 +1,19 @@
|
||||||
package nl.andrewl.coyotecredit.model;
|
package nl.andrewl.coyotecredit.model;
|
||||||
|
|
||||||
|
import lombok.AccessLevel;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
import javax.persistence.*;
|
import javax.persistence.*;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
|
import java.util.Objects;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents a large collection of users that interact with each other.
|
* Represents a large collection of users that interact with each other.
|
||||||
*/
|
*/
|
||||||
@Entity
|
@Entity
|
||||||
|
@NoArgsConstructor(access = AccessLevel.PROTECTED)
|
||||||
@Getter
|
@Getter
|
||||||
public class Exchange {
|
public class Exchange {
|
||||||
@Id
|
@Id
|
||||||
|
@ -22,15 +26,50 @@ public class Exchange {
|
||||||
@Column(nullable = false)
|
@Column(nullable = false)
|
||||||
private String name;
|
private String name;
|
||||||
|
|
||||||
@OneToMany(mappedBy = "exchange")
|
/**
|
||||||
private Set<ExchangePair> currencyPairs;
|
* The primary tradeable that's used by this exchange.
|
||||||
|
*/
|
||||||
|
@ManyToOne(optional = false, fetch = FetchType.LAZY)
|
||||||
|
private Tradeable primaryTradeable;
|
||||||
|
|
||||||
public Set<Currency> getSupportedCurrencies() {
|
/**
|
||||||
Set<Currency> currencies = new HashSet<>();
|
* The set of tradeables that this exchange allows users to interact with.
|
||||||
for (var pair : getCurrencyPairs()) {
|
*/
|
||||||
currencies.add(pair.getFromCurrency());
|
@ManyToMany(fetch = FetchType.LAZY)
|
||||||
currencies.add(pair.getToCurrency());
|
@JoinTable(
|
||||||
|
name = "exchange_supported_tradeable",
|
||||||
|
joinColumns = @JoinColumn(name = "exchange_id"),
|
||||||
|
inverseJoinColumns = @JoinColumn(name = "tradeable_id")
|
||||||
|
)
|
||||||
|
private Set<Tradeable> supportedTradeables;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The set of custom tradeables created specifically for use in this exchange.
|
||||||
|
*/
|
||||||
|
@OneToMany(mappedBy = "exchange", fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true)
|
||||||
|
private Set<CustomTradeable> customTradeables;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The set of accounts that are registered with this exchange.
|
||||||
|
*/
|
||||||
|
@OneToMany(mappedBy = "exchange", fetch = FetchType.LAZY)
|
||||||
|
private Set<Account> accounts;
|
||||||
|
|
||||||
|
public Set<Tradeable> getAllTradeables() {
|
||||||
|
Set<Tradeable> s = new HashSet<>();
|
||||||
|
s.addAll(getSupportedTradeables());
|
||||||
|
s.addAll(getCustomTradeables());
|
||||||
|
return s;
|
||||||
}
|
}
|
||||||
return currencies;
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object other) {
|
||||||
|
if (!(other instanceof Exchange e)) return false;
|
||||||
|
return this.getId() != null && e.getId() != null && this.getId().equals(e.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return Objects.hash(this.getId());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,30 +0,0 @@
|
||||||
package nl.andrewl.coyotecredit.model;
|
|
||||||
|
|
||||||
import lombok.Getter;
|
|
||||||
|
|
||||||
import javax.persistence.*;
|
|
||||||
import java.math.BigDecimal;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Represents a pair of currencies that can be exchanged at a set exchange rate.
|
|
||||||
*/
|
|
||||||
@Entity
|
|
||||||
@Table(uniqueConstraints = @UniqueConstraint(columnNames = {"from_currency_id", "to_currency_id", "exchange_id"}))
|
|
||||||
@Getter
|
|
||||||
public class ExchangePair {
|
|
||||||
@Id
|
|
||||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
|
||||||
private Long id;
|
|
||||||
|
|
||||||
@ManyToOne(optional = false)
|
|
||||||
private Exchange exchange;
|
|
||||||
|
|
||||||
@ManyToOne(optional = false)
|
|
||||||
private Currency fromCurrency;
|
|
||||||
|
|
||||||
@ManyToOne(optional = false)
|
|
||||||
private Currency toCurrency;
|
|
||||||
|
|
||||||
@Column(nullable = false, precision = 24, scale = 10)
|
|
||||||
private BigDecimal exchangeRate = new BigDecimal("1.0");
|
|
||||||
}
|
|
|
@ -0,0 +1,59 @@
|
||||||
|
package nl.andrewl.coyotecredit.model;
|
||||||
|
|
||||||
|
import lombok.AccessLevel;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
import javax.persistence.*;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a type of currency. This can be an actual fiat currency, or
|
||||||
|
* perhaps a cryptocurrency, or stocks, or really anything tradeable on the
|
||||||
|
* exchange.
|
||||||
|
*/
|
||||||
|
@Entity
|
||||||
|
@Inheritance(strategy = InheritanceType.JOINED)
|
||||||
|
@NoArgsConstructor(access = AccessLevel.PROTECTED)
|
||||||
|
@Getter
|
||||||
|
public class Tradeable {
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@Column(nullable = false)
|
||||||
|
private String symbol;
|
||||||
|
|
||||||
|
@Enumerated(EnumType.STRING)
|
||||||
|
private TradeableType type;
|
||||||
|
|
||||||
|
@Column(nullable = false, precision = 24, scale = 10)
|
||||||
|
private BigDecimal marketPriceUsd = new BigDecimal(1);
|
||||||
|
|
||||||
|
@Column(nullable = false)
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
@Column
|
||||||
|
private String description;
|
||||||
|
|
||||||
|
public Tradeable(String symbol, TradeableType type, String name, String description, BigDecimal marketPriceUsd) {
|
||||||
|
this.symbol = symbol;
|
||||||
|
this.type = type;
|
||||||
|
this.name = name;
|
||||||
|
this.description = description;
|
||||||
|
this.marketPriceUsd = marketPriceUsd;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object other) {
|
||||||
|
if (!(other instanceof Tradeable c)) return false;
|
||||||
|
if (this.getId() != null && c.getId() != null) return this.getId().equals(c.getId());
|
||||||
|
return this.getSymbol().equals(c.getSymbol()) && this.getType().equals(c.getType());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return Objects.hash(this.id, this.symbol);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
package nl.andrewl.coyotecredit.model;
|
package nl.andrewl.coyotecredit.model;
|
||||||
|
|
||||||
public enum CurrencyType {
|
public enum TradeableType {
|
||||||
FIAT,
|
FIAT,
|
||||||
CRYPTO,
|
CRYPTO,
|
||||||
STOCK
|
STOCK
|
|
@ -0,0 +1,53 @@
|
||||||
|
package nl.andrewl.coyotecredit.model;
|
||||||
|
|
||||||
|
import lombok.AccessLevel;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import org.hibernate.annotations.CreationTimestamp;
|
||||||
|
|
||||||
|
import javax.persistence.*;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a permanent record of a trade transaction.
|
||||||
|
*/
|
||||||
|
@Entity
|
||||||
|
@NoArgsConstructor(access = AccessLevel.PROTECTED)
|
||||||
|
@Getter
|
||||||
|
public class Transaction {
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@Column(nullable = false)
|
||||||
|
private String accountNumber;
|
||||||
|
|
||||||
|
@ManyToOne(optional = false, fetch = FetchType.LAZY)
|
||||||
|
private Exchange exchange;
|
||||||
|
|
||||||
|
@ManyToOne(optional = false, fetch = FetchType.LAZY)
|
||||||
|
private Tradeable from;
|
||||||
|
|
||||||
|
@Column(nullable = false, precision = 24, scale = 10)
|
||||||
|
private BigDecimal fromAmount;
|
||||||
|
|
||||||
|
@ManyToOne(optional = false, fetch = FetchType.LAZY)
|
||||||
|
private Tradeable to;
|
||||||
|
|
||||||
|
@Column(nullable = false, precision = 24, scale = 10)
|
||||||
|
private BigDecimal toAmount;
|
||||||
|
|
||||||
|
@Column(nullable = false, updatable = false)
|
||||||
|
private LocalDateTime timestamp;
|
||||||
|
|
||||||
|
public Transaction(String accountNumber, Exchange exchange, Tradeable from, BigDecimal fromAmount, Tradeable to, BigDecimal toAmount, LocalDateTime timestamp) {
|
||||||
|
this.accountNumber = accountNumber;
|
||||||
|
this.exchange = exchange;
|
||||||
|
this.from = from;
|
||||||
|
this.fromAmount = fromAmount;
|
||||||
|
this.to = to;
|
||||||
|
this.toAmount = toAmount;
|
||||||
|
this.timestamp = timestamp;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,18 +1,22 @@
|
||||||
package nl.andrewl.coyotecredit.model;
|
package nl.andrewl.coyotecredit.model;
|
||||||
|
|
||||||
|
import lombok.AccessLevel;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
import org.springframework.security.core.GrantedAuthority;
|
import org.springframework.security.core.GrantedAuthority;
|
||||||
import org.springframework.security.core.userdetails.UserDetails;
|
import org.springframework.security.core.userdetails.UserDetails;
|
||||||
|
|
||||||
import javax.persistence.*;
|
import javax.persistence.*;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
import java.util.HashSet;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents a basic user in the system.
|
* Represents a basic user in the system.
|
||||||
*/
|
*/
|
||||||
@Entity
|
@Entity
|
||||||
|
@NoArgsConstructor(access = AccessLevel.PROTECTED)
|
||||||
@Getter
|
@Getter
|
||||||
public class User implements UserDetails {
|
public class User implements UserDetails {
|
||||||
@Id
|
@Id
|
||||||
|
@ -31,6 +35,11 @@ public class User implements UserDetails {
|
||||||
@OneToMany(fetch = FetchType.LAZY, mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true)
|
@OneToMany(fetch = FetchType.LAZY, mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true)
|
||||||
private Set<Account> accounts;
|
private Set<Account> accounts;
|
||||||
|
|
||||||
|
public User(String username, String passwordHash) {
|
||||||
|
this.username = username;
|
||||||
|
this.passwordHash = passwordHash;
|
||||||
|
this.accounts = new HashSet<>();
|
||||||
|
}
|
||||||
|
|
||||||
// USER DETAILS IMPLEMENTATION
|
// USER DETAILS IMPLEMENTATION
|
||||||
|
|
||||||
|
|
|
@ -1,22 +1,34 @@
|
||||||
package nl.andrewl.coyotecredit.service;
|
package nl.andrewl.coyotecredit.service;
|
||||||
|
|
||||||
import lombok.AllArgsConstructor;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import nl.andrewl.coyotecredit.ctl.dto.BalanceData;
|
||||||
|
import nl.andrewl.coyotecredit.ctl.dto.ExchangeData;
|
||||||
|
import nl.andrewl.coyotecredit.ctl.dto.FullAccountData;
|
||||||
|
import nl.andrewl.coyotecredit.ctl.dto.TransactionData;
|
||||||
import nl.andrewl.coyotecredit.dao.AccountRepository;
|
import nl.andrewl.coyotecredit.dao.AccountRepository;
|
||||||
|
import nl.andrewl.coyotecredit.dao.TradeableRepository;
|
||||||
|
import nl.andrewl.coyotecredit.dao.TransactionRepository;
|
||||||
import nl.andrewl.coyotecredit.model.Account;
|
import nl.andrewl.coyotecredit.model.Account;
|
||||||
|
import nl.andrewl.coyotecredit.model.Balance;
|
||||||
|
import nl.andrewl.coyotecredit.model.Tradeable;
|
||||||
import nl.andrewl.coyotecredit.model.User;
|
import nl.andrewl.coyotecredit.model.User;
|
||||||
|
import org.springframework.data.domain.PageRequest;
|
||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
import org.springframework.util.MultiValueMap;
|
||||||
import org.springframework.web.server.ResponseStatusException;
|
import org.springframework.web.server.ResponseStatusException;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.math.BigDecimal;
|
||||||
|
import java.util.Comparator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class AccountService {
|
public class AccountService {
|
||||||
private final AccountRepository accountRepository;
|
private final AccountRepository accountRepository;
|
||||||
|
private final TransactionRepository transactionRepository;
|
||||||
|
private final TradeableRepository tradeableRepository;
|
||||||
|
|
||||||
public static record AccountData (
|
public static record AccountData (
|
||||||
long id,
|
long id,
|
||||||
|
@ -27,40 +39,68 @@ public class AccountService {
|
||||||
@Transactional(readOnly = true)
|
@Transactional(readOnly = true)
|
||||||
public List<AccountData> getAccountsOverview(User user) {
|
public List<AccountData> getAccountsOverview(User user) {
|
||||||
return accountRepository.findAllByUser(user).stream()
|
return accountRepository.findAllByUser(user).stream()
|
||||||
.map(a -> new AccountData(a.getId(), a.getNumber(), a.getExchange().getName()))
|
.map(a -> new AccountData(
|
||||||
|
a.getId(),
|
||||||
|
a.getNumber(),
|
||||||
|
a.getExchange().getName()
|
||||||
|
))
|
||||||
.toList();
|
.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class FullAccountData {
|
|
||||||
public long id;
|
|
||||||
public String number;
|
|
||||||
public String exchangeName;
|
|
||||||
public List<BalanceData> balances;
|
|
||||||
}
|
|
||||||
|
|
||||||
@AllArgsConstructor
|
|
||||||
public static class BalanceData {
|
|
||||||
public String currencyIdentifier;
|
|
||||||
public String amount;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Transactional(readOnly = true)
|
@Transactional(readOnly = true)
|
||||||
public FullAccountData getAccountData(User user, long accountId) {
|
public FullAccountData getAccountData(User user, long accountId) {
|
||||||
Account account = accountRepository.findById(accountId)
|
Account account = accountRepository.findById(accountId)
|
||||||
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
|
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
|
||||||
|
Account userAccount = accountRepository.findByUserAndExchange(user, account.getExchange())
|
||||||
if (!account.getUser().getId().equals(user.getId())) {
|
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
|
||||||
|
if (!userAccount.isAdmin() && !account.getUser().getId().equals(user.getId())) {
|
||||||
throw new ResponseStatusException(HttpStatus.NOT_FOUND);
|
throw new ResponseStatusException(HttpStatus.NOT_FOUND);
|
||||||
}
|
}
|
||||||
FullAccountData d = new FullAccountData();
|
List<TransactionData> transactionData = transactionRepository.findAllByAccountNumberOrderByTimestampDesc(account.getNumber(), PageRequest.of(0, 5))
|
||||||
d.id = account.getId();
|
.map(TransactionData::new)
|
||||||
d.number = account.getNumber();
|
.stream().toList();
|
||||||
d.exchangeName = account.getExchange().getName();
|
return new FullAccountData(
|
||||||
List<BalanceData> balanceData = new ArrayList<>();
|
account.getId(),
|
||||||
for (var bal : account.getBalances()) {
|
account.getNumber(),
|
||||||
balanceData.add(new BalanceData(bal.getCurrency().getIdentifier(), bal.getAmount().toPlainString()));
|
account.getName(),
|
||||||
|
account.isAdmin(),
|
||||||
|
userAccount.isAdmin(),
|
||||||
|
new ExchangeData(
|
||||||
|
account.getExchange().getId(),
|
||||||
|
account.getExchange().getName(),
|
||||||
|
account.getExchange().getPrimaryTradeable().getSymbol()
|
||||||
|
),
|
||||||
|
account.getBalances().stream()
|
||||||
|
.map(b -> new BalanceData(b.getTradeable().getId(), b.getTradeable().getSymbol(), b.getAmount().toPlainString()))
|
||||||
|
.sorted(Comparator.comparing(BalanceData::symbol))
|
||||||
|
.toList(),
|
||||||
|
account.getTotalBalance().toPlainString(),
|
||||||
|
transactionData
|
||||||
|
);
|
||||||
}
|
}
|
||||||
d.balances = balanceData;
|
|
||||||
return d;
|
@Transactional
|
||||||
|
public void editBalances(long accountId, User user, MultiValueMap<String, String> paramMap) {
|
||||||
|
Account account = accountRepository.findById(accountId)
|
||||||
|
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
|
||||||
|
Account userAccount = accountRepository.findByUserAndExchange(user, account.getExchange())
|
||||||
|
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
|
||||||
|
if (!userAccount.isAdmin()) throw new ResponseStatusException(HttpStatus.NOT_FOUND);
|
||||||
|
for (var entry : paramMap.entrySet()) {
|
||||||
|
if (entry.getKey().startsWith("tradeable-")) {
|
||||||
|
long tradeableId = Long.parseLong(entry.getKey().substring(10));
|
||||||
|
Tradeable tradeable = tradeableRepository.findById(tradeableId)
|
||||||
|
.orElseThrow(() -> new ResponseStatusException(HttpStatus.BAD_REQUEST));
|
||||||
|
BigDecimal value = new BigDecimal(entry.getValue().get(0));
|
||||||
|
Balance bal = account.getBalanceForTradeable(tradeable);
|
||||||
|
if (bal == null) {
|
||||||
|
bal = new Balance(account, tradeable, value);
|
||||||
|
account.getBalances().add(bal);
|
||||||
|
} else {
|
||||||
|
bal.setAmount(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
accountRepository.save(account);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,191 @@
|
||||||
|
package nl.andrewl.coyotecredit.service;
|
||||||
|
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import nl.andrewl.coyotecredit.ctl.dto.*;
|
||||||
|
import nl.andrewl.coyotecredit.dao.*;
|
||||||
|
import nl.andrewl.coyotecredit.model.*;
|
||||||
|
import nl.andrewl.coyotecredit.util.AccountNumberUtils;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
import org.springframework.web.server.ResponseStatusException;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.math.RoundingMode;
|
||||||
|
import java.text.DecimalFormat;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.time.ZoneOffset;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class ExchangeService {
|
||||||
|
private final ExchangeRepository exchangeRepository;
|
||||||
|
private final AccountRepository accountRepository;
|
||||||
|
private final TransactionRepository transactionRepository;
|
||||||
|
private final TradeableRepository tradeableRepository;
|
||||||
|
private final UserRepository userRepository;
|
||||||
|
private final PasswordEncoder passwordEncoder;
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public FullExchangeData getData(long exchangeId, User user) {
|
||||||
|
Exchange e = exchangeRepository.findById(exchangeId)
|
||||||
|
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
|
||||||
|
Account account = accountRepository.findByUserAndExchange(user, e)
|
||||||
|
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
|
||||||
|
return new FullExchangeData(
|
||||||
|
e.getId(),
|
||||||
|
e.getName(),
|
||||||
|
new TradeableData(e.getPrimaryTradeable()),
|
||||||
|
e.getAllTradeables().stream()
|
||||||
|
.map(TradeableData::new)
|
||||||
|
.sorted(Comparator.comparing(TradeableData::symbol))
|
||||||
|
.toList(),
|
||||||
|
account.isAdmin()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public List<SimpleAccountData> getAccounts(long exchangeId, User user) {
|
||||||
|
Exchange exchange = exchangeRepository.findById(exchangeId)
|
||||||
|
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
|
||||||
|
Account account = accountRepository.findByUserAndExchange(user, exchange)
|
||||||
|
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
|
||||||
|
if (!account.isAdmin()) {
|
||||||
|
throw new ResponseStatusException(HttpStatus.NOT_FOUND);
|
||||||
|
}
|
||||||
|
DecimalFormat df = new DecimalFormat("#,###0.00");
|
||||||
|
return exchange.getAccounts().stream()
|
||||||
|
.sorted(Comparator.comparing(Account::getName))
|
||||||
|
.map(a -> new SimpleAccountData(
|
||||||
|
a.getId(),
|
||||||
|
a.getNumber(),
|
||||||
|
a.getName(),
|
||||||
|
a.isAdmin(),
|
||||||
|
df.format(a.getTotalBalance()) + ' ' + exchange.getPrimaryTradeable().getSymbol()
|
||||||
|
))
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public void ensureAdminAccount(long exchangeId, User user) {
|
||||||
|
Exchange exchange = exchangeRepository.findById(exchangeId)
|
||||||
|
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
|
||||||
|
Account account = accountRepository.findByUserAndExchange(user, exchange)
|
||||||
|
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
|
||||||
|
if (!account.isAdmin()) {
|
||||||
|
throw new ResponseStatusException(HttpStatus.NOT_FOUND);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public long addAccount(long exchangeId, User user, AddAccountPayload payload) {
|
||||||
|
Exchange exchange = exchangeRepository.findById(exchangeId)
|
||||||
|
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
|
||||||
|
Account account = accountRepository.findByUserAndExchange(user, exchange)
|
||||||
|
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
|
||||||
|
if (!account.isAdmin()) {
|
||||||
|
throw new ResponseStatusException(HttpStatus.NOT_FOUND);
|
||||||
|
}
|
||||||
|
User u = userRepository.save(new User(payload.username(), passwordEncoder.encode(payload.password())));
|
||||||
|
Account a = accountRepository.save(new Account(
|
||||||
|
AccountNumberUtils.generate(),
|
||||||
|
u,
|
||||||
|
payload.name(),
|
||||||
|
exchange
|
||||||
|
));
|
||||||
|
for (var t : exchange.getAllTradeables()) {
|
||||||
|
a.getBalances().add(new Balance(a, t, BigDecimal.ZERO));
|
||||||
|
}
|
||||||
|
a = accountRepository.save(a);
|
||||||
|
return a.getId();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public void removeAccount(long exchangeId, long accountId, User user) {
|
||||||
|
Exchange exchange = exchangeRepository.findById(exchangeId)
|
||||||
|
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
|
||||||
|
Account account = accountRepository.findById(accountId)
|
||||||
|
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
|
||||||
|
Account userAccount = accountRepository.findByUserAndExchange(user, exchange)
|
||||||
|
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
|
||||||
|
if (!userAccount.isAdmin()) {
|
||||||
|
throw new ResponseStatusException(HttpStatus.NOT_FOUND);
|
||||||
|
}
|
||||||
|
accountRepository.delete(account);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public Map<Long, String> getCurrentTradeables(long exchangeId) {
|
||||||
|
Exchange e = exchangeRepository.findById(exchangeId)
|
||||||
|
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
|
||||||
|
Map<Long, String> tradeables = new HashMap<>();
|
||||||
|
for (var t : e.getAllTradeables()) {
|
||||||
|
tradeables.put(t.getId(), t.getMarketPriceUsd().toPlainString());
|
||||||
|
}
|
||||||
|
return tradeables;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public void doTrade(long accountId, TradePayload payload, User user) {
|
||||||
|
Account account = accountRepository.findById(accountId)
|
||||||
|
.orElseThrow(() -> new ResponseStatusException(HttpStatus.BAD_REQUEST, "Account not found."));
|
||||||
|
if (!account.getUser().getId().equals(user.getId())) {
|
||||||
|
throw new ResponseStatusException(HttpStatus.NOT_FOUND);
|
||||||
|
}
|
||||||
|
Exchange exchange = account.getExchange();
|
||||||
|
Tradeable from = tradeableRepository.findById(payload.sellTradeableId())
|
||||||
|
.orElseThrow(() -> new ResponseStatusException(HttpStatus.BAD_REQUEST, "Sell tradeable not found."));
|
||||||
|
Tradeable to = tradeableRepository.findById(payload.buyTradeableId())
|
||||||
|
.orElseThrow(() -> new ResponseStatusException(HttpStatus.BAD_REQUEST, "Buy tradeable not found."));
|
||||||
|
BigDecimal value = new BigDecimal(payload.value());
|
||||||
|
if (from.getType().equals(TradeableType.STOCK)) {
|
||||||
|
if (!payload.type().equalsIgnoreCase("SELL")) {
|
||||||
|
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Can only perform SELL operations when selling stocks.");
|
||||||
|
}
|
||||||
|
if (to.getType().equals(TradeableType.STOCK)) {
|
||||||
|
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Cannot sell stock to purchase stock.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (to.getType().equals(TradeableType.STOCK)) {
|
||||||
|
if (!payload.type().equalsIgnoreCase("BUY")) {
|
||||||
|
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Can only perform BUY operations when buying stocks.");
|
||||||
|
}
|
||||||
|
if (from.getType().equals(TradeableType.STOCK)) {
|
||||||
|
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Cannot sell stock to purchase stock.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (value.compareTo(BigDecimal.ZERO) <= 0) {
|
||||||
|
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Only positive value may be specified.");
|
||||||
|
}
|
||||||
|
|
||||||
|
BigDecimal fromValue;
|
||||||
|
BigDecimal toValue;
|
||||||
|
if (payload.type().equalsIgnoreCase("SELL")) {
|
||||||
|
fromValue = value;
|
||||||
|
toValue = fromValue.multiply(from.getMarketPriceUsd()).divide(to.getMarketPriceUsd(), RoundingMode.HALF_UP);
|
||||||
|
} else {
|
||||||
|
toValue = value;
|
||||||
|
fromValue = toValue.multiply(to.getMarketPriceUsd()).divide(from.getMarketPriceUsd(), RoundingMode.HALF_UP);
|
||||||
|
}
|
||||||
|
Balance fromBalance = account.getBalanceForTradeable(from);
|
||||||
|
if (fromBalance == null || fromBalance.getAmount().compareTo(fromValue) < 0) {
|
||||||
|
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Missing required balance of " + fromValue.toPlainString() + " from " + from.getSymbol());
|
||||||
|
}
|
||||||
|
fromBalance.setAmount(fromBalance.getAmount().subtract(fromValue));
|
||||||
|
Balance toBalance = account.getBalanceForTradeable(to);
|
||||||
|
if (toBalance == null) {
|
||||||
|
toBalance = new Balance(account, to, toValue);
|
||||||
|
account.getBalances().add(toBalance);
|
||||||
|
} else {
|
||||||
|
toBalance.setAmount(toBalance.getAmount().add(toValue));
|
||||||
|
}
|
||||||
|
accountRepository.save(account);
|
||||||
|
Transaction tx = new Transaction(account.getNumber(), exchange, from, fromValue, to, toValue, LocalDateTime.now(ZoneOffset.UTC));
|
||||||
|
transactionRepository.save(tx);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,42 @@
|
||||||
|
package nl.andrewl.coyotecredit.service;
|
||||||
|
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import nl.andrewl.coyotecredit.ctl.dto.TradeData;
|
||||||
|
import nl.andrewl.coyotecredit.ctl.dto.TradeableData;
|
||||||
|
import nl.andrewl.coyotecredit.dao.AccountRepository;
|
||||||
|
import nl.andrewl.coyotecredit.model.Account;
|
||||||
|
import nl.andrewl.coyotecredit.model.User;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
import org.springframework.web.server.ResponseStatusException;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class TradeService {
|
||||||
|
private final AccountRepository accountRepository;
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public TradeData getTradeData(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);
|
||||||
|
}
|
||||||
|
List<TradeableData> sellList = new ArrayList<>();
|
||||||
|
Map<String, BigDecimal> accountBalances = new HashMap<>();
|
||||||
|
for (var bal : account.getBalances()) {
|
||||||
|
sellList.add(new TradeableData(bal.getTradeable()));
|
||||||
|
accountBalances.put(bal.getTradeable().getSymbol(), bal.getAmount());
|
||||||
|
}
|
||||||
|
sellList.sort(Comparator.comparing(TradeableData::symbol));
|
||||||
|
List<TradeableData> buyList = new ArrayList<>();
|
||||||
|
for (var t : account.getExchange().getAllTradeables()) {
|
||||||
|
buyList.add(new TradeableData(t));
|
||||||
|
}
|
||||||
|
|
||||||
|
return new TradeData(account.getId(), account.getNumber(), account.getExchange().getId(), sellList, accountBalances, buyList);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,27 @@
|
||||||
|
package nl.andrewl.coyotecredit.service;
|
||||||
|
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import nl.andrewl.coyotecredit.ctl.dto.UserData;
|
||||||
|
import nl.andrewl.coyotecredit.dao.UserRepository;
|
||||||
|
import nl.andrewl.coyotecredit.model.User;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
import org.springframework.web.server.ResponseStatusException;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class UserService {
|
||||||
|
private final UserRepository userRepository;
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public UserData getUser(long userId, User requestingUser) {
|
||||||
|
User user;
|
||||||
|
if (requestingUser.getId().equals(userId)) {
|
||||||
|
user = requestingUser;
|
||||||
|
} else {
|
||||||
|
user = userRepository.findById(userId).orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
|
||||||
|
}
|
||||||
|
return new UserData(user.getId(), user.getUsername());
|
||||||
|
}
|
||||||
|
}
|
|
@ -8,7 +8,7 @@ public class AccountNumberUtils {
|
||||||
StringBuilder sb = new StringBuilder(19);
|
StringBuilder sb = new StringBuilder(19);
|
||||||
Random rand = new SecureRandom();
|
Random rand = new SecureRandom();
|
||||||
for (int i = 0; i < 16; i++) {
|
for (int i = 0; i < 16; i++) {
|
||||||
if (i % 4 == 0) sb.append('-');
|
if (i > 0 && i % 4 == 0) sb.append('-');
|
||||||
sb.append(rand.nextInt(0, 10));
|
sb.append(rand.nextInt(0, 10));
|
||||||
}
|
}
|
||||||
return sb.toString();
|
return sb.toString();
|
||||||
|
|
|
@ -0,0 +1,155 @@
|
||||||
|
const sellValueInput = document.getElementById("sellValueInput");
|
||||||
|
const sellTradeableSelect = document.getElementById("sellTradeableSelect");
|
||||||
|
const sellTradeableSelectText = document.getElementById("sellTradeableSelectText");
|
||||||
|
const buyValueInput = document.getElementById("buyValueInput");
|
||||||
|
const buyTradeableSelect = document.getElementById("buyTradeableSelect");
|
||||||
|
const submitButton = document.getElementById("submitButton");
|
||||||
|
|
||||||
|
sellTradeableSelect.selectedIndex = null;
|
||||||
|
buyTradeableSelect.selectedIndex = null;
|
||||||
|
|
||||||
|
sellTradeableSelect.addEventListener("change", onSellSelectChanged);
|
||||||
|
sellValueInput.addEventListener("change", onSellInputChanged);
|
||||||
|
buyTradeableSelect.addEventListener("change", onBuySelectChanged);
|
||||||
|
buyValueInput.addEventListener("change", onBuyInputChanged);
|
||||||
|
submitButton.addEventListener("click", onSubmitClicked);
|
||||||
|
|
||||||
|
let tradeables = {};
|
||||||
|
const exchangeId = Number(document.getElementById("exchangeIdInput").value);
|
||||||
|
const accountId = Number(document.getElementById("accountIdInput").value);
|
||||||
|
refreshTradeables();
|
||||||
|
window.setInterval(refreshTradeables, 10000);
|
||||||
|
|
||||||
|
function refreshTradeables() {
|
||||||
|
fetch("/api/exchanges/" + exchangeId + "/tradeables")
|
||||||
|
.then((response) => {
|
||||||
|
if (response.status !== 200) {
|
||||||
|
console.error("Exchange API call failed.");
|
||||||
|
} else {
|
||||||
|
response.json().then((data) => {
|
||||||
|
tradeables = data;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSelectedSellOption() {
|
||||||
|
return sellTradeableSelect.options[sellTradeableSelect.selectedIndex];
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSelectedBuyOption() {
|
||||||
|
return buyTradeableSelect.options[buyTradeableSelect.selectedIndex];
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateInputSettings(input, step, min, max) {
|
||||||
|
input.setAttribute("step", step);
|
||||||
|
input.setAttribute("min", min);
|
||||||
|
input.setAttribute("max", max);
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetValueInputs() {
|
||||||
|
sellValueInput.value = null;
|
||||||
|
buyValueInput.value = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Event handlers
|
||||||
|
|
||||||
|
function onSellSelectChanged() {
|
||||||
|
resetValueInputs();
|
||||||
|
const sellOption = getSelectedSellOption();
|
||||||
|
const sellType = sellOption.dataset.type;
|
||||||
|
const sellBalance = Number(sellOption.dataset.balance);
|
||||||
|
|
||||||
|
// Don't allow the user to buy the same thing they're selling.
|
||||||
|
for (let i = 0; i < buyTradeableSelect.length; i++) {
|
||||||
|
const buyOption = buyTradeableSelect.options[i];
|
||||||
|
const buyType = buyOption.dataset.type;
|
||||||
|
|
||||||
|
let isOptionDisabled = buyOption.value === sellOption.value;
|
||||||
|
if (sellType === "STOCK" && buyType === "STOCK") {
|
||||||
|
isOptionDisabled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isOptionDisabled) {
|
||||||
|
buyOption.setAttribute("disabled", true);
|
||||||
|
} else {
|
||||||
|
buyOption.removeAttribute("disabled");
|
||||||
|
}
|
||||||
|
// If the user is currently selecting an invalid choice, reset.
|
||||||
|
if (i === buyTradeableSelect.selectedIndex && isOptionDisabled) {
|
||||||
|
buyTradeableSelect.value = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the sell value input for the current type's parameters.
|
||||||
|
if (sellType === "STOCK") {
|
||||||
|
updateInputSettings(sellValueInput, 1, 0, sellBalance);
|
||||||
|
buyValueInput.setAttribute("readonly", true);
|
||||||
|
} else if (sellType === "FIAT" || sellType === "CRYPTO") {
|
||||||
|
updateInputSettings(sellValueInput, 0.0000000001, 0, sellBalance);
|
||||||
|
buyValueInput.removeAttribute("readonly");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the subtext to show the value.
|
||||||
|
sellTradeableSelectText.innerText = `Balance: ${sellBalance}`;
|
||||||
|
sellTradeableSelectText.removeAttribute("hidden");
|
||||||
|
}
|
||||||
|
|
||||||
|
function onBuySelectChanged() {
|
||||||
|
resetValueInputs();
|
||||||
|
const buyOption = getSelectedBuyOption();
|
||||||
|
const buyType = buyOption.dataset.type;
|
||||||
|
|
||||||
|
if (buyType === "STOCK") {
|
||||||
|
updateInputSettings(buyValueInput, 1, 0, 1000000);
|
||||||
|
sellValueInput.setAttribute("readonly", true);
|
||||||
|
} else if (buyType === "FIAT" || buyType === "CRYPTO") {
|
||||||
|
updateInputSettings(buyValueInput, 0.0000000001, 0, 1000000);
|
||||||
|
sellValueInput.removeAttribute("readonly");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onSellInputChanged() {
|
||||||
|
const sellOption = getSelectedSellOption();
|
||||||
|
const buyOption = getSelectedBuyOption();
|
||||||
|
const sellId = sellOption.value;
|
||||||
|
const buyId = buyOption.value;
|
||||||
|
const sellPriceUsd = tradeables[sellId];
|
||||||
|
const buyPriceUsd = tradeables[buyId];
|
||||||
|
|
||||||
|
if (sellPriceUsd && buyPriceUsd) {
|
||||||
|
const sellVolume = Number(sellValueInput.value);
|
||||||
|
buyValueInput.value = (sellPriceUsd * sellVolume) / buyPriceUsd;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onBuyInputChanged() {
|
||||||
|
const sellOption = getSelectedSellOption();
|
||||||
|
const buyOption = getSelectedBuyOption();
|
||||||
|
const sellId = sellOption.value;
|
||||||
|
const buyId = buyOption.value;
|
||||||
|
const sellPriceUsd = tradeables[sellId];
|
||||||
|
const buyPriceUsd = tradeables[buyId];
|
||||||
|
|
||||||
|
if (sellPriceUsd && buyPriceUsd) {
|
||||||
|
const buyVolume = Number(buyValueInput.value);
|
||||||
|
sellValueInput.value = (buyPriceUsd * buyVolume) / sellPriceUsd;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onSubmitClicked() {
|
||||||
|
let type = "SELL";
|
||||||
|
let value = Number(sellValueInput.value);
|
||||||
|
if (sellValueInput.hasAttribute("readonly")) {
|
||||||
|
type = "BUY";
|
||||||
|
value = Number(buyValueInput.value);
|
||||||
|
}
|
||||||
|
const sellTradeableId = parseInt(getSelectedSellOption().value);
|
||||||
|
const buyTradeableId = parseInt(getSelectedBuyOption().value);
|
||||||
|
const form = document.getElementById("tradeForm");
|
||||||
|
form.elements["type"].value = type;
|
||||||
|
form.elements["sellTradeableId"].value = sellTradeableId;
|
||||||
|
form.elements["buyTradeableId"].value = buyTradeableId;
|
||||||
|
form.elements["value"].value = value;
|
||||||
|
form.submit();
|
||||||
|
}
|
|
@ -2,18 +2,53 @@
|
||||||
<html
|
<html
|
||||||
lang="en"
|
lang="en"
|
||||||
xmlns:th="http://www.thymeleaf.org"
|
xmlns:th="http://www.thymeleaf.org"
|
||||||
|
th:replace="~{layout/basic_page :: layout (title='Account', content=~{::#content})}"
|
||||||
>
|
>
|
||||||
<head>
|
|
||||||
<title>CC - Account</title>
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
<div id="content" class="container">
|
||||||
<h1>Account <span th:text="${account.number}"></span></h1>
|
<h1>Account <span th:text="${account.number()}"></span></h1>
|
||||||
|
<p>In <a th:href="@{/exchanges/{id}(id=${account.exchange().id()})}" th:text="${account.exchange().name()}"></a></p>
|
||||||
|
<p>Total value of <span th:text="${account.totalBalance() + ' ' + account.exchange().primaryTradeable()}"></span></p>
|
||||||
|
|
||||||
<h3>Balance</h3>
|
<h3>Overview</h3>
|
||||||
<ul>
|
|
||||||
<li th:each="balance : ${account.balances}">
|
<table class="table">
|
||||||
<span th:text="${balance.currencyIdentifier}"></span> - <span th:text="${balance.amount}"></span>
|
<thead>
|
||||||
</li>
|
<tr>
|
||||||
</ul>
|
<th>Currency</th>
|
||||||
</body>
|
<th>Balance</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr th:each="bal : ${account.balances()}">
|
||||||
|
<td th:text="${bal.symbol()}"></td>
|
||||||
|
<td th:text="${bal.amount()}"></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<h3>Recent Transactions</h3>
|
||||||
|
<table class="table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>From</th>
|
||||||
|
<th>Amount From</th>
|
||||||
|
<th>To</th>
|
||||||
|
<th>Amount To</th>
|
||||||
|
<th>Timestamp</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr th:each="tx : ${account.recentTransactions()}">
|
||||||
|
<td th:text="${tx.from().name()}"></td>
|
||||||
|
<td th:text="${tx.fromAmount()}"></td>
|
||||||
|
<td th:text="${tx.to().name()}"></td>
|
||||||
|
<td th:text="${tx.toAmount()}"></td>
|
||||||
|
<td th:text="${tx.timestamp()}"></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<a class="btn btn-success" th:href="@{/trade/{account}(account=${account.id()})}">Trade</a>
|
||||||
|
<a class="btn btn-primary" th:if="${account.userAdmin()}" th:href="@{/accounts/{aId}/editBalances(aId=${account.id()})}">Edit Balances</a>
|
||||||
|
</div>
|
|
@ -0,0 +1,17 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html
|
||||||
|
lang="en"
|
||||||
|
xmlns:th="http://www.thymeleaf.org"
|
||||||
|
th:replace="~{layout/basic_page :: layout (title='Add Account', content=~{::#content})}"
|
||||||
|
>
|
||||||
|
<div id="content" class="container">
|
||||||
|
<h1>Edit Balances</h1>
|
||||||
|
|
||||||
|
<form th:action="@{/accounts/{aId}/editBalances(aId=${accountId})}" th:method="post">
|
||||||
|
<div class="mb-3" th:each="bal, iter : ${account.balances()}">
|
||||||
|
<label th:for="${'tradeable-' + bal.id()}" class="form-label" th:text="${bal.symbol()}"></label>
|
||||||
|
<input type="number" min="0" th:value="${bal.amount()}" th:name="${'tradeable-' + bal.id()}" class="form-control"/>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-success">Submit</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
|
@ -0,0 +1,39 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html
|
||||||
|
lang="en"
|
||||||
|
xmlns:th="http://www.thymeleaf.org"
|
||||||
|
th:replace="~{layout/basic_page :: layout (title='Exchange', content=~{::#content})}"
|
||||||
|
>
|
||||||
|
<div id="content" class="container">
|
||||||
|
<h1 th:text="${exchange.name()}"></h1>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Primary tradeable: <span th:text="${exchange.primaryTradeable().name()}"></span>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h3>Supported Tradeable Currencies / Stocks</h3>
|
||||||
|
<table class="table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Symbol</th>
|
||||||
|
<th>Type</th>
|
||||||
|
<th>Price ($)</th>
|
||||||
|
<th>Name</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr th:each="tradeable : ${exchange.supportedTradeables()}">
|
||||||
|
<td th:text="${tradeable.symbol()}"></td>
|
||||||
|
<td th:text="${tradeable.type()}"></td>
|
||||||
|
<td th:text="${tradeable.marketPriceUsd()}"></td>
|
||||||
|
<td th:text="${tradeable.name()}"></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<div th:if="${exchange.accountAdmin()}">
|
||||||
|
<div>
|
||||||
|
<a class="btn btn-primary" th:href="@{/exchanges/{eId}/accounts(eId=${exchange.id()})}">View Accounts</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
|
@ -0,0 +1,31 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html
|
||||||
|
lang="en"
|
||||||
|
xmlns:th="http://www.thymeleaf.org"
|
||||||
|
th:replace="~{layout/basic_page :: layout (title='Exchange Accounts', content=~{::#content})}"
|
||||||
|
>
|
||||||
|
<div id="content" class="container">
|
||||||
|
<h1>Accounts</h1>
|
||||||
|
|
||||||
|
<a class="btn btn-primary" th:href="@{/exchanges/{eId}/addAccount(eId=${exchangeId})}">Add Account</a>
|
||||||
|
|
||||||
|
<table class="table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Number</th>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Admin</th>
|
||||||
|
<th>Balance</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr th:each="account : ${accounts}">
|
||||||
|
<td><a th:href="@{/accounts/{id}(id=${account.id()})}" th:text="${account.number()}"></a></td>
|
||||||
|
<td th:text="${account.name()}"></td>
|
||||||
|
<td th:text="${account.admin()}"></td>
|
||||||
|
<td th:text="${account.totalBalance()}"></td>
|
||||||
|
<td><a th:href="@{/exchanges/{eId}/removeAccount/{aId}(eId=${exchangeId}, aId=${account.id()})}">Remove</a></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
|
@ -0,0 +1,29 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html
|
||||||
|
lang="en"
|
||||||
|
xmlns:th="http://www.thymeleaf.org"
|
||||||
|
th:replace="~{layout/basic_page :: layout (title='Add Account', content=~{::#content})}"
|
||||||
|
>
|
||||||
|
<div id="content" class="container">
|
||||||
|
<h1>Add Account</h1>
|
||||||
|
<p>
|
||||||
|
Use this page to add an account to the exchange.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<form th:href="@{/exchanges/{eId}/addAccount(eId=${exchangeId})}" th:method="post">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="nameInput" class="form-label">Name</label>
|
||||||
|
<input id="nameInput" type="text" class="form-control" name="name"/>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="usernameInput" class="form-label">Username</label>
|
||||||
|
<input id="usernameInput" type="text" class="form-control" name="username"/>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="passwordInput" class="form-label">Password</label>
|
||||||
|
<input id="passwordInput" type="password" class="form-control" name="password"/>
|
||||||
|
</div>
|
||||||
|
<input type="hidden" name="_csrf" th:value="${_csrf.getToken()}"/>
|
||||||
|
<button type="submit" class="btn btn-primary">Submit</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
|
@ -0,0 +1,16 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html
|
||||||
|
lang="en"
|
||||||
|
xmlns:th="http://www.thymeleaf.org"
|
||||||
|
th:replace="~{layout/basic_page :: layout (title='Add Account', content=~{::#content})}"
|
||||||
|
>
|
||||||
|
<div id="content" class="container">
|
||||||
|
<h1>Remove Account</h1>
|
||||||
|
<p>
|
||||||
|
Are you sure you want to remove this account?
|
||||||
|
</p>
|
||||||
|
<form th:action="@{/exchanges/{eId}/removeAccount/{aId}(eId=${exchangeId}, aId=${accountId})}" th:method="post">
|
||||||
|
<a class="btn btn-secondary" th:href="@{/exchanges/{eId}/accounts(eId=${exchangeId})}">Cancel</a>
|
||||||
|
<button class="btn btn-danger" type="submit">Remove</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
|
@ -0,0 +1,35 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html
|
||||||
|
lang="en"
|
||||||
|
xmlns:th="http://www.thymeleaf.org"
|
||||||
|
xmlns:sec="http://www.thymeleaf.org/extras/spring-security"
|
||||||
|
>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>header</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<nav th:fragment="header" class="navbar navbar-expand-lg navbar-light bg-light">
|
||||||
|
<div class="container-fluid">
|
||||||
|
<a class="navbar-brand" href="/">Coyote Credit</a>
|
||||||
|
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
|
||||||
|
<span class="navbar-toggler-icon"></span>
|
||||||
|
</button>
|
||||||
|
<div class="collapse navbar-collapse" id="navbarSupportedContent">
|
||||||
|
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" th:href="@{/}">Home</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" th:href="@{/users/{id}(id=${#authentication.getPrincipal().getId()})}">My Profile</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<form class="d-flex" th:action="@{/logout}" th:method="post">
|
||||||
|
<button class="btn btn-outline-success" type="submit">Logout</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
</body>
|
|
@ -2,12 +2,10 @@
|
||||||
<html
|
<html
|
||||||
lang="en"
|
lang="en"
|
||||||
xmlns:th="http://www.thymeleaf.org"
|
xmlns:th="http://www.thymeleaf.org"
|
||||||
|
th:replace="~{layout/basic_page :: layout (title='Home', content=~{::#content})}"
|
||||||
>
|
>
|
||||||
<head>
|
|
||||||
<title>CC - Home</title>
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
<div id="content" class="container">
|
||||||
<h1>Welcome!</h1>
|
<h1>Welcome!</h1>
|
||||||
|
|
||||||
<h3>Your Accounts:</h3>
|
<h3>Your Accounts:</h3>
|
||||||
|
@ -19,8 +17,4 @@
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
</div>
|
||||||
<form th:action="@{/logout}" th:method="post">
|
|
||||||
<input type="submit" value="Logout">
|
|
||||||
</form>
|
|
||||||
</body>
|
|
|
@ -0,0 +1,39 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html
|
||||||
|
lang="en"
|
||||||
|
xmlns:th="http://www.thymeleaf.org"
|
||||||
|
xmlns:sec="http://www.thymeleaf.org/extras/spring-security"
|
||||||
|
th:fragment="layout (title, content)"
|
||||||
|
>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title th:text="${'Coyote Credit - ' + title}">Coyote Credit</title>
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="/static/css/style.css"/>
|
||||||
|
<link
|
||||||
|
href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css"
|
||||||
|
rel="stylesheet"
|
||||||
|
integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3"
|
||||||
|
crossorigin="anonymous"
|
||||||
|
>
|
||||||
|
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<header sec:authorize="isAuthenticated()">
|
||||||
|
<div th:replace="~{fragment/header :: header}"></div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="container-fluid">
|
||||||
|
<div th:replace="${content}" class="row">
|
||||||
|
<p>Placeholder content.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script
|
||||||
|
src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"
|
||||||
|
integrity="sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH9sENBO0LRn5q+8nbTov4+1p"
|
||||||
|
crossorigin="anonymous"
|
||||||
|
></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1,64 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html
|
||||||
|
lang="en"
|
||||||
|
xmlns:th="http://www.thymeleaf.org"
|
||||||
|
th:replace="~{layout/basic_page :: layout (title='Trade', content=~{::#content})}"
|
||||||
|
>
|
||||||
|
<div id="content" class="container">
|
||||||
|
<h1>Trade</h1>
|
||||||
|
|
||||||
|
<form id="tradeForm" th:action="@{/trade/{account}(account=${data.accountId()})}" method="post">
|
||||||
|
<input type="hidden" id="exchangeIdInput" th:value="${data.exchangeId()}"/>
|
||||||
|
<input type="hidden" id="accountIdInput" th:value="${data.accountId()}"/>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="sellTradeableSelect" class="form-label">Tradeable to Sell</label>
|
||||||
|
<select id="sellTradeableSelect" class="form-select">
|
||||||
|
<option selected hidden>Choose something to sell</option>
|
||||||
|
<option
|
||||||
|
th:each="t : ${data.tradeablesToSell()}"
|
||||||
|
th:value="${t.id()}"
|
||||||
|
th:text="${t.name() + ' (' + t.symbol() + ')'}"
|
||||||
|
th:data-priceusd="${t.marketPriceUsd()}"
|
||||||
|
th:data-type="${t.type()}"
|
||||||
|
th:data-balance="${data.accountBalances().get(t.symbol()).toPlainString()}"
|
||||||
|
th:disabled="${data.accountBalances().get(t.symbol()).signum() == 0 ? true : false}"
|
||||||
|
></option>
|
||||||
|
</select>
|
||||||
|
<div id="sellTradeableSelectText" class="form-text" hidden></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="sellValueInput" class="form-label">Value to Sell</label>
|
||||||
|
<input type="number" class="form-control" id="sellValueInput"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="buyTradeableSelect" class="form-label">Tradeable to Buy</label>
|
||||||
|
<select id="buyTradeableSelect" class="form-select">
|
||||||
|
<option value="" selected disabled hidden>Choose something to buy</option>
|
||||||
|
<option
|
||||||
|
th:each="t : ${data.tradeablesToBuy()}"
|
||||||
|
th:value="${t.id()}"
|
||||||
|
th:text="${t.name() + ' (' + t.symbol() + ')'}"
|
||||||
|
th:data-priceusd="${t.marketPriceUsd()}"
|
||||||
|
th:data-type="${t.type()}"
|
||||||
|
></option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="buyValueInput" class="form-label">Value to Buy</label>
|
||||||
|
<input type="number" class="form-control" id="buyValueInput"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<input type="hidden" name="_csrf" th:value="${_csrf.getToken()}"/>
|
||||||
|
<input type="hidden" name="type"/>
|
||||||
|
<input type="hidden" name="sellTradeableId"/>
|
||||||
|
<input type="hidden" name="buyTradeableId"/>
|
||||||
|
<input type="hidden" name="value"/>
|
||||||
|
|
||||||
|
<button id="submitButton" type="button" class="btn btn-primary">Submit</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<script src="/static/js/trade.js"></script>
|
||||||
|
</div>
|
|
@ -0,0 +1,18 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html
|
||||||
|
lang="en"
|
||||||
|
xmlns:th="http://www.thymeleaf.org"
|
||||||
|
th:replace="~{layout/basic_page :: layout (title='My Profile', content=~{::#content})}"
|
||||||
|
>
|
||||||
|
<div id="content" class="container">
|
||||||
|
<h1>My Profile</h1>
|
||||||
|
|
||||||
|
<table class="table">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">Username</th>
|
||||||
|
<td th:text="${user.username()}"></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
Loading…
Reference in New Issue