Repository navigation
Conversation
darbyjack
force-pushed
the
fix/economy-transaction-safety
branch
from
October 22, 2026 05:58
512007b to
ee76b2b
Compare
darbyjack
force-pushed
the
fix/economy-transaction-safety
branch
from
October 22, 2026 07:39
ee76b2b to
4ce668e
Compare
This was referenced Oct 22, 2026
A guild creation that the player can no longer afford must be refused before anything is charged and before a guild is made. The test moves the money between the prompt and the confirmation, which is what the check at confirmation time is for. The test for a refused payment now also says that the offer is void, so a later confirm cannot charge for a guild that was never created.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Vault's
Economymethods answer with anEconomyResponsethat says whether the money moved. Guilds ignored that answer and committed the guild side of the transaction anyway.Three commands did this:
CommandBank.depositwithdrew from the player and then setguild.balance = total. A provider that refused the withdrawal left the player where they were and credited the bank with money nobody paid for.CommandBank.withdrawsetguild.balance = bal - roundedand then paid the player. A provider that refused the deposit took the money out of the bank and never handed it over.CommandCreate.accept()added the guild to the handler and then withdrew the creation cost. A refused withdrawal left the player with a guild they did not pay for, and a provider that threw let the exception escape the command with the guild already in the handler.Reproduction
FakeEconomyis a Vault provider that can succeed, refuse a transaction or throw. Driving the real commands with it against the code on master:The fix
EconomyUtilsgainedwithdrawanddeposit. Both call the provider, readEconomyResponse.transactionSuccess()and report the provider's ownerrorMessageto the player when the transaction failed. A provider that throws is caught, logged and reported the same way. ANullPointerExceptionstill sendserror.economy-required, the messagehasEnoughhas always sent for a missing economy plugin.Each command commits its guild side only after the transaction succeeded:
A failed transaction leaves both sides as they were and drops the pending confirmation. An amount of zero is not sent to the provider at all, since the shipped default for the guild creation cost is
0.0and there is no money to move then.CommandAdminBankandCommandUpgradewere checked too. Both only move money inside the guild's own ledger and never call the economy provider, so there is no response for them to read.How this composes with #851
#851 re-runs the command-time checks when the pending confirmation is accepted. It edits the same file, and the two merge cleanly: this branch's hunks are in the payment and guild-building block, #851's are in the check block, and
git merge-treeagainstorigin/masterand againstfix/guild-creation-confirm-revalidationreports no conflict.The order the two changes leave behind is:
GuildCreateEventfires, and a cancelled event stops the creation before the paymentA refused check at step 3 leaves no guild and charges nothing. A refused payment at step 5 leaves no guild and no membership, and voids the offer so a later
/guild confirmcannot charge for a guild that was never created.The two changes do not undo each other. #851 re-runs
EconomyUtils.hasEnough, which reads the balance, and the amount it checks is the same quotedcostthat this branch charges, so the check and the charge are about the same amount. The read can go stale between the check and the transaction, and that costs nothing: the provider's response, not the check, decides. A player who spends the money in the meantime gets the withdrawal refused, the failure reported, and no guild.I merged this branch with #851 and ran the tests against the merged tree, and again against the merged trees of every other open PR. All of them merge cleanly, compile, and pass the eleven tests below.
The new message
The catalogue had nothing for a refused transaction, so
error.economy-transaction-failedwas added with a{reason}placeholder. It goes intoen-US.ymlonly.crowdin.ymldeclaresen-US.ymlas the single source and every other locale file as a Crowdin-managed translation, so the key reaches the other locales on the next sync. Writing an English line into the translated files by hand would be reverted by that sync.Until the sync lands, ACF resolves a key that the player's locale does not have against the manager's default locale. The plugin sets that default from
settings.messagesLanguage, which isen-USout of the box. A server on another language sees the English string, and a server that pointssettings.messagesLanguageat a locale the sync has not reached yet sees the key placeholder, the same as it does today for the keys still waiting on Crowdin. PR #805 addedadmin.cant-remove-masterandtransfer.failedthe same way.Tests
New files under
src/test/java/me/glaremasters/guilds/economy/:FakeEconomy.java, a Vault provider that can succeed, refuse or throw and that tracks the balanceAcfCommandContext.java, which puts a command issuer where ACF's commands read it from so a test can drive a command directlyEconomyTransactionTest.java, eleven tests: a successful deposit and withdrawal, a refused deposit and withdrawal, a provider that throws on either side, a refused and a throwing guild creation payment, a paid guild creation, a guild creation that costs nothing, and a guild creation the player can no longer afford when the confirmation is acceptedThe last one is the order between the checks and the payment. The player is quoted a price they can pay and spends the money before they confirm, and the creation is refused with nothing charged and no guild made.
Results
The E2E suite was run twice locally on this tree, once before the message was trimmed back to
en-US.ymland once on the final head, with the same result both times.This branch was also merged with each open PR and the eleven tests were run against every merged tree. All of them merge cleanly and pass:
git merge-tree --write-tree origin/master fix/economy-transaction-safetyreports no conflict either.The E2E check on this PR
The Plugwright check on this branch failed on "a second teleport home cannot happen until the cooldown expires" in
homes.spec.ts, with "pw_be4d's connection had already ended (socketClosed); /guild home would never have been sent". The bot's socket dropped, so the command never left the client. The assertion is not about this change, and this branch does not touchhomes.spec.ts.That test is flaky on the runner rather than on any one branch:
Runs:
https://github.com/guilds-plugin/Guilds/actions/runs/37211243837
https://github.com/guilds-plugin/Guilds/actions/runs/39727120928
https://github.com/guilds-plugin/Guilds/actions/runs/39728538651
https://github.com/guilds-plugin/Guilds/actions/runs/39730173487
https://github.com/guilds-plugin/Guilds/actions/runs/39742006003
I left
homes.spec.tsalone and did not weaken the assertion. The suite is green locally on this head, 144 passed, 0 failed, 0 skipped (7m 2s), and the re-run of the failed job on the same head is green as well: run 39742006003, attempt 2, passed.Compatibility
No new dependency, no new API and no config key. The plugin still compiles to Java 11 bytecode and the 1.8.8 Material probe passes. No
Materialconstant was touched.EconomyResponsehas been part of the Vault API since Vault 1.0, so this works with every provider the plugin already supports.Limitations
EconomyUtils.hasEnoughstill catches onlyNullPointerException. A provider that throws something else while the balance is read lets the exception out of the command, before any state has changed.{reason}placeholder carries the economy provider's ownerrorMessage. Provider text is not translated, so a player on a non-English server sees an English fragment inside an otherwise translated sentence. It is a failure path, and the alternative was a message that says nothing about what went wrong.GuildCreateEventis still fired before the creation cost is taken. Charging first would mean refunding when the event is cancelled, and a refund that fails loses the player's money, so the event order is deliberate. A listener that reacts toGuildCreateEventcan therefore see a guild creation that then fails to be paid for.