Currency / Authority¶
Purpose¶
The Authority folder defines the authority hooks for the Currency system.
It is intended to answer a single question:
Who is allowed to mutate this wallet?
This folder does not provide replication, prediction, rollback, or reconciliation. Those responsibilities belong to your chosen netcode layer.
Important Notes¶
- This is not a multiplayer framework
- Authority answers yes/no only
- Authority does not mutate state or perform side effects
- Replication remains the responsibility of the host project
- Currency authority is netcode-agnostic
What Lives Here¶
ICurrencyAuthority¶
The minimal authority contract:
public interface ICurrencyAuthority
{
bool HasAuthority(GameObject owner, CurrencyId currency, CurOpKind kind);
bool HasAuthorityTransfer(GameObject from, GameObject to, CurrencyId currency);
}
You implement this interface on any component that defines who may mutate which wallet.
Common placements:
- On the
CurrencyServiceBootstrap, or a parent of it — the context the decorator resolves from - On a scene-level manager
- On a netcode authority object
Putting it on a wallet owner works only in the sense that the scene scan will eventually find it, and it does not scope the answer to that owner. See "Resolution order" below.
CurrencyAuthority¶
Static resolver with scene-scoped caching.
It refreshes automatically on common Unity lifecycle events such as scene load/unload, active scene changes, and domain reload.
Resolution order — from the context you passed to WithAuthority, which in the shipped stack is the CurrencyServiceBootstrap, never the owner being mutated:
- Scene cache
- Local component on the context GameObject
- Parent transforms
- Scene roots (including inactive)
- Global scan
The first usable authority found is cached for the whole scene, so one implementation answers for every owner in it and the owner arrives as an argument to HasAuthority. That is where a per-owner rule belongs; no placement gives you per-owner scoping.
The resolver is internal, and so is everything on it. There is no public Resolve and no public Invalidate — this section used to print both as if they were callable, and the FAQ told you to call one. You do not need to:
CurrencyAuthorityBinderinvalidates its scene's cache as it is enabled and disabled- a custom
MonoBehaviourauthority that appears is found on the next resolve — or on the next frame, in play mode, if a resolve already came up empty earlier in the same frame - one that is disabled or destroyed is dropped when the cache is read, and a replacement is found
The resolver returns usable implementations only — enabled, active in hierarchy, not destroyed. Implementations must be MonoBehaviours: every discovery step enumerates components, so a plain class or ScriptableObject implementing the interface is never found. On a wrapped stack that fails closed, which is safe but looks like a broken authority.
CurrencyAuthorityBinder¶
A built-in binder for single-player or local development:
[AddComponentMenu("RevFramework/Currency/Authority/Currency Authority Binder (Local)")]
public sealed class CurrencyAuthorityBinder : MonoBehaviour, ICurrencyAuthority
{
public bool alwaysTrue = true;
public bool HasAuthority(GameObject owner, CurrencyId id, CurOpKind kind)
=> alwaysTrue && owner;
public bool HasAuthorityTransfer(GameObject from, GameObject to, CurrencyId id)
=> alwaysTrue && from && to;
}
Attach this to the bootstrap, or anywhere in the scene, to allow local mutations.
For multiplayer, replace it with a project-specific binder.
Usage Guidance¶
How Currency uses authority¶
Authority is enforced through the AuthorityCurrencyService decorator.
Added during composition:
svc = CurrencyFactories.WithAuthority(inner, context);
Behaviour:
- If a binder exists and denies, the operation returns
CurOpCode.Unauthorized - If no binder is found, the operation returns
CurOpCode.Unauthorized - If the binder throws, the exception is logged via
Debug.LogExceptionand the operation returnsCurOpCode.Unauthorizedwith nothing mutated — a gate that cannot answer is a gate that denies. Currency is the only system that converts an authority exception; the others let it propagate, which is also before any mutation. See the cross-system authority reference - When the authority decorator is composed but no binder is discoverable, mutating operations are blocked until an
ICurrencyAuthorityimplementation is available - Authority is checked for all mutating operations, including audit-aware overloads
- Read operations (
GetBalance,EnsureWallet) are not authority-gated
Which question each two-party path asks. HasAuthorityTransfer governs ICurrencyService.Transfer and CurrencyTxn.Transfer, and nothing else. Escrow-backed transfers and exchanges — CurrencyHoldTxn.HoldTransfer, CurrencyHoldCraftingAdapter, ICurrencyExchange.TryExchange — are decided as HasAuthority(from, Debit) plus HasAuthority(to, Credit), because escrow composes outside the authority layer and a hold is an ordinary debit by the time the gate sees it. A rule such as "no player-to-player trading" expressed only in HasAuthorityTransfer does not reach those paths; restate it in HasAuthority if it must.
If you want permissive behaviour, add a binder that returns true.
Typically only one binder should be active per resolution path. If multiple binders exist, the first valid one found is used.
Typical stacks¶
Single-player¶
SceneCurrencyService → Caps → Audit → Authority (Local Binder)
Multiplayer¶
SceneCurrencyService → Caps → Audit → Authority → Idempotency → Batch
RequireEscrow may also be present depending on policy.
Quick start¶
Single-player¶
svc = CurrencyFactories.WithCapsAuditAuthority(inner, policy, this);
This enables explicit authority with a permissive local binder.
Multiplayer¶
- Implement an
ICurrencyAuthoritybinder for your authority rules - Attach it to the appropriate GameObject
- Compose the authority decorator:
svc = CurrencyFactories.WithCapsAuditAuthority(inner, policy, this);
- Run mutations on the authority side
- Replicate balances through your netcode layer
If authority denies:
CurOpResult.Code == CurOpCode.Unauthorized
Diagnostics¶
Troubleshooting¶
Unauthorizedusually means no authority binder was found, or the active binder returnedfalse- Missing client updates usually means replication has not been implemented
FAQs¶
Where should the binder live? Anywhere the resolver will find it from the composition context — the CurrencyServiceBootstrap in the shipped stack, then its parents, then any scene root, then a global scan. One per scene. Placing it on a wallet owner does not scope it to that owner: the answer is cached per scene, and the owner is an argument to HasAuthority, which is where a per-owner rule goes.
Do I need to call Invalidate()? No, and you cannot — CurrencyAuthority is internal. The shipped binder refreshes the cache as it is enabled and disabled; an authority that appears is found on the next resolve; one that is disabled or destroyed is dropped when the cache is read.
Can I register multiple binders? The resolver uses the first valid one it finds. Placement matters.
Does Currency replicate balances? No. Replication is outside the scope of this folder.
Not for Production Use¶
This folder supplies the seam only
Example netcode rules such as NGO, Mirror, or Fusion authority checks are not provided here.
Related Documentation¶
- Core — composition helpers such as
WithAuthorityandWithCapsAuditAuthority - Internal —
AuthorityCurrencyServicedecorator - Policies — optional cap and transfer rule enforcement
- Adapters — inventory-backed currency implementations