Sistema de Wallet e Tokens - Sport Tech Club
Índice
- Visão Geral
- Arquitetura
- Modelo de Domínio
- Entidades
- Value Objects
- Regras de Negócio
- Casos de Uso
- Fluxos de Transação
- Segurança e Auditoria
- Integrações
- Implementação Técnica
Visão Geral
Objetivo
Criar um sistema de economia virtual própria da arena esportiva, permitindo:
- Valorização da participação e engajamento
- Gamificação e recompensas
- Apostas não-financeiras entre times
- Premiações em campeonatos
- Cashback por consumo
- Ecossistema fechado e auditável
Princípios Fundamentais
┌─────────────────────────────────────────────────────────────┐
│ VALOR ≠ DINHEIRO │
├─────────────────────────────────────────────────────────────┤
│ • ArenaCoin é uma ABSTRAÇÃO DE VALOR │
│ • Não é moeda regulada (não é criptomoeda) │
│ • Sistema fechado (arena ecosystem) │
│ • Conversibilidade controlada pela arena │
│ • Foco em ENGAJAMENTO, não especulação │
└─────────────────────────────────────────────────────────────┘1. Auditabilidade Total
- Toda transação é registrada em ledger imutável
- Rastreabilidade completa de origem e destino
- Histórico perpétuo para compliance
2. Separação de Concerns
- Wallet (armazenamento) ≠ Transaction (movimentação)
- Token Definition (regras) ≠ Token Balance (saldo)
- Business Rules (negócio) ≠ Technical Rules (técnica)
3. Integrabilidade
- Parte do Antóctica Ecosystem
- Outros apps podem emitir/aceitar ArenaCoin
- ApplicationWallet para integração cross-platform
4. Consistência Eventual
- Event-driven para escalabilidade
- CQRS para leitura/escrita
- Saga pattern para transações distribuídas
Arquitetura
Visão Macro
┌─────────────────────────────────────────────────────────────────────┐
│ SPORT TECH CLUB ECOSYSTEM │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ User Wallet │ │ Arena Wallet │ │ Event Wallet │ │
│ │ │ │ │ │ │ │
│ │ • Players │ │ • Cashback │ │ • Tournament│ │
│ │ • Students │ │ • Prizes │ │ • Bets │ │
│ │ • Coaches │ │ • Sponsors │ │ • Temp Pool │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
│ │ │ │ │
│ └────────────────────┴────────────────────┘ │
│ │ │
│ ┌─────────▼─────────┐ │
│ │ TRANSACTION │ │
│ │ ENGINE │ │
│ │ │ │
│ │ • Credit/Debit │ │
│ │ • Transfer │ │
│ │ • Bet/Reward │ │
│ │ • Cashback │ │
│ └─────────┬─────────┘ │
│ │ │
│ ┌─────────▼─────────┐ │
│ │ LEDGER STORE │ │
│ │ (Event Sourcing) │ │
│ └───────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────┘
│ │
▼ ▼
┌──────────────────┐ ┌──────────────────┐
│ Antóctica Apps │ │ Analytics & │
│ (External) │ │ Reporting │
└──────────────────┘ └──────────────────┘Clean Architecture Layers
┌─────────────────────────────────────────────────────────────┐
│ PRESENTATION LAYER │
│ • REST Controllers │
│ • GraphQL Resolvers │
│ • WebSocket Handlers (real-time balance) │
└────────────────────────┬────────────────────────────────────┘
│
┌────────────────────────▼────────────────────────────────────┐
│ APPLICATION LAYER │
│ • Use Cases (CreateWalletUseCase, TransferTokensUseCase) │
│ • DTOs (CreateWalletDto, TransactionDto) │
│ • Mappers (WalletMapper, TransactionMapper) │
└────────────────────────┬────────────────────────────────────┘
│
┌────────────────────────▼────────────────────────────────────┐
│ DOMAIN LAYER │
│ • Entities (Wallet, Transaction, TokenDefinition) │
│ • Value Objects (TokenAmount, WalletAddress) │
│ • Domain Services (TransactionService, RuleEngine) │
│ • Domain Events (WalletCreated, TransactionCompleted) │
│ • Repository Interfaces (WalletRepository) │
└────────────────────────┬────────────────────────────────────┘
│
┌────────────────────────▼────────────────────────────────────┐
│ INFRASTRUCTURE LAYER │
│ • Repository Implementations (PrismaWalletRepository) │
│ • Event Store (Event Sourcing) │
│ • External Services (PaymentGateway, NotificationService) │
│ • Database (PostgreSQL + Redis) │
└─────────────────────────────────────────────────────────────┘Modelo de Domínio
Domain Model Overview
┌────────────────────────────────────────────────────────────────────┐
│ WALLET AGGREGATE │
├────────────────────────────────────────────────────────────────────┤
│ │
│ Wallet (Aggregate Root) │
│ ├── id: WalletId │
│ ├── owner: OwnerId │
│ ├── type: WalletType │
│ ├── balances: TokenBalance[] │
│ ├── status: WalletStatus │
│ ├── createdAt: DateTime │
│ └── metadata: WalletMetadata │
│ │
│ TokenBalance (Entity) │
│ ├── tokenDefinition: TokenDefinitionId │
│ ├── available: TokenAmount │
│ ├── locked: TokenAmount │
│ └── lastUpdated: DateTime │
│ │
└────────────────────────────────────────────────────────────────────┘
┌────────────────────────────────────────────────────────────────────┐
│ TRANSACTION AGGREGATE │
├────────────────────────────────────────────────────────────────────┤
│ │
│ Transaction (Aggregate Root) │
│ ├── id: TransactionId │
│ ├── type: TransactionType │
│ ├── from: WalletId (optional) │
│ ├── to: WalletId │
│ ├── amount: TokenAmount │
│ ├── token: TokenDefinitionId │
│ ├── status: TransactionStatus │
│ ├── reason: TransactionReason │
│ ├── metadata: TransactionMetadata │
│ ├── createdAt: DateTime │
│ └── completedAt: DateTime │
│ │
│ TransactionType: │
│ • CreditTransaction │
│ • DebitTransaction │
│ • TransferTransaction │
│ • RewardTransaction │
│ • BetTransaction │
│ • CashbackTransaction │
│ • PrizeTransaction │
│ │
└────────────────────────────────────────────────────────────────────┘
┌────────────────────────────────────────────────────────────────────┐
│ TOKEN DEFINITION AGGREGATE │
├────────────────────────────────────────────────────────────────────┤
│ │
│ TokenDefinition (Aggregate Root) │
│ ├── id: TokenDefinitionId │
│ ├── name: TokenName ("ArenaCoin") │
│ ├── symbol: TokenSymbol ("ARC") │
│ ├── description: string │
│ ├── rules: TokenRules │
│ ├── isActive: boolean │
│ └── createdAt: DateTime │
│ │
│ TokenRules (Value Object) │
│ ├── exchangeRules: ExchangeRule[] │
│ ├── conversionRules: ConversionRule[] │
│ ├── expirationRule: ExpirationRule │
│ ├── rewardRules: RewardRule[] │
│ ├── betRules: BetRule[] │
│ └── prizeRules: PrizeRule[] │
│ │
└────────────────────────────────────────────────────────────────────┘Entidades
1. Wallet (Aggregate Root)
typescript
/**
* Wallet - Carteira digital para armazenamento de tokens
*
* Aggregate Root: Fronteira de consistência para operações de saldo
* Invariantes:
* - Saldo disponível nunca pode ser negativo
* - Saldo bloqueado + disponível = saldo total
* - Apenas uma carteira por (owner, type)
*/
class Wallet {
private constructor(
public readonly id: WalletId,
private readonly _owner: OwnerId,
private readonly _type: WalletType,
private _balances: Map<TokenDefinitionId, TokenBalance>,
private _status: WalletStatus,
private readonly _createdAt: DateTime,
private _metadata: WalletMetadata
) {}
static create(props: CreateWalletProps): Wallet {
// Validação de domínio
if (!props.owner) {
throw new DomainError('Wallet owner is required');
}
const wallet = new Wallet(
WalletId.generate(),
props.owner,
props.type,
new Map(),
WalletStatus.ACTIVE,
DateTime.now(),
props.metadata || WalletMetadata.empty()
);
wallet.addDomainEvent(new WalletCreatedEvent(wallet.id, props.owner));
return wallet;
}
// Comportamentos de domínio
credit(amount: TokenAmount, token: TokenDefinitionId): void {
this.ensureActive();
const balance = this.getOrCreateBalance(token);
balance.add(amount);
this.addDomainEvent(
new TokensCreditedEvent(this.id, token, amount)
);
}
debit(amount: TokenAmount, token: TokenDefinitionId): void {
this.ensureActive();
const balance = this.getBalance(token);
if (!balance.hasAvailable(amount)) {
throw new InsufficientBalanceError(
`Insufficient balance. Available: ${balance.available}, Required: ${amount}`
);
}
balance.subtract(amount);
this.addDomainEvent(
new TokensDebitedEvent(this.id, token, amount)
);
}
lock(amount: TokenAmount, token: TokenDefinitionId): void {
const balance = this.getBalance(token);
if (!balance.hasAvailable(amount)) {
throw new InsufficientBalanceError();
}
balance.lock(amount);
this.addDomainEvent(
new TokensLockedEvent(this.id, token, amount)
);
}
unlock(amount: TokenAmount, token: TokenDefinitionId): void {
const balance = this.getBalance(token);
balance.unlock(amount);
this.addDomainEvent(
new TokensUnlockedEvent(this.id, token, amount)
);
}
suspend(reason: string): void {
if (this._status === WalletStatus.SUSPENDED) {
throw new DomainError('Wallet already suspended');
}
this._status = WalletStatus.SUSPENDED;
this.addDomainEvent(
new WalletSuspendedEvent(this.id, reason)
);
}
activate(): void {
if (this._status === WalletStatus.ACTIVE) {
throw new DomainError('Wallet already active');
}
this._status = WalletStatus.ACTIVE;
this.addDomainEvent(
new WalletActivatedEvent(this.id)
);
}
// Getters
get owner(): OwnerId {
return this._owner;
}
get type(): WalletType {
return this._type;
}
get status(): WalletStatus {
return this._status;
}
getAvailableBalance(token: TokenDefinitionId): TokenAmount {
const balance = this._balances.get(token.value);
return balance ? balance.available : TokenAmount.zero();
}
getTotalBalance(token: TokenDefinitionId): TokenAmount {
const balance = this._balances.get(token.value);
return balance ? balance.total : TokenAmount.zero();
}
// Helpers privados
private ensureActive(): void {
if (this._status !== WalletStatus.ACTIVE) {
throw new InactiveWalletError('Wallet is not active');
}
}
private getBalance(token: TokenDefinitionId): TokenBalance {
const balance = this._balances.get(token.value);
if (!balance) {
throw new TokenNotFoundError(`Token ${token.value} not found in wallet`);
}
return balance;
}
private getOrCreateBalance(token: TokenDefinitionId): TokenBalance {
let balance = this._balances.get(token.value);
if (!balance) {
balance = TokenBalance.create(token);
this._balances.set(token.value, balance);
}
return balance;
}
}2. Transaction (Aggregate Root)
typescript
/**
* Transaction - Movimentação de tokens entre wallets
*
* Aggregate Root: Fronteira de consistência para operações transacionais
* Invariantes:
* - Transação completada não pode ser modificada
* - Valor deve ser positivo
* - From e To não podem ser iguais (exceto em edge cases)
*/
abstract class Transaction {
protected constructor(
public readonly id: TransactionId,
public readonly type: TransactionType,
protected readonly _from: WalletId | null,
protected readonly _to: WalletId,
protected readonly _amount: TokenAmount,
protected readonly _token: TokenDefinitionId,
protected _status: TransactionStatus,
protected readonly _reason: TransactionReason,
protected readonly _metadata: TransactionMetadata,
protected readonly _createdAt: DateTime,
protected _completedAt: DateTime | null
) {}
// Template Method Pattern
async execute(
walletRepository: WalletRepository,
ruleEngine: RuleEngine
): Promise<void> {
this.ensurePending();
try {
// 1. Validar regras de negócio
await this.validateBusinessRules(ruleEngine);
// 2. Carregar wallets
const wallets = await this.loadWallets(walletRepository);
// 3. Aplicar transação (comportamento específico)
await this.applyTransaction(wallets);
// 4. Persistir wallets
await this.persistWallets(walletRepository, wallets);
// 5. Completar transação
this.complete();
// 6. Emitir evento
this.addDomainEvent(this.createCompletedEvent());
} catch (error) {
this.fail(error.message);
throw error;
}
}
protected abstract validateBusinessRules(ruleEngine: RuleEngine): Promise<void>;
protected abstract loadWallets(repo: WalletRepository): Promise<Wallet[]>;
protected abstract applyTransaction(wallets: Wallet[]): Promise<void>;
protected abstract createCompletedEvent(): DomainEvent;
private complete(): void {
this._status = TransactionStatus.COMPLETED;
this._completedAt = DateTime.now();
}
private fail(reason: string): void {
this._status = TransactionStatus.FAILED;
this._metadata.failureReason = reason;
}
private ensurePending(): void {
if (this._status !== TransactionStatus.PENDING) {
throw new DomainError('Transaction already processed');
}
}
// Getters
get from(): WalletId | null { return this._from; }
get to(): WalletId { return this._to; }
get amount(): TokenAmount { return this._amount; }
get token(): TokenDefinitionId { return this._token; }
get status(): TransactionStatus { return this._status; }
}
/**
* CreditTransaction - Crédito de tokens na carteira
* Origem: Sistema (emissão) ou Arena (recompensa)
*/
class CreditTransaction extends Transaction {
static create(props: CreateCreditTransactionProps): CreditTransaction {
return new CreditTransaction(
TransactionId.generate(),
TransactionType.CREDIT,
null, // Sistema é a origem
props.to,
props.amount,
props.token,
TransactionStatus.PENDING,
props.reason,
props.metadata,
DateTime.now(),
null
);
}
protected async validateBusinessRules(ruleEngine: RuleEngine): Promise<void> {
// Validar se o token permite emissão
const canEmit = await ruleEngine.canEmitToken(this._token, this._amount);
if (!canEmit) {
throw new BusinessRuleError('Token emission not allowed');
}
}
protected async loadWallets(repo: WalletRepository): Promise<Wallet[]> {
const toWallet = await repo.findById(this._to);
if (!toWallet) {
throw new WalletNotFoundError(`Wallet ${this._to.value} not found`);
}
return [toWallet];
}
protected async applyTransaction(wallets: Wallet[]): Promise<void> {
const [toWallet] = wallets;
toWallet.credit(this._amount, this._token);
}
protected createCompletedEvent(): DomainEvent {
return new CreditTransactionCompletedEvent(
this.id,
this._to,
this._amount,
this._token
);
}
}
/**
* TransferTransaction - Transferência entre wallets
*/
class TransferTransaction extends Transaction {
static create(props: CreateTransferTransactionProps): TransferTransaction {
if (!props.from) {
throw new DomainError('Transfer requires source wallet');
}
if (props.from.equals(props.to)) {
throw new DomainError('Cannot transfer to same wallet');
}
return new TransferTransaction(
TransactionId.generate(),
TransactionType.TRANSFER,
props.from,
props.to,
props.amount,
props.token,
TransactionStatus.PENDING,
props.reason,
props.metadata,
DateTime.now(),
null
);
}
protected async validateBusinessRules(ruleEngine: RuleEngine): Promise<void> {
// Validar se o token permite transferência
const canTransfer = await ruleEngine.canTransferToken(
this._token,
this._from!,
this._to,
this._amount
);
if (!canTransfer) {
throw new BusinessRuleError('Transfer not allowed by token rules');
}
}
protected async loadWallets(repo: WalletRepository): Promise<Wallet[]> {
const [fromWallet, toWallet] = await Promise.all([
repo.findById(this._from!),
repo.findById(this._to)
]);
if (!fromWallet) {
throw new WalletNotFoundError(`Source wallet ${this._from!.value} not found`);
}
if (!toWallet) {
throw new WalletNotFoundError(`Destination wallet ${this._to.value} not found`);
}
return [fromWallet, toWallet];
}
protected async applyTransaction(wallets: Wallet[]): Promise<void> {
const [fromWallet, toWallet] = wallets;
// Operação atômica
fromWallet.debit(this._amount, this._token);
toWallet.credit(this._amount, this._token);
}
protected createCompletedEvent(): DomainEvent {
return new TransferTransactionCompletedEvent(
this.id,
this._from!,
this._to,
this._amount,
this._token
);
}
}
/**
* BetTransaction - Aposta em partida/campeonato
*/
class BetTransaction extends Transaction {
constructor(
id: TransactionId,
from: WalletId,
to: WalletId, // EventWallet
amount: TokenAmount,
token: TokenDefinitionId,
status: TransactionStatus,
reason: TransactionReason,
metadata: TransactionMetadata,
createdAt: DateTime,
completedAt: DateTime | null,
private readonly _matchId: MatchId,
private readonly _prediction: BetPrediction
) {
super(id, TransactionType.BET, from, to, amount, token, status, reason, metadata, createdAt, completedAt);
}
static create(props: CreateBetTransactionProps): BetTransaction {
return new BetTransaction(
TransactionId.generate(),
props.from,
props.eventWallet,
props.amount,
props.token,
TransactionStatus.PENDING,
TransactionReason.BET,
props.metadata,
DateTime.now(),
null,
props.matchId,
props.prediction
);
}
protected async validateBusinessRules(ruleEngine: RuleEngine): Promise<void> {
// Validar regras de aposta
const canBet = await ruleEngine.canPlaceBet(
this._matchId,
this._from!,
this._amount,
this._token
);
if (!canBet) {
throw new BusinessRuleError('Bet not allowed');
}
}
protected async loadWallets(repo: WalletRepository): Promise<Wallet[]> {
const [fromWallet, eventWallet] = await Promise.all([
repo.findById(this._from!),
repo.findById(this._to)
]);
if (!fromWallet) throw new WalletNotFoundError();
if (!eventWallet) throw new WalletNotFoundError();
return [fromWallet, eventWallet];
}
protected async applyTransaction(wallets: Wallet[]): Promise<void> {
const [fromWallet, eventWallet] = wallets;
// Lock tokens na carteira do apostador
fromWallet.lock(this._amount, this._token);
// Creditar na pool do evento
eventWallet.credit(this._amount, this._token);
}
protected createCompletedEvent(): DomainEvent {
return new BetPlacedEvent(
this.id,
this._from!,
this._matchId,
this._amount,
this._prediction
);
}
get matchId(): MatchId {
return this._matchId;
}
get prediction(): BetPrediction {
return this._prediction;
}
}3. TokenDefinition (Aggregate Root)
typescript
/**
* TokenDefinition - Definição e regras de um token
*
* Aggregate Root: Fronteira de consistência para configuração de tokens
*/
class TokenDefinition {
private constructor(
public readonly id: TokenDefinitionId,
private _name: TokenName,
private _symbol: TokenSymbol,
private _description: string,
private _rules: TokenRules,
private _isActive: boolean,
private readonly _createdAt: DateTime
) {}
static create(props: CreateTokenDefinitionProps): TokenDefinition {
const tokenDef = new TokenDefinition(
TokenDefinitionId.generate(),
props.name,
props.symbol,
props.description,
props.rules,
true,
DateTime.now()
);
tokenDef.addDomainEvent(
new TokenDefinitionCreatedEvent(tokenDef.id, props.name, props.symbol)
);
return tokenDef;
}
// Factory method para ArenaCoin padrão
static createArenaCoin(): TokenDefinition {
return TokenDefinition.create({
name: TokenName.create('ArenaCoin'),
symbol: TokenSymbol.create('ARC'),
description: 'Moeda oficial da Sport Tech Arena',
rules: TokenRules.createDefault()
});
}
updateRules(rules: TokenRules): void {
this._rules = rules;
this.addDomainEvent(
new TokenRulesUpdatedEvent(this.id, rules)
);
}
deactivate(): void {
this._isActive = false;
this.addDomainEvent(
new TokenDefinitionDeactivatedEvent(this.id)
);
}
// Validações de regras
canTransfer(from: WalletId, to: WalletId, amount: TokenAmount): boolean {
if (!this._isActive) return false;
// Verificar regras de transferência
return this._rules.exchangeRules.every(rule =>
rule.allowsTransfer(from, to, amount)
);
}
canConvertToReal(amount: TokenAmount): boolean {
return this._rules.conversionRules.some(rule =>
rule.allowsConversion(amount)
);
}
isExpired(balance: TokenBalance): boolean {
if (!this._rules.expirationRule) return false;
return this._rules.expirationRule.isExpired(balance.lastUpdated);
}
calculateReward(event: RewardEvent): TokenAmount {
const applicableRules = this._rules.rewardRules.filter(rule =>
rule.appliesTo(event)
);
const amounts = applicableRules.map(rule => rule.calculate(event));
return TokenAmount.sum(amounts);
}
// Getters
get name(): TokenName { return this._name; }
get symbol(): TokenSymbol { return this._symbol; }
get description(): string { return this._description; }
get rules(): TokenRules { return this._rules; }
get isActive(): boolean { return this._isActive; }
}Value Objects
TokenAmount
typescript
/**
* TokenAmount - Quantidade de tokens
* Value Object: Imutável, sem identidade
*/
class TokenAmount {
private constructor(private readonly value: number) {
if (value < 0) {
throw new DomainError('Token amount cannot be negative');
}
if (!Number.isFinite(value)) {
throw new DomainError('Token amount must be finite');
}
}
static create(value: number): TokenAmount {
return new TokenAmount(value);
}
static zero(): TokenAmount {
return new TokenAmount(0);
}
static sum(amounts: TokenAmount[]): TokenAmount {
const total = amounts.reduce((acc, amount) => acc + amount.value, 0);
return new TokenAmount(total);
}
add(other: TokenAmount): TokenAmount {
return new TokenAmount(this.value + other.value);
}
subtract(other: TokenAmount): TokenAmount {
return new TokenAmount(this.value - other.value);
}
multiply(factor: number): TokenAmount {
return new TokenAmount(this.value * factor);
}
isGreaterThan(other: TokenAmount): boolean {
return this.value > other.value;
}
isGreaterThanOrEqual(other: TokenAmount): boolean {
return this.value >= other.value;
}
equals(other: TokenAmount): boolean {
return this.value === other.value;
}
toNumber(): number {
return this.value;
}
toString(): string {
return this.value.toFixed(2);
}
}WalletType
typescript
/**
* WalletType - Tipo de carteira
*/
enum WalletType {
USER = 'USER', // Carteira de jogador/aluno
ARENA = 'ARENA', // Carteira da arena
EVENT = 'EVENT', // Carteira temporária de evento
APPLICATION = 'APPLICATION' // Carteira de integração externa
}
class WalletTypeVO {
private constructor(private readonly value: WalletType) {}
static create(value: string): WalletTypeVO {
if (!Object.values(WalletType).includes(value as WalletType)) {
throw new DomainError(`Invalid wallet type: ${value}`);
}
return new WalletTypeVO(value as WalletType);
}
static user(): WalletTypeVO {
return new WalletTypeVO(WalletType.USER);
}
static arena(): WalletTypeVO {
return new WalletTypeVO(WalletType.ARENA);
}
static event(): WalletTypeVO {
return new WalletTypeVO(WalletType.EVENT);
}
static application(): WalletTypeVO {
return new WalletTypeVO(WalletType.APPLICATION);
}
isUser(): boolean {
return this.value === WalletType.USER;
}
isArena(): boolean {
return this.value === WalletType.ARENA;
}
equals(other: WalletTypeVO): boolean {
return this.value === other.value;
}
toString(): string {
return this.value;
}
}Regras de Negócio
ExchangeRule - Conversão entre Valores
typescript
/**
* ExchangeRule - Regra de troca/conversão entre tipos de valor
*
* Exemplos:
* - 100 ARC = 1 hora de quadra
* - 50 ARC = 1 aula particular
* - 200 ARC = 1 uniforme
*/
class ExchangeRule {
constructor(
public readonly id: string,
private readonly _fromToken: TokenDefinitionId,
private readonly _toType: ExchangeType,
private readonly _rate: ExchangeRate,
private readonly _minAmount: TokenAmount,
private readonly _maxAmount: TokenAmount | null,
private readonly _isActive: boolean
) {}
static create(props: CreateExchangeRuleProps): ExchangeRule {
return new ExchangeRule(
crypto.randomUUID(),
props.fromToken,
props.toType,
props.rate,
props.minAmount,
props.maxAmount || null,
true
);
}
// Exemplo: Troca de ARC por hora de quadra
static arenaCourtHour(): ExchangeRule {
return ExchangeRule.create({
fromToken: TokenDefinitionId.arenaCoin(),
toType: ExchangeType.COURT_HOUR,
rate: ExchangeRate.create(100, 1), // 100 ARC = 1 hora
minAmount: TokenAmount.create(100)
});
}
canExchange(amount: TokenAmount): boolean {
if (!this._isActive) return false;
if (amount.isLessThan(this._minAmount)) return false;
if (this._maxAmount && amount.isGreaterThan(this._maxAmount)) return false;
return true;
}
calculateExchange(amount: TokenAmount): number {
return this._rate.calculate(amount);
}
allowsTransfer(from: WalletId, to: WalletId, amount: TokenAmount): boolean {
return this.canExchange(amount);
}
}
enum ExchangeType {
COURT_HOUR = 'COURT_HOUR',
PRIVATE_CLASS = 'PRIVATE_CLASS',
UNIFORM = 'UNIFORM',
EQUIPMENT = 'EQUIPMENT',
FOOD = 'FOOD',
BEVERAGE = 'BEVERAGE'
}
class ExchangeRate {
constructor(
private readonly tokenAmount: number,
private readonly valueAmount: number
) {}
static create(tokenAmount: number, valueAmount: number): ExchangeRate {
if (tokenAmount <= 0 || valueAmount <= 0) {
throw new DomainError('Exchange rate must be positive');
}
return new ExchangeRate(tokenAmount, valueAmount);
}
calculate(amount: TokenAmount): number {
return (amount.toNumber() / this.tokenAmount) * this.valueAmount;
}
}RewardRule - Regras de Recompensa
typescript
/**
* RewardRule - Regra de recompensa por ação/conquista
*
* Exemplos:
* - Participar de campeonato: 50 ARC
* - Vencer partida: 100 ARC
* - Fazer check-in: 10 ARC
* - Trazer amigo: 200 ARC
*/
class RewardRule {
constructor(
public readonly id: string,
private readonly _eventType: RewardEventType,
private readonly _token: TokenDefinitionId,
private readonly _amount: TokenAmount,
private readonly _conditions: RewardCondition[],
private readonly _maxPerUser: number | null,
private readonly _isActive: boolean
) {}
static participationReward(): RewardRule {
return new RewardRule(
crypto.randomUUID(),
RewardEventType.TOURNAMENT_PARTICIPATION,
TokenDefinitionId.arenaCoin(),
TokenAmount.create(50),
[],
null, // Sem limite
true
);
}
static victoryReward(): RewardRule {
return new RewardRule(
crypto.randomUUID(),
RewardEventType.MATCH_VICTORY,
TokenDefinitionId.arenaCoin(),
TokenAmount.create(100),
[
new RewardCondition('official_match', true)
],
null,
true
);
}
static checkInReward(): RewardRule {
return new RewardRule(
crypto.randomUUID(),
RewardEventType.CHECK_IN,
TokenDefinitionId.arenaCoin(),
TokenAmount.create(10),
[],
30, // Máximo 30 por mês
true
);
}
appliesTo(event: RewardEvent): boolean {
if (!this._isActive) return false;
if (event.type !== this._eventType) return false;
return this._conditions.every(cond => cond.isMet(event));
}
calculate(event: RewardEvent): TokenAmount {
if (!this.appliesTo(event)) {
return TokenAmount.zero();
}
// Aplicar multiplicadores baseados no evento
let multiplier = 1;
if (event.metadata.isFirstTime) {
multiplier *= 2; // Dobra na primeira vez
}
if (event.metadata.streak && event.metadata.streak > 5) {
multiplier *= 1.5; // Bônus por sequência
}
return this._amount.multiply(multiplier);
}
}
enum RewardEventType {
CHECK_IN = 'CHECK_IN',
MATCH_VICTORY = 'MATCH_VICTORY',
TOURNAMENT_PARTICIPATION = 'TOURNAMENT_PARTICIPATION',
TOURNAMENT_VICTORY = 'TOURNAMENT_VICTORY',
REFERRAL = 'REFERRAL',
ACHIEVEMENT_UNLOCKED = 'ACHIEVEMENT_UNLOCKED'
}
interface RewardEvent {
type: RewardEventType;
userId: string;
metadata: Record<string, any>;
occurredAt: DateTime;
}
class RewardCondition {
constructor(
private readonly field: string,
private readonly expectedValue: any
) {}
isMet(event: RewardEvent): boolean {
return event.metadata[this.field] === this.expectedValue;
}
}BetRule - Regras de Apostas
typescript
/**
* BetRule - Regra de aposta
*
* Define:
* - Limite mínimo/máximo de aposta
* - Odds/multiplicadores
* - Prazo para apostar
* - Distribuição de prêmios
*/
class BetRule {
constructor(
public readonly id: string,
private readonly _minBet: TokenAmount,
private readonly _maxBet: TokenAmount,
private readonly _deadlineBeforeMatch: Duration,
private readonly _prizeDistribution: PrizeDistribution,
private readonly _isActive: boolean
) {}
static createDefault(): BetRule {
return new BetRule(
crypto.randomUUID(),
TokenAmount.create(10), // Mínimo 10 ARC
TokenAmount.create(500), // Máximo 500 ARC
Duration.fromMinutes(30), // Encerra 30min antes
PrizeDistribution.winnerTakesAll(),
true
);
}
canPlaceBet(
matchId: MatchId,
amount: TokenAmount,
matchStartTime: DateTime
): boolean {
if (!this._isActive) return false;
// Verificar valores
if (amount.isLessThan(this._minBet)) return false;
if (amount.isGreaterThan(this._maxBet)) return false;
// Verificar prazo
const now = DateTime.now();
const deadline = matchStartTime.subtract(this._deadlineBeforeMatch);
if (now.isAfter(deadline)) return false;
return true;
}
calculatePrize(
totalPool: TokenAmount,
winningBets: BetTransaction[]
): Map<WalletId, TokenAmount> {
return this._prizeDistribution.distribute(totalPool, winningBets);
}
}
class PrizeDistribution {
constructor(
private readonly strategy: PrizeDistributionStrategy
) {}
static winnerTakesAll(): PrizeDistribution {
return new PrizeDistribution(
PrizeDistributionStrategy.WINNER_TAKES_ALL
);
}
static proportional(): PrizeDistribution {
return new PrizeDistribution(
PrizeDistributionStrategy.PROPORTIONAL
);
}
distribute(
totalPool: TokenAmount,
winningBets: BetTransaction[]
): Map<WalletId, TokenAmount> {
const prizes = new Map<WalletId, TokenAmount>();
if (winningBets.length === 0) {
return prizes;
}
switch (this.strategy) {
case PrizeDistributionStrategy.WINNER_TAKES_ALL:
// Divide igualmente entre vencedores
const prizePerWinner = totalPool.divide(winningBets.length);
winningBets.forEach(bet => {
prizes.set(bet.from!, prizePerWinner);
});
break;
case PrizeDistributionStrategy.PROPORTIONAL:
// Proporcional ao valor apostado
const totalBetAmount = TokenAmount.sum(
winningBets.map(bet => bet.amount)
);
winningBets.forEach(bet => {
const proportion = bet.amount.toNumber() / totalBetAmount.toNumber();
const prize = totalPool.multiply(proportion);
prizes.set(bet.from!, prize);
});
break;
}
return prizes;
}
}
enum PrizeDistributionStrategy {
WINNER_TAKES_ALL = 'WINNER_TAKES_ALL',
PROPORTIONAL = 'PROPORTIONAL'
}ExpirationRule - Expiração de Tokens
typescript
/**
* ExpirationRule - Regra de expiração de tokens
*
* Exemplos:
* - Tokens de cashback expiram em 90 dias
* - Tokens de campeonato não expiram
*/
class ExpirationRule {
constructor(
private readonly _enabled: boolean,
private readonly _expirationPeriod: Duration | null,
private readonly _warningPeriod: Duration | null
) {}
static noExpiration(): ExpirationRule {
return new ExpirationRule(false, null, null);
}
static expireAfter(period: Duration, warningPeriod?: Duration): ExpirationRule {
return new ExpirationRule(
true,
period,
warningPeriod || Duration.fromDays(7)
);
}
isExpired(lastUpdated: DateTime): boolean {
if (!this._enabled || !this._expirationPeriod) {
return false;
}
const expirationDate = lastUpdated.add(this._expirationPeriod);
return DateTime.now().isAfter(expirationDate);
}
shouldWarn(lastUpdated: DateTime): boolean {
if (!this._enabled || !this._expirationPeriod || !this._warningPeriod) {
return false;
}
const warningDate = lastUpdated
.add(this._expirationPeriod)
.subtract(this._warningPeriod);
const now = DateTime.now();
return now.isAfter(warningDate) && !this.isExpired(lastUpdated);
}
}Casos de Uso
1. CreateWalletUseCase
typescript
/**
* CreateWalletUseCase - Criar carteira para usuário/arena/evento
*/
class CreateWalletUseCase {
constructor(
private readonly walletRepository: WalletRepository,
private readonly eventDispatcher: EventDispatcher
) {}
async execute(request: CreateWalletRequest): Promise<CreateWalletResponse> {
// 1. Validar se já existe carteira
const existing = await this.walletRepository.findByOwnerAndType(
request.owner,
request.type
);
if (existing) {
throw new WalletAlreadyExistsError(
`Wallet already exists for owner ${request.owner.value} and type ${request.type}`
);
}
// 2. Criar wallet
const wallet = Wallet.create({
owner: request.owner,
type: request.type,
metadata: request.metadata
});
// 3. Persistir
await this.walletRepository.save(wallet);
// 4. Disparar eventos
await this.eventDispatcher.dispatchAll(wallet.getDomainEvents());
// 5. Retornar
return {
walletId: wallet.id,
owner: wallet.owner,
type: wallet.type,
createdAt: wallet.createdAt
};
}
}
interface CreateWalletRequest {
owner: OwnerId;
type: WalletTypeVO;
metadata?: WalletMetadata;
}
interface CreateWalletResponse {
walletId: WalletId;
owner: OwnerId;
type: WalletTypeVO;
createdAt: DateTime;
}2. TransferTokensUseCase
typescript
/**
* TransferTokensUseCase - Transferir tokens entre wallets
*/
class TransferTokensUseCase {
constructor(
private readonly walletRepository: WalletRepository,
private readonly transactionRepository: TransactionRepository,
private readonly ruleEngine: RuleEngine,
private readonly eventDispatcher: EventDispatcher
) {}
async execute(request: TransferTokensRequest): Promise<TransferTokensResponse> {
// 1. Criar transação
const transaction = TransferTransaction.create({
from: request.fromWallet,
to: request.toWallet,
amount: request.amount,
token: request.token,
reason: request.reason,
metadata: request.metadata
});
// 2. Executar transação (Template Method)
await transaction.execute(this.walletRepository, this.ruleEngine);
// 3. Persistir transação
await this.transactionRepository.save(transaction);
// 4. Disparar eventos
await this.eventDispatcher.dispatchAll(transaction.getDomainEvents());
// 5. Retornar
return {
transactionId: transaction.id,
status: transaction.status,
completedAt: transaction.completedAt!
};
}
}
interface TransferTokensRequest {
fromWallet: WalletId;
toWallet: WalletId;
amount: TokenAmount;
token: TokenDefinitionId;
reason: TransactionReason;
metadata?: TransactionMetadata;
}
interface TransferTokensResponse {
transactionId: TransactionId;
status: TransactionStatus;
completedAt: DateTime;
}3. PlaceBetUseCase
typescript
/**
* PlaceBetUseCase - Realizar aposta em partida
*/
class PlaceBetUseCase {
constructor(
private readonly walletRepository: WalletRepository,
private readonly transactionRepository: TransactionRepository,
private readonly matchRepository: MatchRepository,
private readonly ruleEngine: RuleEngine,
private readonly eventDispatcher: EventDispatcher
) {}
async execute(request: PlaceBetRequest): Promise<PlaceBetResponse> {
// 1. Carregar partida e validar
const match = await this.matchRepository.findById(request.matchId);
if (!match) {
throw new MatchNotFoundError();
}
if (match.hasStarted()) {
throw new BusinessRuleError('Cannot bet on started match');
}
// 2. Obter/criar EventWallet para a partida
let eventWallet = await this.walletRepository.findByOwnerAndType(
request.matchId,
WalletTypeVO.event()
);
if (!eventWallet) {
eventWallet = Wallet.create({
owner: request.matchId,
type: WalletTypeVO.event(),
metadata: WalletMetadata.forMatch(request.matchId)
});
await this.walletRepository.save(eventWallet);
}
// 3. Criar transação de aposta
const betTransaction = BetTransaction.create({
from: request.userWallet,
eventWallet: eventWallet.id,
amount: request.amount,
token: request.token,
matchId: request.matchId,
prediction: request.prediction,
metadata: TransactionMetadata.forBet(request.matchId)
});
// 4. Executar
await betTransaction.execute(this.walletRepository, this.ruleEngine);
// 5. Persistir
await this.transactionRepository.save(betTransaction);
// 6. Disparar eventos
await this.eventDispatcher.dispatchAll(betTransaction.getDomainEvents());
// 7. Retornar
return {
betId: betTransaction.id,
matchId: request.matchId,
amount: request.amount,
prediction: request.prediction,
lockedUntil: match.startTime
};
}
}
interface PlaceBetRequest {
userWallet: WalletId;
matchId: MatchId;
amount: TokenAmount;
token: TokenDefinitionId;
prediction: BetPrediction;
}
interface PlaceBetResponse {
betId: TransactionId;
matchId: MatchId;
amount: TokenAmount;
prediction: BetPrediction;
lockedUntil: DateTime;
}
class BetPrediction {
constructor(
public readonly type: BetPredictionType,
public readonly teamId?: string
) {}
static homeWin(teamId: string): BetPrediction {
return new BetPrediction(BetPredictionType.HOME_WIN, teamId);
}
static awayWin(teamId: string): BetPrediction {
return new BetPrediction(BetPredictionType.AWAY_WIN, teamId);
}
static draw(): BetPrediction {
return new BetPrediction(BetPredictionType.DRAW);
}
}
enum BetPredictionType {
HOME_WIN = 'HOME_WIN',
AWAY_WIN = 'AWAY_WIN',
DRAW = 'DRAW'
}4. RewardPlayerUseCase
typescript
/**
* RewardPlayerUseCase - Recompensar jogador por conquista
*/
class RewardPlayerUseCase {
constructor(
private readonly walletRepository: WalletRepository,
private readonly transactionRepository: TransactionRepository,
private readonly tokenDefinitionRepository: TokenDefinitionRepository,
private readonly eventDispatcher: EventDispatcher
) {}
async execute(request: RewardPlayerRequest): Promise<RewardPlayerResponse> {
// 1. Carregar token definition e calcular recompensa
const tokenDef = await this.tokenDefinitionRepository.findById(
request.token
);
if (!tokenDef) {
throw new TokenNotFoundError();
}
const rewardAmount = tokenDef.calculateReward(request.rewardEvent);
if (rewardAmount.equals(TokenAmount.zero())) {
throw new BusinessRuleError('No reward applicable for this event');
}
// 2. Carregar ArenaWallet (fonte dos tokens)
const arenaWallet = await this.walletRepository.findByType(
WalletTypeVO.arena()
);
if (!arenaWallet) {
throw new WalletNotFoundError('Arena wallet not found');
}
// 3. Criar transação de recompensa
const rewardTransaction = RewardTransaction.create({
from: arenaWallet.id,
to: request.playerWallet,
amount: rewardAmount,
token: request.token,
reason: TransactionReason.REWARD,
metadata: TransactionMetadata.forReward(request.rewardEvent)
});
// 4. Executar
await rewardTransaction.execute(
this.walletRepository,
new NoOpRuleEngine() // Rewards não passam por validação
);
// 5. Persistir
await this.transactionRepository.save(rewardTransaction);
// 6. Disparar eventos
await this.eventDispatcher.dispatchAll(rewardTransaction.getDomainEvents());
// 7. Retornar
return {
rewardId: rewardTransaction.id,
amount: rewardAmount,
reason: request.rewardEvent.type
};
}
}
interface RewardPlayerRequest {
playerWallet: WalletId;
token: TokenDefinitionId;
rewardEvent: RewardEvent;
}
interface RewardPlayerResponse {
rewardId: TransactionId;
amount: TokenAmount;
reason: RewardEventType;
}5. SettleBetsUseCase
typescript
/**
* SettleBetsUseCase - Liquidar apostas após fim da partida
*/
class SettleBetsUseCase {
constructor(
private readonly walletRepository: WalletRepository,
private readonly transactionRepository: TransactionRepository,
private readonly matchRepository: MatchRepository,
private readonly betRepository: BetRepository,
private readonly tokenDefinitionRepository: TokenDefinitionRepository,
private readonly eventDispatcher: EventDispatcher
) {}
async execute(request: SettleBetsRequest): Promise<SettleBetsResponse> {
// 1. Carregar partida e validar
const match = await this.matchRepository.findById(request.matchId);
if (!match) {
throw new MatchNotFoundError();
}
if (!match.isFinished()) {
throw new BusinessRuleError('Match not finished yet');
}
// 2. Carregar EventWallet
const eventWallet = await this.walletRepository.findByOwnerAndType(
request.matchId,
WalletTypeVO.event()
);
if (!eventWallet) {
throw new WalletNotFoundError('Event wallet not found');
}
// 3. Carregar todas as apostas
const allBets = await this.betRepository.findByMatch(request.matchId);
// 4. Determinar apostas vencedoras
const winningBets = allBets.filter(bet =>
this.isWinningBet(bet, match.result)
);
if (winningBets.length === 0) {
// Sem vencedores, arena fica com o pool
return {
totalBets: allBets.length,
winningBets: 0,
totalPrize: TokenAmount.zero()
};
}
// 5. Obter regras de premiação
const tokenDef = await this.tokenDefinitionRepository.findById(
allBets[0].token
);
const betRule = tokenDef!.rules.betRules[0]; // Simplificado
// 6. Calcular distribuição de prêmios
const totalPool = eventWallet.getAvailableBalance(allBets[0].token);
const prizes = betRule.calculatePrize(totalPool, winningBets);
// 7. Criar transações de prêmio para cada vencedor
const prizeTransactions: PrizeTransaction[] = [];
for (const [winnerId, prizeAmount] of prizes.entries()) {
const prizeTransaction = PrizeTransaction.create({
from: eventWallet.id,
to: winnerId,
amount: prizeAmount,
token: allBets[0].token,
reason: TransactionReason.PRIZE,
metadata: TransactionMetadata.forPrize(request.matchId)
});
await prizeTransaction.execute(
this.walletRepository,
new NoOpRuleEngine()
);
prizeTransactions.push(prizeTransaction);
}
// 8. Persistir transações
await this.transactionRepository.saveMany(prizeTransactions);
// 9. Unlock tokens dos perdedores (já foram debitados)
const losingBets = allBets.filter(bet => !winningBets.includes(bet));
for (const losingBet of losingBets) {
const wallet = await this.walletRepository.findById(losingBet.from!);
wallet!.unlock(losingBet.amount, losingBet.token);
await this.walletRepository.save(wallet!);
}
// 10. Disparar eventos
await this.eventDispatcher.dispatch(
new BetsSettledEvent(
request.matchId,
allBets.length,
winningBets.length,
totalPool
)
);
// 11. Retornar
return {
totalBets: allBets.length,
winningBets: winningBets.length,
totalPrize: totalPool
};
}
private isWinningBet(bet: BetTransaction, result: MatchResult): boolean {
switch (bet.prediction.type) {
case BetPredictionType.HOME_WIN:
return result.winner === 'HOME';
case BetPredictionType.AWAY_WIN:
return result.winner === 'AWAY';
case BetPredictionType.DRAW:
return result.winner === 'DRAW';
default:
return false;
}
}
}
interface SettleBetsRequest {
matchId: MatchId;
}
interface SettleBetsResponse {
totalBets: number;
winningBets: number;
totalPrize: TokenAmount;
}Fluxos de Transação
Fluxo 1: Jogador Ganha Recompensa por Participação
┌─────────────┐
│ Player │
│ Finishes │
│ Match │
└──────┬──────┘
│
▼
┌──────────────────────────────────────────────┐
│ 1. MatchFinishedEvent Dispatched │
└──────┬───────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────┐
│ 2. RewardEventHandler Listens │
│ - Identify reward rules applicable │
│ - Calculate reward amount (50 ARC) │
└──────┬───────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────┐
│ 3. RewardPlayerUseCase.execute() │
│ - Load ArenaWallet │
│ - Load PlayerWallet │
│ - Create RewardTransaction │
└──────┬───────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────┐
│ 4. Transaction.execute() │
│ - ArenaWallet.debit(50 ARC) │
│ - PlayerWallet.credit(50 ARC) │
└──────┬───────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────┐
│ 5. Events Dispatched │
│ - TokensCreditedEvent │
│ - RewardTransactionCompletedEvent │
└──────┬───────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────┐
│ 6. Notification Sent to Player │
│ "You earned 50 ARC for playing!" │
└──────────────────────────────────────────────┘Fluxo 2: Jogador Aposta em Partida
┌─────────────┐
│ Player │
│ Places Bet │
│ 100 ARC │
└──────┬──────┘
│
▼
┌──────────────────────────────────────────────┐
│ 1. PlaceBetUseCase.execute() │
│ - Validate match not started │
│ - Validate bet amount (min/max) │
└──────┬───────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────┐
│ 2. Load/Create EventWallet for match │
└──────┬───────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────┐
│ 3. Create BetTransaction │
│ - From: PlayerWallet │
│ - To: EventWallet │
│ - Amount: 100 ARC │
│ - Prediction: HomeWin │
└──────┬───────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────┐
│ 4. Transaction.execute() │
│ - PlayerWallet.lock(100 ARC) │
│ - EventWallet.credit(100 ARC) │
└──────┬───────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────┐
│ 5. BetPlacedEvent Dispatched │
└──────┬───────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────┐
│ 6. Notification Sent │
│ "Bet placed! 100 ARC locked" │
└──────────────────────────────────────────────┘
│
│ (Wait for match to finish)
│
▼
┌──────────────────────────────────────────────┐
│ 7. Match Finishes │
│ - MatchFinishedEvent │
└──────┬───────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────┐
│ 8. SettleBetsUseCase.execute() │
│ - Identify winning bets │
│ - Calculate prize distribution │
│ - Transfer prizes to winners │
│ - Unlock tokens for losers │
└──────┬───────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────┐
│ 9. If Won: │
│ PlayerWallet.credit(200 ARC) + unlock │
│ │
│ If Lost: │
│ PlayerWallet.unlock(100 ARC already gone)│
└──────────────────────────────────────────────┘Fluxo 3: Patrocinador Financia Prêmios
┌─────────────┐
│ Sponsor │
│ Deposits │
│ 10,000 ARC │
└──────┬──────┘
│
▼
┌──────────────────────────────────────────────┐
│ 1. CreateSponsorWalletUseCase │
│ (if not exists) │
└──────┬───────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────┐
│ 2. Admin converts real money to ARC │
│ $1000 USD → 10,000 ARC │
│ (via ConversionRule) │
└──────┬───────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────┐
│ 3. CreditTransaction to SponsorWallet │
│ - System mints 10,000 ARC │
│ - Credits SponsorWallet │
└──────┬───────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────┐
│ 4. TransferToArenaWallet │
│ - From: SponsorWallet │
│ - To: ArenaWallet │
│ - Amount: 10,000 ARC │
│ - Reason: TOURNAMENT_PRIZE │
└──────┬───────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────┐
│ 5. Tournament Prizes Now Available │
│ ArenaWallet balance += 10,000 ARC │
└──────────────────────────────────────────────┘Fluxo 4: Jogador Troca ARC por Hora de Quadra
┌─────────────┐
│ Player │
│ Requests │
│ Court Hour │
└──────┬──────┘
│
▼
┌──────────────────────────────────────────────┐
│ 1. ExchangeTokensUseCase.execute() │
│ - exchangeType: COURT_HOUR │
│ - amount: 100 ARC │
└──────┬───────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────┐
│ 2. Validate ExchangeRule │
│ - 100 ARC = 1 Court Hour │
│ - Min: 100 ARC (✓) │
│ - Player has balance (✓) │
└──────┬───────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────┐
│ 3. Create TransferTransaction │
│ - From: PlayerWallet │
│ - To: ArenaWallet │
│ - Amount: 100 ARC │
│ - Reason: EXCHANGE_COURT_HOUR │
└──────┬───────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────┐
│ 4. Transaction.execute() │
│ - PlayerWallet.debit(100 ARC) │
│ - ArenaWallet.credit(100 ARC) │
└──────┬───────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────┐
│ 5. Create Court Reservation Credit │
│ (Integration with Booking System) │
│ - User gets 1 hour credit │
└──────┬───────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────┐
│ 6. ExchangeCompletedEvent Dispatched │
└──────┬───────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────┐
│ 7. Notification Sent │
│ "You exchanged 100 ARC for 1 court hour" │
└──────────────────────────────────────────────┘Segurança e Auditoria
Event Sourcing & Ledger
typescript
/**
* Event Store - Armazena todos os eventos de domínio
* Garante auditabilidade completa e rastreabilidade
*/
class EventStore {
constructor(private readonly db: PrismaClient) {}
async append(events: DomainEvent[]): Promise<void> {
const records = events.map(event => ({
id: crypto.randomUUID(),
aggregateId: event.aggregateId,
aggregateType: event.aggregateType,
eventType: event.constructor.name,
eventData: JSON.stringify(event),
metadata: JSON.stringify(event.metadata),
occurredAt: event.occurredAt.toISOString(),
version: event.version
}));
await this.db.eventStoreEntry.createMany({
data: records
});
}
async getEventsForAggregate(
aggregateId: string,
fromVersion: number = 0
): Promise<DomainEvent[]> {
const records = await this.db.eventStoreEntry.findMany({
where: {
aggregateId,
version: { gte: fromVersion }
},
orderBy: { version: 'asc' }
});
return records.map(record => this.deserializeEvent(record));
}
async getAllEvents(
filters?: EventFilters,
pagination?: Pagination
): Promise<DomainEvent[]> {
const records = await this.db.eventStoreEntry.findMany({
where: this.buildWhereClause(filters),
orderBy: { occurredAt: 'asc' },
skip: pagination?.offset,
take: pagination?.limit
});
return records.map(record => this.deserializeEvent(record));
}
private deserializeEvent(record: EventStoreEntry): DomainEvent {
const EventClass = this.getEventClass(record.eventType);
const eventData = JSON.parse(record.eventData);
return new EventClass(eventData);
}
private getEventClass(eventType: string): any {
// Event Registry Pattern
return EventRegistry.get(eventType);
}
}
/**
* Transaction Ledger - View materializada para queries rápidas
*/
class TransactionLedger {
constructor(private readonly db: PrismaClient) {}
async record(transaction: Transaction): Promise<void> {
await this.db.transactionLedger.create({
data: {
transactionId: transaction.id.value,
type: transaction.type,
from: transaction.from?.value,
to: transaction.to.value,
amount: transaction.amount.toNumber(),
token: transaction.token.value,
status: transaction.status,
reason: transaction.reason,
metadata: transaction.metadata,
createdAt: transaction.createdAt.toDate(),
completedAt: transaction.completedAt?.toDate()
}
});
}
async getHistory(
walletId: WalletId,
filters?: TransactionFilters
): Promise<TransactionLedgerEntry[]> {
return this.db.transactionLedger.findMany({
where: {
OR: [
{ from: walletId.value },
{ to: walletId.value }
],
...this.buildFilters(filters)
},
orderBy: { createdAt: 'desc' }
});
}
async getAuditTrail(
startDate: DateTime,
endDate: DateTime
): Promise<AuditReport> {
const entries = await this.db.transactionLedger.findMany({
where: {
createdAt: {
gte: startDate.toDate(),
lte: endDate.toDate()
}
}
});
return this.generateAuditReport(entries);
}
private generateAuditReport(entries: TransactionLedgerEntry[]): AuditReport {
return {
totalTransactions: entries.length,
totalVolume: entries.reduce((sum, e) => sum + e.amount, 0),
byType: this.groupByType(entries),
byStatus: this.groupByStatus(entries),
byToken: this.groupByToken(entries)
};
}
}Access Control
typescript
/**
* Wallet Access Control - Controle de acesso a wallets
*/
class WalletAccessControl {
async canAccess(userId: UserId, walletId: WalletId): Promise<boolean> {
const wallet = await this.walletRepository.findById(walletId);
if (!wallet) return false;
// Owner sempre pode acessar
if (wallet.owner.equals(userId)) return true;
// Admin pode acessar qualquer wallet
if (await this.isAdmin(userId)) return true;
// Verificar permissões delegadas
const permission = await this.permissionRepository.findByUserAndWallet(
userId,
walletId
);
return permission?.canRead || false;
}
async canTransfer(
userId: UserId,
fromWallet: WalletId,
toWallet: WalletId
): Promise<boolean> {
const wallet = await this.walletRepository.findById(fromWallet);
if (!wallet) return false;
// Owner pode transferir
if (wallet.owner.equals(userId)) return true;
// Verificar permissão de transferência
const permission = await this.permissionRepository.findByUserAndWallet(
userId,
fromWallet
);
return permission?.canTransfer || false;
}
}
/**
* Transaction Authorization
*/
class TransactionAuthorizer {
async authorize(
transaction: Transaction,
initiator: UserId
): Promise<AuthorizationResult> {
// 1. Verificar access control
const canAccess = await this.walletAccessControl.canAccess(
initiator,
transaction.from || transaction.to
);
if (!canAccess) {
return AuthorizationResult.denied('Insufficient permissions');
}
// 2. Verificar limites (rate limiting, amount limits)
const limitsCheck = await this.checkLimits(transaction, initiator);
if (!limitsCheck.passed) {
return AuthorizationResult.denied(limitsCheck.reason);
}
// 3. Verificar compliance (KYC, AML)
const complianceCheck = await this.checkCompliance(transaction);
if (!complianceCheck.passed) {
return AuthorizationResult.denied(complianceCheck.reason);
}
// 4. Tudo OK
return AuthorizationResult.allowed();
}
private async checkLimits(
transaction: Transaction,
initiator: UserId
): Promise<LimitCheckResult> {
// Limite por transação
const maxPerTransaction = TokenAmount.create(1000);
if (transaction.amount.isGreaterThan(maxPerTransaction)) {
return LimitCheckResult.failed('Amount exceeds maximum per transaction');
}
// Limite diário
const todayTotal = await this.getTodayTotal(initiator, transaction.token);
const dailyLimit = TokenAmount.create(5000);
if (todayTotal.add(transaction.amount).isGreaterThan(dailyLimit)) {
return LimitCheckResult.failed('Daily limit exceeded');
}
return LimitCheckResult.passed();
}
private async checkCompliance(
transaction: Transaction
): Promise<ComplianceCheckResult> {
// Para tokens de alto valor, verificar compliance
if (transaction.amount.isGreaterThan(TokenAmount.create(10000))) {
// Verificar KYC do usuário
// Verificar watchlists
// etc.
}
return ComplianceCheckResult.passed();
}
}Fraud Detection
typescript
/**
* Fraud Detection Service
*/
class FraudDetectionService {
constructor(
private readonly transactionRepository: TransactionRepository,
private readonly userRepository: UserRepository
) {}
async analyzeTransaction(
transaction: Transaction
): Promise<FraudAnalysisResult> {
const signals: FraudSignal[] = [];
// 1. Verificar padrões suspeitos
const patterns = await this.detectSuspiciousPatterns(transaction);
signals.push(...patterns);
// 2. Verificar velocidade (muitas transações em pouco tempo)
const velocity = await this.checkVelocity(transaction);
if (velocity.isSuspicious) {
signals.push(velocity.signal);
}
// 3. Verificar valores anômalos
const anomaly = await this.detectAnomalies(transaction);
if (anomaly.isAnomaly) {
signals.push(anomaly.signal);
}
// 4. Calcular risk score
const riskScore = this.calculateRiskScore(signals);
// 5. Decidir ação
if (riskScore > 0.8) {
return FraudAnalysisResult.block('High fraud risk');
} else if (riskScore > 0.5) {
return FraudAnalysisResult.review('Medium fraud risk');
} else {
return FraudAnalysisResult.allow();
}
}
private async detectSuspiciousPatterns(
transaction: Transaction
): Promise<FraudSignal[]> {
const signals: FraudSignal[] = [];
// Padrão: Transferências circulares (A → B → C → A)
const isCircular = await this.isCircularTransfer(transaction);
if (isCircular) {
signals.push(FraudSignal.circularTransfer());
}
// Padrão: Conta nova com alto volume
const isNewAccount = await this.isNewAccount(transaction.from!);
const isHighVolume = transaction.amount.isGreaterThan(TokenAmount.create(1000));
if (isNewAccount && isHighVolume) {
signals.push(FraudSignal.newAccountHighVolume());
}
return signals;
}
private async checkVelocity(
transaction: Transaction
): Promise<VelocityCheck> {
const recentTransactions = await this.transactionRepository.findRecent(
transaction.from!,
Duration.fromMinutes(10)
);
// Mais de 10 transações em 10 minutos é suspeito
if (recentTransactions.length > 10) {
return VelocityCheck.suspicious(
FraudSignal.highVelocity(recentTransactions.length)
);
}
return VelocityCheck.normal();
}
private calculateRiskScore(signals: FraudSignal[]): number {
if (signals.length === 0) return 0;
const weights = {
[FraudSignalType.CIRCULAR_TRANSFER]: 0.4,
[FraudSignalType.HIGH_VELOCITY]: 0.3,
[FraudSignalType.NEW_ACCOUNT_HIGH_VOLUME]: 0.5,
[FraudSignalType.ANOMALY]: 0.2
};
const score = signals.reduce((sum, signal) => {
return sum + (weights[signal.type] || 0.1);
}, 0);
return Math.min(score, 1.0); // Cap at 1.0
}
}Integrações
Antóctica Ecosystem
typescript
/**
* ApplicationWallet - Integração com outras apps do ecossistema
*/
interface AntocticaIntegration {
applicationId: string;
applicationName: string;
walletId: WalletId;
apiKey: string;
webhookUrl: string;
}
/**
* Cross-Application Transfer
*/
class CrossApplicationTransferUseCase {
async execute(
request: CrossApplicationTransferRequest
): Promise<CrossApplicationTransferResponse> {
// 1. Validar aplicação externa
const externalApp = await this.applicationRepository.findById(
request.targetApplicationId
);
if (!externalApp || !externalApp.isActive) {
throw new ApplicationNotFoundError();
}
// 2. Obter ApplicationWallet da app externa
let appWallet = await this.walletRepository.findByOwnerAndType(
externalApp.id,
WalletTypeVO.application()
);
if (!appWallet) {
appWallet = Wallet.create({
owner: externalApp.id,
type: WalletTypeVO.application(),
metadata: WalletMetadata.forApplication(externalApp.id)
});
await this.walletRepository.save(appWallet);
}
// 3. Executar transferência
const transfer = await this.transferTokensUseCase.execute({
fromWallet: request.fromWallet,
toWallet: appWallet.id,
amount: request.amount,
token: request.token,
reason: TransactionReason.CROSS_APP_TRANSFER,
metadata: TransactionMetadata.forCrossAppTransfer(
request.targetApplicationId
)
});
// 4. Notificar aplicação externa via webhook
await this.notifyExternalApplication(externalApp, transfer);
return {
transferId: transfer.transactionId,
targetWallet: appWallet.id
};
}
private async notifyExternalApplication(
app: Application,
transfer: TransferTokensResponse
): Promise<void> {
await this.httpClient.post(app.webhookUrl, {
event: 'tokens.received',
data: {
transactionId: transfer.transactionId,
amount: transfer.amount,
token: transfer.token
},
signature: this.generateSignature(app.apiKey, transfer)
});
}
}Payment Gateway Integration (Conversão Real)
typescript
/**
* ConvertRealToCrypto - Conversão de dinheiro real para ArenaCoin
* (se permitido pela arena)
*/
class ConvertRealToTokenUseCase {
constructor(
private readonly paymentGateway: PaymentGateway,
private readonly walletRepository: WalletRepository,
private readonly conversionRateService: ConversionRateService
) {}
async execute(
request: ConvertRealToTokenRequest
): Promise<ConvertRealToTokenResponse> {
// 1. Validar se conversão é permitida
const tokenDef = await this.tokenDefinitionRepository.findById(
request.token
);
if (!tokenDef!.rules.conversionRules.length) {
throw new BusinessRuleError('Token does not allow real money conversion');
}
// 2. Obter taxa de conversão
const rate = await this.conversionRateService.getRate(
request.currency,
request.token
);
const tokenAmount = rate.convert(request.amount);
// 3. Processar pagamento real
const payment = await this.paymentGateway.charge({
amount: request.amount,
currency: request.currency,
paymentMethod: request.paymentMethod,
description: `Purchase ${tokenAmount.toNumber()} ${tokenDef!.symbol}`
});
if (!payment.success) {
throw new PaymentFailedError(payment.error);
}
// 4. Creditar tokens na wallet
const creditTransaction = CreditTransaction.create({
to: request.wallet,
amount: tokenAmount,
token: request.token,
reason: TransactionReason.PURCHASE,
metadata: TransactionMetadata.forPurchase(payment.id)
});
await creditTransaction.execute(
this.walletRepository,
this.ruleEngine
);
await this.transactionRepository.save(creditTransaction);
// 5. Retornar
return {
paymentId: payment.id,
transactionId: creditTransaction.id,
tokenAmount,
rate: rate.value
};
}
}Implementação Técnica
Database Schema (Prisma)
prisma
// schema.prisma
model Wallet {
id String @id @default(uuid())
ownerId String
ownerType String // USER, ARENA, EVENT, APPLICATION
type String // WalletType enum
status String @default("ACTIVE")
metadata Json?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
balances TokenBalance[]
outgoingTransactions Transaction[] @relation("FromWallet")
incomingTransactions Transaction[] @relation("ToWallet")
@@unique([ownerId, type])
@@index([ownerId])
@@index([type])
}
model TokenBalance {
id String @id @default(uuid())
walletId String
tokenDefinitionId String
available Decimal @default(0)
locked Decimal @default(0)
lastUpdated DateTime @default(now())
wallet Wallet @relation(fields: [walletId], references: [id])
tokenDefinition TokenDefinition @relation(fields: [tokenDefinitionId], references: [id])
@@unique([walletId, tokenDefinitionId])
@@index([walletId])
}
model Transaction {
id String @id @default(uuid())
type String // CREDIT, DEBIT, TRANSFER, REWARD, BET, etc.
fromWallet String?
toWallet String
amount Decimal
token String
status String // PENDING, COMPLETED, FAILED
reason String
metadata Json?
createdAt DateTime @default(now())
completedAt DateTime?
from Wallet? @relation("FromWallet", fields: [fromWallet], references: [id])
to Wallet @relation("ToWallet", fields: [toWallet], references: [id])
@@index([fromWallet])
@@index([toWallet])
@@index([type])
@@index([status])
@@index([createdAt])
}
model TokenDefinition {
id String @id @default(uuid())
name String @unique
symbol String @unique
description String
rules Json // TokenRules
isActive Boolean @default(true)
createdAt DateTime @default(now())
balances TokenBalance[]
}
model EventStoreEntry {
id String @id @default(uuid())
aggregateId String
aggregateType String
eventType String
eventData Json
metadata Json?
occurredAt DateTime
version Int
@@index([aggregateId, version])
@@index([eventType])
@@index([occurredAt])
}
model TransactionLedger {
id String @id @default(uuid())
transactionId String @unique
type String
from String?
to String
amount Decimal
token String
status String
reason String
metadata Json?
createdAt DateTime
completedAt DateTime?
@@index([from])
@@index([to])
@@index([type])
@@index([createdAt])
}API Endpoints (REST)
typescript
// routes/wallet.routes.ts
/**
* POST /api/wallets
* Criar nova carteira
*/
router.post('/wallets', async (req, res) => {
const result = await createWalletUseCase.execute({
owner: OwnerId.create(req.body.ownerId),
type: WalletTypeVO.create(req.body.type),
metadata: req.body.metadata
});
res.status(201).json(result);
});
/**
* GET /api/wallets/:id
* Obter detalhes da carteira
*/
router.get('/wallets/:id', async (req, res) => {
const wallet = await walletRepository.findById(
WalletId.create(req.params.id)
);
res.json(WalletMapper.toDto(wallet));
});
/**
* GET /api/wallets/:id/balance
* Obter saldo da carteira
*/
router.get('/wallets/:id/balance', async (req, res) => {
const wallet = await walletRepository.findById(
WalletId.create(req.params.id)
);
const balances = wallet.balances.map(balance => ({
token: balance.tokenDefinition,
available: balance.available.toNumber(),
locked: balance.locked.toNumber(),
total: balance.total.toNumber()
}));
res.json({ balances });
});
/**
* GET /api/wallets/:id/transactions
* Obter histórico de transações
*/
router.get('/wallets/:id/transactions', async (req, res) => {
const history = await transactionLedger.getHistory(
WalletId.create(req.params.id),
{
type: req.query.type,
from: req.query.from,
to: req.query.to,
limit: parseInt(req.query.limit || '50'),
offset: parseInt(req.query.offset || '0')
}
);
res.json({ transactions: history });
});
/**
* POST /api/transactions/transfer
* Transferir tokens entre wallets
*/
router.post('/transactions/transfer', async (req, res) => {
const result = await transferTokensUseCase.execute({
fromWallet: WalletId.create(req.body.from),
toWallet: WalletId.create(req.body.to),
amount: TokenAmount.create(req.body.amount),
token: TokenDefinitionId.create(req.body.token),
reason: TransactionReason.create(req.body.reason),
metadata: req.body.metadata
});
res.status(201).json(result);
});
/**
* POST /api/transactions/bet
* Apostar em partida
*/
router.post('/transactions/bet', async (req, res) => {
const result = await placeBetUseCase.execute({
userWallet: WalletId.create(req.body.walletId),
matchId: MatchId.create(req.body.matchId),
amount: TokenAmount.create(req.body.amount),
token: TokenDefinitionId.create(req.body.token),
prediction: BetPrediction.fromDto(req.body.prediction)
});
res.status(201).json(result);
});
/**
* POST /api/transactions/exchange
* Trocar tokens por serviço/produto
*/
router.post('/transactions/exchange', async (req, res) => {
const result = await exchangeTokensUseCase.execute({
wallet: WalletId.create(req.body.walletId),
amount: TokenAmount.create(req.body.amount),
token: TokenDefinitionId.create(req.body.token),
exchangeType: ExchangeType.create(req.body.exchangeType)
});
res.status(201).json(result);
});WebSocket Real-Time Updates
typescript
// websocket/wallet.gateway.ts
@WebSocketGateway()
export class WalletGateway {
@WebSocketServer()
server: Server;
constructor(private readonly eventBus: EventBus) {
// Subscribe to wallet events
this.eventBus.subscribe(TokensCreditedEvent, this.onTokensCredited.bind(this));
this.eventBus.subscribe(TokensDebitedEvent, this.onTokensDebited.bind(this));
this.eventBus.subscribe(TransactionCompletedEvent, this.onTransactionCompleted.bind(this));
}
@SubscribeMessage('subscribe:wallet')
handleSubscribeWallet(
@ConnectedSocket() client: Socket,
@MessageBody() data: { walletId: string }
) {
// Add client to room
client.join(`wallet:${data.walletId}`);
}
@SubscribeMessage('unsubscribe:wallet')
handleUnsubscribeWallet(
@ConnectedSocket() client: Socket,
@MessageBody() data: { walletId: string }
) {
client.leave(`wallet:${data.walletId}`);
}
private async onTokensCredited(event: TokensCreditedEvent) {
this.server.to(`wallet:${event.walletId}`).emit('balance:updated', {
walletId: event.walletId,
token: event.token,
amount: event.amount,
type: 'credit'
});
}
private async onTokensDebited(event: TokensDebitedEvent) {
this.server.to(`wallet:${event.walletId}`).emit('balance:updated', {
walletId: event.walletId,
token: event.token,
amount: event.amount,
type: 'debit'
});
}
private async onTransactionCompleted(event: TransactionCompletedEvent) {
// Notify both sender and receiver
this.server.to(`wallet:${event.fromWallet}`).emit('transaction:completed', {
transactionId: event.transactionId,
type: event.type,
status: 'completed'
});
this.server.to(`wallet:${event.toWallet}`).emit('transaction:completed', {
transactionId: event.transactionId,
type: event.type,
status: 'completed'
});
}
}Métricas e Observabilidade
typescript
/**
* Wallet Metrics
*/
class WalletMetrics {
async getTotalCirculation(token: TokenDefinitionId): Promise<TokenAmount> {
const result = await this.db.$queryRaw`
SELECT SUM(available + locked) as total
FROM TokenBalance
WHERE tokenDefinitionId = ${token.value}
`;
return TokenAmount.create(result[0].total || 0);
}
async getActiveWallets(): Promise<number> {
return this.db.wallet.count({
where: { status: 'ACTIVE' }
});
}
async getTransactionVolume(
period: Duration
): Promise<TransactionVolumeMetrics> {
const since = DateTime.now().subtract(period);
const result = await this.db.transaction.aggregate({
where: {
createdAt: { gte: since.toDate() },
status: 'COMPLETED'
},
_sum: { amount: true },
_count: true
});
return {
totalAmount: TokenAmount.create(result._sum.amount || 0),
totalCount: result._count,
period: period.toString()
};
}
async getTopHolders(
token: TokenDefinitionId,
limit: number = 10
): Promise<WalletBalance[]> {
const balances = await this.db.tokenBalance.findMany({
where: { tokenDefinitionId: token.value },
orderBy: { available: 'desc' },
take: limit,
include: { wallet: true }
});
return balances.map(b => ({
walletId: b.walletId,
owner: b.wallet.ownerId,
balance: TokenAmount.create(b.available)
}));
}
}Considerações Finais
Próximos Passos
Implementar Sistema de Níveis (Gamificação)
- Bronze, Silver, Gold, Platinum
- Benefícios por nível
- Evolução baseada em atividade
Marketplace de Tokens
- P2P trading (se permitido)
- Auction system
- Price discovery
Mobile Wallet
- App nativo iOS/Android
- QR Code scanning
- Push notifications
Analytics Dashboard
- Token economics
- User behavior
- Fraud detection
Smart Contracts (Futuro)
- Migração para blockchain real
- Descentralização
- Interoperabilidade
Performance Considerations
- Caching: Redis para balanços frequentes
- Read Replicas: Queries de leitura em replicas
- CQRS: Separação de write/read models
- Event Sourcing: Rebuild state from events
- Partitioning: Sharding por wallet ranges
Compliance
- KYC/AML: Para conversões > threshold
- GDPR: Right to erasure (event anonymization)
- SOC 2: Audit trails
- PCI-DSS: Payment data handling
"In code we trust, but we verify with events." — Domain-Driven Design Community
Documento criado por: Engenheiro (Arquiteto de Software Sênior)
Data: 2026-01-09
Versão: 1.0