[econ] fix room owner key purchase

This commit is contained in:
Devin Zuczek
2026-10-22 11:55:24 -04:00
parent 337367da52
commit 3cf4b4029e
2 changed files with 19 additions and 7 deletions
+11 -5
View File
@@ -6517,8 +6517,10 @@ const app = new Hono<App>({ strict: false })
'Looks the key up by `RoomKeyId`, confirms the client’s `RequestedPrice` matches its', 'Looks the key up by `RoomKeyId`, confirms the client’s `RequestedPrice` matches its',
'stored `Price`, debits the buyer that price in RecCenterTokens, pays it to the', 'stored `Price`, debits the buyer that price in RecCenterTokens, pays it to the',
'room’s OWNER (its `CreatorAccountId`), and records the buyer as holding the key', 'room’s OWNER (its `CreatorAccountId`), and records the buyer as holding the key',
'(`room_key_player`). A free key, and an owner buying their own room’s key, move no', '(`room_key_player`). A free key moves no tokens, and nor does a key bought by the',
'tokens. A GET because that is how the client sends it.', 'room’s creator or a CO-OWNER: they get their own room’s keys free, and the client',
'asks them `RequestedPrice=0` (the stored price is accepted too). A GET because that',
'is how the client sends it.',
'', '',
'Answers the client’s `RoomKeyPurchaseResponseDTO`: `RoomKeyResponse` is the', 'Answers the client’s `RoomKeyPurchaseResponseDTO`: `RoomKeyResponse` is the',
'`{ Status, RoomKey }` the create answers in, and `BalanceUpdateResponse` carries one', '`{ Status, RoomKey }` the create answers in, and `BalanceUpdateResponse` carries one',
@@ -6588,17 +6590,21 @@ const app = new Hono<App>({ strict: false })
if (key === null || key.PurchaseCurrencyId !== null) { if (key === null || key.PurchaseCurrencyId !== null) {
return answer(UpdateResponse.NoItemAvailable) return answer(UpdateResponse.NoItemAvailable)
} }
if (requestedPrice !== key.Price) return answer(UpdateResponse.RequestedPriceDoesNotMatch)
const ownerId = await getRoomOwnerId(c.env.DB, key.RoomId) const ownerId = await getRoomOwnerId(c.env.DB, key.RoomId)
if (ownerId === null) return answer(UpdateResponse.NoItemAvailable) if (ownerId === null) return answer(UpdateResponse.NoItemAvailable)
// The room's creator and co-owners get their own room's keys free — the client asks
// them `RequestedPrice=0` — and are never charged, whatever price they name.
const canManage = (await canManageRoomById(c.env.DB, key.RoomId, id)) === true
if (requestedPrice !== key.Price && !(canManage && requestedPrice === 0)) {
return answer(UpdateResponse.RequestedPriceDoesNotMatch)
}
// Claim the key BEFORE charging: the insert is atomic, so of two concurrent buys only // Claim the key BEFORE charging: the insert is atomic, so of two concurrent buys only
// one gets past here and only one is charged. A buyer who cannot pay gives it back. // one gets past here and only one is charged. A buyer who cannot pay gives it back.
if (!(await claimRoomKey(c.env.DB, id, key.RoomKeyId))) { if (!(await claimRoomKey(c.env.DB, id, key.RoomKeyId))) {
return answer(UpdateResponse.AlreadyOwned) return answer(UpdateResponse.AlreadyOwned)
} }
// The owner paying themselves would be a debit and a credit of the same tokens. if (key.Price > 0 && !canManage) {
if (key.Price > 0 && ownerId !== id) {
if (!(await spendCurrency(c.env.DB, id, currencyType, key.Price, startingTokens))) { if (!(await spendCurrency(c.env.DB, id, currencyType, key.Price, startingTokens))) {
await releaseRoomKey(c.env.DB, id, key.RoomKeyId) await releaseRoomKey(c.env.DB, id, key.RoomKeyId)
return answer(UpdateResponse.NotEnoughCredit) return answer(UpdateResponse.NotEnoughCredit)
+8 -2
View File
@@ -3054,7 +3054,8 @@ describe('econ endpoints', () => {
expect(await (await buy('7301', key.RoomKeyId, 300)).json()).toEqual(refused(3, key, 200)) expect(await (await buy('7301', key.RoomKeyId, 300)).json()).toEqual(refused(3, key, 200))
expect([await tokens(1), await tokens(7301)]).toEqual([1300, 200]) expect([await tokens(1), await tokens(7301)]).toEqual([1300, 200])
// A free key moves nothing; nor does the owner buying their own room's key. // A free key moves nothing; nor does a room manager buying their own room's key —
// the owner at the stored price, or a co-owner at the 0 the client asks them.
const gratis = (await (await buy('7302', free.RoomKeyId)).json()) as { const gratis = (await (await buy('7302', free.RoomKeyId)).json()) as {
RoomKeyResponse: { Status: number } RoomKeyResponse: { Status: number }
BalanceUpdateResponse: { Balance: number } BalanceUpdateResponse: { Balance: number }
@@ -3064,7 +3065,12 @@ describe('econ endpoints', () => {
const own = (await (await buy('1', key.RoomKeyId, 300)).json()) as typeof gratis const own = (await (await buy('1', key.RoomKeyId, 300)).json()) as typeof gratis
expect(own.RoomKeyResponse.Status).toBe(0) expect(own.RoomKeyResponse.Status).toBe(0)
expect(own.BalanceUpdateResponse.Balance).toBe(1300) expect(own.BalanceUpdateResponse.Balance).toBe(1300)
expect(await holders(key.RoomKeyId)).toEqual([1, 7301]) await setTokens(2, 50)
const coOwner = (await (await buy('2', key.RoomKeyId, 0)).json()) as typeof gratis
expect(coOwner.RoomKeyResponse.Status).toBe(0)
expect(coOwner.BalanceUpdateResponse.Balance).toBe(50)
expect(await holders(key.RoomKeyId)).toEqual([1, 2, 7301])
expect([await tokens(1), await tokens(2)]).toEqual([1300, 50])
expect(await drainFrames()).toEqual([]) expect(await drainFrames()).toEqual([])
// A key priced in a room currency is not sold for tokens. // A key priced in a room currency is not sold for tokens.