diff --git a/http-api/src/main/java/net/runelite/http/api/ge/GrandExchangeTrade.java b/http-api/src/main/java/net/runelite/http/api/ge/GrandExchangeTrade.java
index 17b6effb0f..85dccbc793 100644
--- a/http-api/src/main/java/net/runelite/http/api/ge/GrandExchangeTrade.java
+++ b/http-api/src/main/java/net/runelite/http/api/ge/GrandExchangeTrade.java
@@ -30,7 +30,10 @@ import lombok.Data;
public class GrandExchangeTrade
{
private boolean buy;
+ private boolean cancel;
private int itemId;
private int quantity;
+ private int total;
private int price;
+ private int offer;
}
diff --git a/http-service/src/main/java/net/runelite/http/service/ge/GrandExchangeController.java b/http-service/src/main/java/net/runelite/http/service/ge/GrandExchangeController.java
index ba291385cf..63555cbe7b 100644
--- a/http-service/src/main/java/net/runelite/http/service/ge/GrandExchangeController.java
+++ b/http-service/src/main/java/net/runelite/http/service/ge/GrandExchangeController.java
@@ -29,6 +29,7 @@ import java.util.Collection;
import java.util.stream.Collectors;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
+import net.runelite.http.api.RuneLiteAPI;
import net.runelite.http.api.ge.GrandExchangeTrade;
import net.runelite.http.service.account.AuthFilter;
import net.runelite.http.service.account.beans.SessionEntry;
@@ -58,14 +59,23 @@ public class GrandExchangeController
@PostMapping
public void submit(HttpServletRequest request, HttpServletResponse response, @RequestBody GrandExchangeTrade grandExchangeTrade) throws IOException
{
- SessionEntry session = authFilter.handle(request, response);
-
- if (session == null)
+ SessionEntry session = null;
+ if (request.getHeader(RuneLiteAPI.RUNELITE_AUTH) != null)
{
- return;
+ session = authFilter.handle(request, response);
+ if (session == null)
+ {
+ // error is set here on the response, so we shouldn't continue
+ return;
+ }
}
+ Integer userId = session == null ? null : session.getUser();
- grandExchangeService.add(session.getUser(), grandExchangeTrade);
+ // We don't keep track of pending trades in the web UI, so only add cancelled or completed trades
+ if (userId != null && (grandExchangeTrade.isCancel() || grandExchangeTrade.getQuantity() == grandExchangeTrade.getTotal()))
+ {
+ grandExchangeService.add(userId, grandExchangeTrade);
+ }
}
@GetMapping
diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/grandexchange/GrandExchangePlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/grandexchange/GrandExchangePlugin.java
index e43cf40475..46dbb93c64 100644
--- a/runelite-client/src/main/java/net/runelite/client/plugins/grandexchange/GrandExchangePlugin.java
+++ b/runelite-client/src/main/java/net/runelite/client/plugins/grandexchange/GrandExchangePlugin.java
@@ -371,39 +371,88 @@ public class GrandExchangePlugin extends Plugin
BufferedImage itemImage = itemManager.getImage(offer.getItemId(), offer.getTotalQuantity(), shouldStack);
SwingUtilities.invokeLater(() -> panel.getOffersPanel().updateOffer(offerItem, itemImage, offer, slot));
- submitTrades(slot, offer);
+ submitTrade(slot, offer);
updateConfig(slot, offer);
}
- private void submitTrades(int slot, GrandExchangeOffer offer)
+ @VisibleForTesting
+ void submitTrade(int slot, GrandExchangeOffer offer)
{
- if (offer.getState() != GrandExchangeOfferState.BOUGHT && offer.getState() != GrandExchangeOfferState.SOLD &&
- offer.getState() != GrandExchangeOfferState.CANCELLED_BUY && offer.getState() != GrandExchangeOfferState.CANCELLED_SELL)
- {
- return;
- }
+ GrandExchangeOfferState state = offer.getState();
- // Cancelled offers may have been cancelled before buying/selling any items
- if (offer.getQuantitySold() == 0)
+ if (state != GrandExchangeOfferState.CANCELLED_BUY && state != GrandExchangeOfferState.CANCELLED_SELL && state != GrandExchangeOfferState.BUYING && state != GrandExchangeOfferState.SELLING)
{
return;
}
SavedOffer savedOffer = getOffer(slot);
- if (!shouldUpdate(savedOffer, offer))
+ if (savedOffer == null && (state == GrandExchangeOfferState.BUYING || state == GrandExchangeOfferState.SELLING) && offer.getQuantitySold() == 0)
+ {
+ // new offer
+ GrandExchangeTrade grandExchangeTrade = new GrandExchangeTrade();
+ grandExchangeTrade.setBuy(state == GrandExchangeOfferState.BUYING);
+ grandExchangeTrade.setItemId(offer.getItemId());
+ grandExchangeTrade.setQuantity(0);
+ grandExchangeTrade.setTotal(offer.getTotalQuantity());
+ grandExchangeTrade.setPrice(0);
+ grandExchangeTrade.setOffer(offer.getPrice());
+
+ log.debug("Submitting new trade: {}", grandExchangeTrade);
+ grandExchangeClient.submit(grandExchangeTrade);
+ return;
+ }
+
+ if (savedOffer == null || savedOffer.getItemId() != offer.getItemId() || savedOffer.getPrice() != offer.getPrice() || savedOffer.getTotalQuantity() != offer.getTotalQuantity())
+ {
+ // desync
+ return;
+ }
+
+ if (savedOffer.getState() == offer.getState() && savedOffer.getQuantitySold() == offer.getQuantitySold())
+ {
+ // no change
+ return;
+ }
+
+ if (state == GrandExchangeOfferState.CANCELLED_BUY || state == GrandExchangeOfferState.CANCELLED_SELL)
+ {
+ GrandExchangeTrade grandExchangeTrade = new GrandExchangeTrade();
+ grandExchangeTrade.setBuy(state == GrandExchangeOfferState.CANCELLED_BUY);
+ grandExchangeTrade.setCancel(true);
+ grandExchangeTrade.setItemId(offer.getItemId());
+ grandExchangeTrade.setQuantity(offer.getQuantitySold());
+ grandExchangeTrade.setTotal(offer.getTotalQuantity());
+ grandExchangeTrade.setPrice(offer.getQuantitySold() > 0 ? offer.getSpent() / offer.getQuantitySold() : 0);
+ grandExchangeTrade.setOffer(offer.getPrice());
+
+ log.debug("Submitting cancelled: {}", grandExchangeTrade);
+ grandExchangeClient.submit(grandExchangeTrade);
+ return;
+ }
+
+ final int qty = offer.getQuantitySold() - savedOffer.getQuantitySold();
+ if (qty <= 0)
{
return;
}
- // getPrice() is the price of the offer, not necessarily what the item bought at
- int priceEach = offer.getSpent() / offer.getQuantitySold();
+ // offer.getPrice() is the price of the offer, not necessarily what the item bought at, so we compute it
+ // based on how much was spent & the qty
+ final int dspent = offer.getSpent() - savedOffer.getSpent();
+ final int price = dspent / qty;
+ if (price <= 0)
+ {
+ return;
+ }
GrandExchangeTrade grandExchangeTrade = new GrandExchangeTrade();
- grandExchangeTrade.setBuy(offer.getState() == GrandExchangeOfferState.BOUGHT || offer.getState() == GrandExchangeOfferState.CANCELLED_BUY);
+ grandExchangeTrade.setBuy(state == GrandExchangeOfferState.BUYING);
grandExchangeTrade.setItemId(offer.getItemId());
- grandExchangeTrade.setQuantity(offer.getQuantitySold());
- grandExchangeTrade.setPrice(priceEach);
+ grandExchangeTrade.setQuantity(qty);
+ grandExchangeTrade.setTotal(offer.getTotalQuantity());
+ grandExchangeTrade.setPrice(price);
+ grandExchangeTrade.setOffer(offer.getPrice());
log.debug("Submitting trade: {}", grandExchangeTrade);
grandExchangeClient.submit(grandExchangeTrade);
@@ -430,17 +479,6 @@ public class GrandExchangePlugin extends Plugin
}
}
- private boolean shouldUpdate(SavedOffer savedOffer, GrandExchangeOffer grandExchangeOffer)
- {
- if (savedOffer == null)
- {
- return false;
- }
-
- // Only update offer if state has changed
- return savedOffer.getState() != grandExchangeOffer.getState();
- }
-
@Subscribe
public void onChatMessage(ChatMessage event)
{
diff --git a/runelite-client/src/test/java/net/runelite/client/plugins/grandexchange/GrandExchangePluginTest.java b/runelite-client/src/test/java/net/runelite/client/plugins/grandexchange/GrandExchangePluginTest.java
index dc16d25dfc..d95df70528 100644
--- a/runelite-client/src/test/java/net/runelite/client/plugins/grandexchange/GrandExchangePluginTest.java
+++ b/runelite-client/src/test/java/net/runelite/client/plugins/grandexchange/GrandExchangePluginTest.java
@@ -24,14 +24,100 @@
*/
package net.runelite.client.plugins.grandexchange;
+import com.google.inject.Guice;
+import com.google.inject.testing.fieldbinder.Bind;
+import com.google.inject.testing.fieldbinder.BoundFieldModule;
import java.util.Arrays;
import java.util.List;
+import java.util.concurrent.ScheduledExecutorService;
+import javax.inject.Inject;
+import net.runelite.api.Client;
+import net.runelite.api.GrandExchangeOffer;
+import net.runelite.api.GrandExchangeOfferState;
+import net.runelite.api.ItemID;
+import net.runelite.client.Notifier;
+import net.runelite.client.account.SessionManager;
+import net.runelite.client.config.ConfigManager;
+import net.runelite.client.config.RuneLiteConfig;
+import net.runelite.client.game.ItemManager;
+import net.runelite.client.input.KeyManager;
+import net.runelite.client.input.MouseManager;
import static net.runelite.client.plugins.grandexchange.GrandExchangePlugin.findFuzzyIndices;
+import static net.runelite.http.api.RuneLiteAPI.GSON;
+import net.runelite.http.api.ge.GrandExchangeClient;
+import net.runelite.http.api.ge.GrandExchangeTrade;
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import org.junit.Before;
import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import static org.mockito.ArgumentMatchers.any;
+import org.mockito.Mock;
+import static org.mockito.Mockito.lenient;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import org.mockito.junit.MockitoJUnitRunner;
+@RunWith(MockitoJUnitRunner.class)
public class GrandExchangePluginTest
{
+ @Inject
+ private GrandExchangePlugin grandExchangePlugin;
+
+ @Mock
+ @Bind
+ private GrandExchangeConfig grandExchangeConfig;
+
+ @Mock
+ @Bind
+ private Notifier notifier;
+
+ @Mock
+ @Bind
+ private SessionManager sessionManager;
+
+ @Mock
+ @Bind
+ private ConfigManager configManager;
+
+ @Mock
+ @Bind
+ private ItemManager itemManager;
+
+ @Mock
+ @Bind
+ private KeyManager keyManager;
+
+ @Mock
+ @Bind
+ private MouseManager mouseManager;
+
+ @Mock
+ @Bind
+ private ScheduledExecutorService scheduledExecutorService;
+
+ @Mock
+ @Bind
+ private GrandExchangeClient grandExchangeClient;
+
+ @Mock
+ @Bind
+ private Client client;
+
+ @Mock
+ @Bind
+ private RuneLiteConfig runeLiteConfig;
+
+ @Before
+ public void setUp()
+ {
+ Guice.createInjector(BoundFieldModule.of(this)).injectMembers(this);
+ when(client.getUsername()).thenReturn("adam");
+ }
+
@Test
public void testFindFuzzyIndices()
{
@@ -39,4 +125,94 @@ public class GrandExchangePluginTest
// robe bottom
assertEquals(Arrays.asList(11, 12, 15), fuzzyIndices);
}
+
+ @Test
+ public void testSubmitTrade()
+ {
+ SavedOffer savedOffer = new SavedOffer();
+ savedOffer.setItemId(ItemID.ABYSSAL_WHIP);
+ savedOffer.setQuantitySold(1);
+ savedOffer.setTotalQuantity(10);
+ savedOffer.setPrice(1000);
+ savedOffer.setSpent(25);
+ savedOffer.setState(GrandExchangeOfferState.BUYING);
+ when(configManager.getConfiguration("geoffer.adam", "0")).thenReturn(GSON.toJson(savedOffer));
+
+ // buy 2 @ 10/ea
+ GrandExchangeOffer grandExchangeOffer = mock(GrandExchangeOffer.class);
+ when(grandExchangeOffer.getQuantitySold()).thenReturn(1 + 2);
+ when(grandExchangeOffer.getItemId()).thenReturn(ItemID.ABYSSAL_WHIP);
+ when(grandExchangeOffer.getTotalQuantity()).thenReturn(10);
+ when(grandExchangeOffer.getPrice()).thenReturn(1000);
+ when(grandExchangeOffer.getSpent()).thenReturn(25 + 10 * 2);
+ when(grandExchangeOffer.getState()).thenReturn(GrandExchangeOfferState.BUYING);
+ grandExchangePlugin.submitTrade(0, grandExchangeOffer);
+
+ ArgumentCaptor captor = ArgumentCaptor.forClass(GrandExchangeTrade.class);
+ verify(grandExchangeClient).submit(captor.capture());
+
+ GrandExchangeTrade trade = captor.getValue();
+ assertTrue(trade.isBuy());
+ assertEquals(ItemID.ABYSSAL_WHIP, trade.getItemId());
+ assertEquals(2, trade.getQuantity());
+ assertEquals(10, trade.getTotal());
+ assertEquals(10, trade.getPrice());
+ }
+
+ @Test
+ public void testDuplicateTrade()
+ {
+ SavedOffer savedOffer = new SavedOffer();
+ savedOffer.setItemId(ItemID.ABYSSAL_WHIP);
+ savedOffer.setQuantitySold(1);
+ savedOffer.setTotalQuantity(10);
+ savedOffer.setPrice(1000);
+ savedOffer.setSpent(25);
+ savedOffer.setState(GrandExchangeOfferState.BUYING);
+ when(configManager.getConfiguration("geoffer.adam", "0")).thenReturn(GSON.toJson(savedOffer));
+
+ GrandExchangeOffer grandExchangeOffer = mock(GrandExchangeOffer.class);
+ when(grandExchangeOffer.getQuantitySold()).thenReturn(1);
+ when(grandExchangeOffer.getItemId()).thenReturn(ItemID.ABYSSAL_WHIP);
+ when(grandExchangeOffer.getTotalQuantity()).thenReturn(10);
+ when(grandExchangeOffer.getPrice()).thenReturn(1000);
+ lenient().when(grandExchangeOffer.getSpent()).thenReturn(25);
+ when(grandExchangeOffer.getState()).thenReturn(GrandExchangeOfferState.BUYING);
+ grandExchangePlugin.submitTrade(0, grandExchangeOffer);
+
+ verify(grandExchangeClient, never()).submit(any(GrandExchangeTrade.class));
+ }
+
+ @Test
+ public void testCancelTrade()
+ {
+ SavedOffer savedOffer = new SavedOffer();
+ savedOffer.setItemId(ItemID.ABYSSAL_WHIP);
+ savedOffer.setQuantitySold(1);
+ savedOffer.setTotalQuantity(10);
+ savedOffer.setPrice(1000);
+ savedOffer.setSpent(25);
+ savedOffer.setState(GrandExchangeOfferState.BUYING);
+ when(configManager.getConfiguration("geoffer.adam", "0")).thenReturn(GSON.toJson(savedOffer));
+
+ GrandExchangeOffer grandExchangeOffer = mock(GrandExchangeOffer.class);
+ when(grandExchangeOffer.getQuantitySold()).thenReturn(1);
+ when(grandExchangeOffer.getItemId()).thenReturn(ItemID.ABYSSAL_WHIP);
+ when(grandExchangeOffer.getTotalQuantity()).thenReturn(10);
+ when(grandExchangeOffer.getPrice()).thenReturn(1000);
+ when(grandExchangeOffer.getSpent()).thenReturn(25);
+ when(grandExchangeOffer.getState()).thenReturn(GrandExchangeOfferState.CANCELLED_BUY);
+ grandExchangePlugin.submitTrade(0, grandExchangeOffer);
+
+ ArgumentCaptor captor = ArgumentCaptor.forClass(GrandExchangeTrade.class);
+ verify(grandExchangeClient).submit(captor.capture());
+
+ GrandExchangeTrade trade = captor.getValue();
+ assertTrue(trade.isBuy());
+ assertTrue(trade.isCancel());
+ assertEquals(ItemID.ABYSSAL_WHIP, trade.getItemId());
+ assertEquals(1, trade.getQuantity());
+ assertEquals(10, trade.getTotal());
+ assertEquals(25, trade.getPrice());
+ }
}
\ No newline at end of file