Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
7ee6401
Add functionality to copy augments to compared items, like anointment…
vaisest Apr 17, 2026
51df964
Port jewel comparison tooltip socket sorting from pob1
vaisest Apr 18, 2026
17cec65
Fix item comparison augment copy not treating spell weapons as weapon…
vaisest Apr 18, 2026
c1ec658
port compatible trader tool changes from pob1
vaisest Apr 18, 2026
fa9bd6b
Fix corrupted mods being fractured mods in trade mod generation
vaisest Apr 19, 2026
b2f9ff4
Fix radius jewel weight generation
vaisest Apr 19, 2026
52fc62d
Regenerate QueryMods.lua
vaisest Apr 19, 2026
0d2a6a9
convert trade tool mod weight generation to use tradeHash
vaisest Apr 19, 2026
072aa2b
wip: change poesessid to bearer token
vaisest Apr 19, 2026
083f4fe
Cleanup: remove extra logout button and fix poeapi comments
vaisest Apr 20, 2026
d3f24c9
Fix trader crash when rate limited on startup
vaisest Apr 20, 2026
58ba609
Use https://poe.ninja/poe2/api/economy/exchange/current/overview for …
vaisest Apr 20, 2026
4b685b5
Fix poe.ninja tests
vaisest Apr 20, 2026
9f48446
Fix trader section anchor
vaisest Apr 20, 2026
68bc973
Adjust price scaling factor due to things being in divs (still an abi…
vaisest Apr 20, 2026
6bd15dd
Clarify price options and rate limit waits, and use Retry-After for r…
vaisest Apr 20, 2026
45ee686
rate limiting pls work
vaisest Apr 20, 2026
b2f9b4f
Fix perfect essences not appearing in generated weights, and regenera…
vaisest Apr 20, 2026
339488c
Fix tradehashes for radius jewels
vaisest Apr 20, 2026
49fba18
Improve rate limit countdown to prevent simplegraphic suspension prob…
vaisest Apr 21, 2026
7f3c57d
Fix debug print causing crash, and remove extra debug print
vaisest Apr 21, 2026
86669c9
disable wiping trader controls to fix crash when it is closed and a s…
vaisest Apr 21, 2026
f5f7b48
Add note about doing weird filter requirements (e.g. adorned)
vaisest Apr 21, 2026
223fd6d
remove whisper for instant buyout items
vaisest Apr 21, 2026
e122020
make cspell happy
vaisest Apr 21, 2026
674c6b6
Fix database radius jewels being nonfunctional
vaisest Apr 22, 2026
64c38e6
Fix currency conversion button not being updated after reopening trad…
vaisest Apr 23, 2026
b89b567
Avoid useless search in "search for" button
vaisest Apr 23, 2026
ddb73c8
fix api error on invalid token
vaisest Apr 24, 2026
628ab84
disable reuseaddr
vaisest Apr 25, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion runtime/lua/socket.lua
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ function _M.bind(host, port, backlog)
sock, err = socket.tcp6()
end
if not sock then return nil, err end
sock:setoption("reuseaddr", true)
-- sock:setoption("reuseaddr", true)
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seemed to make PoB think it's listening on the first port when another program was already listening, which meant the key goes to the wrong program

Copy link
Copy Markdown
Member

@Wires77 Wires77 Apr 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, I thought I tested this specifically since it's a security issue, but I can check again. Regardless, the right fix for this would likely be to use the option in LaunchServer.lua instead: server:setoption(option [, value]) from https://lunarmodules.github.io/luasocket/tcp.html

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tested again, you can see PoB change port to an unused one if a server is running elsewhere:
image

Copy link
Copy Markdown
Member

@Wires77 Wires77 Apr 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've figured out the root issues here:

  • We're only allowed to redirect users to http://localhost from GGG's server due to their implementation restrictions
  • Binding to port 0.0.0.0 (or *) with LuaSocket will still connect because that address is not conflicting with 127.0.0.1 (apparently)
  • Binding with the default method will choose an IPv6 address if the IPv4 one is busy, but with the same port

So in this case:

  • Start a server with python3 -m http.server -b 127.0.0.1 49082
  • LuaSocket connects to :: as IPv6
  • Redirect to http://localhost reaches the python server instead because localhost resolves to the IPv4 address and server

Forcing IPv4 revealed the other issue, where 0.0.0.0 doesn't conflict with 127.0.0.1 when binding to the port (at least on Windows). This picture reveals some of the results. Port 49083 was the only one that works properly.
image

Here's the fix to go into LaunchServer.lua:

local luaSocket = require("socket")
local server = luaSocket.tcp4()
local function bindSocket()
	local res, err
	server:setoption("reuseaddr", true)
	res, err = server:bind("localhost", 49082) or server:bind("localhost", 49083) or server:bind("localhost", 49084)
	if not res then
		server:close()
	else
		res, err = server:listen(1)
		if not res then
			server:close()
		else
			return server
		end
	end
	return nil, err
end
assert(bindSocket())

res, err = sock:bind(alt.addr, port)
if not res then
sock:close()
Expand Down
64 changes: 34 additions & 30 deletions spec/System/TestTradeQueryCurrency_spec.lua
Original file line number Diff line number Diff line change
@@ -1,54 +1,58 @@
describe("TradeQuery Currency Conversion", function()
local mock_tradeQuery = new("TradeQuery", { itemsTab = {} })
local mock_tradeQuery

-- test case for commit: "Skip callback on errors to prevent incomplete conversions"
describe("FetchCurrencyConversionTable", function()
-- Pass: Callback not called on error
-- Fail: Callback called, indicating partial data risk
it("skips callback on error", function()
local orig_launch = launch
local spy = { called = false }
launch = {
DownloadPage = function(url, callback, opts)
callback(nil, "test error")
end
}
mock_tradeQuery:FetchCurrencyConversionTable(function()
spy.called = true
end)
launch = orig_launch
assert.is_false(spy.called)
end)
before_each(function()
mock_tradeQuery = new("TradeQuery", { itemsTab = {} })
end)

describe("ConvertCurrencyToChaos", function()
-- Pass: Ceils amount to integer (e.g., 4.9 -> 5)
-- Fail: Wrong value or nil, indicating broken rounding/baseline logic, causing inaccurate chaos totals
describe("ConvertCurrencyToDivs", function()
-- Pass: Calculates price in divs
-- Fail: Wrong value or nil, indicating broken rounding/baseline logic
it("handles chaos currency", function()
mock_tradeQuery.pbCurrencyConversion = { league = { chaos = 1 } }
mock_tradeQuery.pbCurrencyConversion = { league = { chaos = 0.1 } }
mock_tradeQuery.pbLeague = "league"
local result = mock_tradeQuery:ConvertCurrencyToChaos("chaos", 4.9)
assert.are.equal(result, 5)
local result = mock_tradeQuery:ConvertCurrencyToDivs("chaos", 5)
assert.are.equal(result, 0.5)
end)

-- Pass: Returns nil without crash
-- Fail: Crashes or wrong value, indicating unhandled currencies, corrupting price conversions
it("returns nil for unmapped", function()
local result = mock_tradeQuery:ConvertCurrencyToChaos("exotic", 10)
local result = mock_tradeQuery:ConvertCurrencyToDivs("exotic", 10)
assert.is_nil(result)
end)
end)

describe("PriceBuilderProcessPoENinjaResponse", function()
-- Pass: Processes without error, restoring map
-- Pass: Processes without error, restoring map while adding a notice
-- Fail: Corrupts map or crashes, indicating fragile API response handling, breaking future conversions
it("handles unmapped currency", function()
it("handles empty response", function()
local orig_conv = mock_tradeQuery.currencyConversionTradeMap
mock_tradeQuery.currencyConversionTradeMap = { div = "id" }
local resp = { exotic = 10 }
mock_tradeQuery:PriceBuilderProcessPoENinjaResponse(resp)
mock_tradeQuery.pbLeague = "league"
mock_tradeQuery.pbCurrencyConversion = { league = {} }
mock_tradeQuery.controls.pbNotice = { label = "" }
local resp = { lines = { }}
mock_tradeQuery:PriceBuilderProcessPoENinjaResponse(resp.lines)
-- No crash expected
assert.is_true(true)
assert.is_true(mock_tradeQuery.controls.pbNotice.label == "No currencies received from PoE Ninja")
mock_tradeQuery.currencyConversionTradeMap = orig_conv
end)

-- Pass: Processes without error, restoring map while adding a notice
-- Fail: Corrupts map or crashes, indicating fragile API response handling, breaking future conversions
it("handles empty response", function()
local orig_conv = mock_tradeQuery.currencyConversionTradeMap
mock_tradeQuery.currencyConversionTradeMap = { div = "id" }
mock_tradeQuery.pbLeague = "league"
mock_tradeQuery.pbCurrencyConversion = { league = {} }
mock_tradeQuery.controls.pbNotice = { label = "" }
local resp = { lines = { { malformedLine = "lol"} }}
mock_tradeQuery:PriceBuilderProcessPoENinjaResponse(resp.lines)
-- No crash expected
assert.is_true(true)
assert.is_true(mock_tradeQuery.controls.pbNotice.label == "Currencies not updated: malformed PoE Ninja response")
mock_tradeQuery.currencyConversionTradeMap = orig_conv
end)
end)
Expand Down
36 changes: 36 additions & 0 deletions spec/System/TestTradeQueryRequests_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,42 @@ describe("TradeQueryRequests", function()
launch = orig_launch
end)

-- Pass: Does not crash on 401, and passes error message
-- Fail: Crash, or returned error is wrong
it("does not crash on 401", function()
local json = '"{"error":"invalid_token","error_description":"The access token provided is invalid or has expired"}"'
local header = [[HTTP/1.1 401 Unauthorized
Date: Fri, 24 Apr 2026 07:30:38 GMT
Content-Type: application/json
Transfer-Encoding: chunked
Connection: keep-alive
Server: cloudflare
WWW-Authenticate: Bearer realm="pathofexile:production", error="invalid_token", error_description="The access token provided is invalid or has expired"
Cache-Control: no-store
Strict-Transport-Security: max-age=63115200; includeSubDomains; preload]]
local orig_launch = launch
launch = {
DownloadPage = function(url, onComplete, opts)
onComplete({ body = json, header = header }, nil)
end
}
table.insert(requests.requestQueue.search, {
url = "test",
callback = function(body, msg)
assert.are.equal(body, json)
assert.truthy(msg:find("Response code: 401"))
end,
retryTime = nil
})
local function mock_next_time(self, policy, time)
return time - 1
end
mock_limiter.NextRequestTime = mock_next_time
requests:ProcessQueue()
assert.are.equal(#requests.requestQueue.search, 0)
launch = orig_launch
end)

-- Pass: Retries with increasing backoff up to cap, preventing infinite loops
-- Fail: No backoff or uncapped, indicating retry bug, risking API bans
it("retries on 429 with exponential backoff", function()
Expand Down
69 changes: 38 additions & 31 deletions src/Classes/ImportTab.lua
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,10 @@ local ImportTabClass = newClass("ImportTab", "ControlHost", "Control", function(
self.Control()

self.build = build
self.api = new("PoEAPI", main.lastToken, main.lastRefreshToken, main.tokenExpiry)
if not main.api then
main.api = new("PoEAPI", main.lastToken, main.lastRefreshToken, main.tokenExpiry)
end


self.charImportMode = "AUTHENTICATION"
self.charImportStatus = colorCodes.WARNING.."Not authenticated"
Expand All @@ -31,32 +34,32 @@ local ImportTabClass = newClass("ImportTab", "ControlHost", "Control", function(

self.controls.logoutApiButton = new("ButtonControl", {"TOPLEFT",self.controls.charImportStatusLabel,"TOPRIGHT"}, {4, 0, 180, 16}, "^7Logout from Path of Exile API", function()
main.lastToken = nil
self.api.authToken = nil
main.api.authToken = nil
main.lastRefreshToken = nil
self.api.refreshToken = nil
main.api.refreshToken = nil
main.tokenExpiry = nil
self.api.tokenExpiry = nil
main.api.tokenExpiry = nil
main:SaveSettings()
self.charImportMode = "AUTHENTICATION"
self.charImportStatus = colorCodes.WARNING.."Not authenticated"
end)
self.controls.logoutApiButton.shown = function()
return (self.charImportMode == "SELECTCHAR" or self.charImportMode == "GETACCOUNTNAME") and self.api.authToken ~= nil
return (self.charImportMode == "SELECTCHAR" or self.charImportMode == "GETACCOUNTNAME") and main.api.authToken ~= nil
end

self.controls.characterImportAnchor = new("Control", {"TOPLEFT",self.controls.sectionCharImport,"TOPLEFT"}, {6, 40, 200, 16})
self.controls.sectionCharImport.height = function() return self.charImportMode == "AUTHENTICATION" and 60 or 200 end

-- Stage: Authenticate
self.controls.authenticateButton = new("ButtonControl", {"TOPLEFT",self.controls.characterImportAnchor,"TOPLEFT"}, {0, 0, 200, 16}, "^7Authorize with Path of Exile", function()
self.api:FetchAuthToken(function()
if self.api.authToken then
main.api:FetchAuthToken(function()
if main.api.authToken then
self.charImportMode = "GETACCOUNTNAME"
self.charImportStatus = "Authenticated"

main.lastToken = self.api.authToken
main.lastRefreshToken = self.api.refreshToken
main.tokenExpiry = self.api.tokenExpiry
main.lastToken = main.api.authToken
main.lastRefreshToken = main.api.refreshToken
main.tokenExpiry = main.api.tokenExpiry
main:SaveSettings()
self:DownloadCharacterList()
else
Expand Down Expand Up @@ -332,26 +335,30 @@ local ImportTabClass = newClass("ImportTab", "ControlHost", "Control", function(
end

-- validate the status of the api the first time
self.api:ValidateAuth(function(valid, updateSettings)
if valid then
if self.charImportMode == "AUTHENTICATION" then
self.charImportMode = "GETACCOUNTNAME"
self.charImportStatus = "Authenticated"
end
if updateSettings then
self:SaveApiSettings()
end
else
self.charImportMode = "AUTHENTICATION"
self.charImportStatus = colorCodes.WARNING.."Not authenticated"
end
end)
self:RefreshAuthStatus()
end)

function ImportTabClass:RefreshAuthStatus()
main.api:ValidateAuth(function(valid, updateSettings)
if valid then
if self.charImportMode == "AUTHENTICATION" then
self.charImportMode = "GETACCOUNTNAME"
self.charImportStatus = "Authenticated"
end
if updateSettings then
self:SaveApiSettings()
end
else
self.charImportMode = "AUTHENTICATION"
self.charImportStatus = colorCodes.WARNING.."Not authenticated"
end
end)
end

function ImportTabClass:SaveApiSettings()
main.lastToken = self.api.authToken
main.lastRefreshToken = self.api.refreshToken
main.tokenExpiry = self.api.tokenExpiry
main.lastToken = main.api.authToken
main.lastRefreshToken = main.api.refreshToken
main.tokenExpiry = main.api.tokenExpiry
main:SaveSettings()
end

Expand Down Expand Up @@ -405,11 +412,11 @@ function ImportTabClass:DownloadCharacterList()
self.charImportMode = "DOWNLOADCHARLIST"
self.charImportStatus = "Retrieving character list..."
local realm = realmList[self.controls.accountRealm.selIndex]
self.api:DownloadCharacterList(realm.realmCode, function(body, errMsg, updateSettings)
main.api:DownloadCharacterList(realm.realmCode, function(body, errMsg, updateSettings)
if updateSettings then
self:SaveApiSettings()
end
if errMsg == self.api.ERROR_NO_AUTH then
if errMsg == main.api.ERROR_NO_AUTH then
self.charImportMode = "AUTHENTICATION"
self.charImportStatus = colorCodes.WARNING.."Not authenticated"
return
Expand Down Expand Up @@ -530,13 +537,13 @@ function ImportTabClass:DownloadCharacter(callback)
local realm = realmList[self.controls.accountRealm.selIndex]
local charSelect = self.controls.charSelect
local charData = charSelect.list[charSelect.selIndex].char
self.api:DownloadCharacter(realm.realmCode, charData.name, function(body, errMsg, updateSettings)
main.api:DownloadCharacter(realm.realmCode, charData.name, function(body, errMsg, updateSettings)
self.charImportMode = "SELECTCHAR"
if updateSettings then
self:SaveApiSettings()
end
if errMsg then
if errMsg == self.api.ERROR_NO_AUTH then
if errMsg == main.api.ERROR_NO_AUTH then
self.charImportMode = "AUTHENTICATION"
self.charImportStatus = colorCodes.WARNING.."Not authenticated"
return
Expand Down
Loading
Loading