-
Notifications
You must be signed in to change notification settings - Fork 1
Development
When it comes to depending on AccountInterface, you will first need to add AccountInterface into your dependencies. If your project uses maven, the following can be used
<repositories>
<repository>
<id>jitpack.io</id>
<url>https://jitpack.io</url>
</repository>
</repositories>
<dependencies>
<dependency>
<groupId>com.github.EcoToolBox</groupId>
<artifactId>AccountInterface</artifactId>
<version>main-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
</dependencies>After adding the dependency into your development environment, you will then need to add AccountInterface dependency into your plugin.yml
depend:
- AccountInterfaceThe AccountInterfaceManager is the one stop shop for all AccountInterface generic handling (aka, you will use this a lot). This is provided by the currency implementation, however you are able to receive it in two different ways depending on your situation / what you prefer.
The simplest way is to simply do one of the following:
By hard depends
AccountInterfaceManager manager = AccountInterface.getManager();By soft depends
public Optional<AccountInterfaceManager> getAccountInterfaceManager(){
Plugin accountInterfacePlugin = Bukkit
.getPluginManager()
.getPlugin("AccountInterface");
if(
accountInterfacePlugin == null ||
(!(accountInterfacePlugin instanceOf AccountInterface plugin))){
return Optional.empty();
}
return Optional.of(plugin.getManager());
}This has the benefit of just being a one-liner, however if you prefer to do the old vault method, then you can too (The above does this way under the hood, but caches the result). This method allows you to also get the Plugin of the implementation if you so prefer
RegisteredServiceProvider<AccountInterfaceManager> reg = Bukkit
.getServicesManager()
.getRegistration(AccountInterfaceManager.class);
AccountInterfaceManager manager = reg.getProvider();AccountInterface supports multiple currencies, this being directly supported by AccountInterface means that other plugins can interact with currencies without guessing the default, with that being said, if the admin only wants a single currency then that is a possibility.
The default currency is the currency that should be used if you don't need to specify a currency. This is also the currency that the vault emulation will use by default (as vault doesn't support multiple currencies)
AccountInterfaceManager accountInterfaceManager;
Currency currency = accountInterfaceManager.getDefaultCurrency();AccountInterfaceManager accountInterfaceManager;
Collection<Currency> currencies = accountInterfaceManager.getCurrencies();When creating a new currency, we will use the CurrencyBuilder to develop the new currency, here are the details of using a currency builder
| Name | Type | Optional | Description |
|---|---|---|---|
| name | String | false | The registered name of the currency, this typically will not be shown to the user in transactions |
| plugin | Plugin | false | The implementation plugin of the currency. This cannot be specified in the builder |
| symbol | String | false | The currencies sign |
| worth | BigDecimal | true | The worth of the currency compared to 1 (value must be greater then 0) |
| name single | String | true | The name of the currency if the amount is 1
|
| name multiple | String | true | The name of the currency if the amount is more then 1
|
| name short | String | true | The initials of the currency |
| default | boolean | false | If the currency is the default currency, please note that setting this in the builder may result in a error where two or more default currencies are active, so best practice would be to remove the default status from the currency default currency before registering yours |
When it comes to the specified names (single, multiple and short) these are optional, however if not specified, it will use the next best thing based upon the implementation, so it is best to specify these if known.
Plugin plugin;
Currency currency = new CurrencyBuilder()
.setWorth(1)
.setDisplayNameSingle("pound")
.setDisplayNameMultiple("pounds")
.setDisplayNameShort("gbp")
.setSymbol("£")
.setName("British Pounds")
.setDefault(true);When handling any transaction, a payment object will be needed. This will give the context for the transaction before the transaction has even started.
A payment includes the following:
| Name | Type | Optional |
|---|---|---|
| amount | BigDecimal | false |
| currency | Currency | false |
| reason | String | true |
| from | Account | true |
As you can see, the amount is not a double but instead a BigDecimal. This is due to the precision of BigDecimal compared to double. With that being said, the PaymentBuilder does accept double values for the convenience, this then gets converted to a BigDecimal.
The reason and from are optional as they are only there to provide context for the payment.
A account is the holder of money, typically this will be a player, however could be a bank or something else (depending on the currency plugin).
A account class is prefered to be connected directly onto the object its representing, its not always possible (such as Player whereby the player object is part of the Bukkit API), so we may use the account as a wrapper
The player account is a wrapper for the Bukkit's OfflinePlayer. A Player account can have banks attached to it too (see below) and therefore you gain extra transaction methods (see below) for handling transactions between what your wanting and the PlayerAccount as well as all banks that are loaded that the PlayerAccount has access to.
Bank accounts may not be attached to a player, therefore there is a basic BankAccount with a further PlayerBankAccount to represent the holder being a player.
A Bank Account allows multiple accounts to access the money inside the account. Each account will have BankPermissions attached to them. While you can bypass these permissions with code, the permissions should be respected unless for a specific reason (such as a direct debit)
There are two main types of transaction objects in AccountInterface, the first being Transaction with the other being TransactionResult.
The transaction class is typically only accessible while the transaction is happening. It is designed in a way to give you even more context to the transaction happening then the Payment, so much so that the payment object is part of the transaction. You are also able to receive the account target of the transaction, the type of transaction (withdraw, deposit, set, etc) and you can even modify the transaction amount
The transaction result class is created when the transaction has been completed (if that be though success or failure). The result class is designed to give you context on if it was a success or a failure, if the latter then why it failed.
TransactionResult result;
if(result instanceof SuccessfulTransactionResult){
getLogger().log("Successful");
}
if(result instanceof FailedTransactionResult failed){
getLogger().warning("Failed due to " + failed.getReason());
}Its all well and good knowing how to manipulate the transaction, but how we start one is the fun part.
Transactions in AccountInterface can be asynced (meaning off main thread). This maybe different to what you are used to as Bukkit was written when Minecraft was heavily single threaded, however thats a side story. The main result of being asynced is that you will be using CompleteableFuture if you care about the result of the transaction.
When it comes to actual transactions, there are 3 to choose from as of writing.
| type | code | description |
|---|---|---|
| Set | account.set(Payment) |
Sets the account's balance in the currency specified to the specified amount |
| deposit | account.deposit(Payment) |
Adds the specified amount in the specified currency to the account |
| withdraw | acount.withdraw(Payment) |
Removes the specified amount in the specified currency from the account |
Please note that account balances cannot be below 0, so attempting to set the value lower or withdrawing more then what the account has will result in a fail
If you require to do multiple transactions whereby if one fails, all should fail and not make any changes (example such as paying someone else, whereby a withdraw needs to occur to one account and a deposit of the same amount needs to occur to the other) then isolated transactions are the way to do it.
The accounts being used in the transaction need to be of
AccountType
In the following example, we will be paying an account £1.
final AccountType payee;
final AccountType payer;
final BigDecimal amount = BigDecimal.ONE;
final Currency currency;
CompletableFuture<TransactionResult> result = new IsolatedTransaction(
accounts -> {
IsolatedAccount isolatedPayee = accounts.get(payee);
IsolatedAccount isolatedPayer = accounts.get(payer);
Payment withdrawPayment = new PaymentBuilder().setAmount(amount).setCurrency(currency).build();
CompletableFuture<SingleTransactionResult> withdraw = isolatedPayer.withdraw(withdrawPayment);
Payment depositPayment = new PaymentBuilder().setAmount(amount).setCurrency(currency).from(payer).build();
CompletableFuture<SingleTransactionResult> deposit = isolatedPayee.deposit(depositPayment);
return Arrays.asList(withdrawPayment, depositPayment);
},
payee,
payer).start();The magic behind the IsolatedTransaction comes mainly from the lamda. All accounts (2nd parameter and beyond) are converted into a IsolatedAccount. These are then stored in a Map that is sent with the lamda (hence the get(payee)). The transactions you wish to commit are then done on the isolated accounts so not to interact with the balance of the regular account. These are then all awaited for with the results then checked for any failures. If all passed, then the isolated accounts are then applied to the respected original account.
The isolated transaction needs to know what transactions to await for, therefore we return a collection that holds all of the transactions (hence the Arrays.asList().