import "mainnet-hub-types"
import "mainnet-jetton-types"
import "mainnet-position-types"

contract HalvoraMainnetHubCandidate {
    author: "Halvora"
    version: "1.0.0-mainnet-candidate"
    description: "Upgradeable USDT Hub candidate with serialized payouts and isolated fee accounting"
    storage: MainnetHubStorage
    incomingMessages: MainnetHubMessage
}

type MainnetHubMessage =
    | MainnetTransferNotification
    | SetMainnetDepositsPaused
    | InitializeMainnetJettonWallet
    | MainnetPositionDepositAccepted
    | MainnetPositionClaimIntent
    | MainnetPositionPayoutRecorded
    | DispatchMainnetPayout
    | RetryMainnetPayoutConfirmation
    | RequestMainnetDeveloperFeePayout
    | RequestMainnetReferralFeePayout
    | MainnetJettonExcesses
    | FreezeMainnetUpgrades
    | UpgradeMainnetHub
    | UpgradeMainnetPosition

type MainnetHubBounceMessage =
    | MainnetCreditPositionDeposit
    | MainnetJettonTransfer
    | ConfirmMainnetPayout

fun claimIntentKey(position: address, cycleId: uint32, positionQueryId: uint64): uint256 {
    return beginCell()
        .storeAddress(position)
        .storeUint(cycleId, 32)
        .storeUint(positionQueryId, 64)
        .endCell()
        .hash();
}

fun parseDepositPayload(raw: RemainingBitsAndRefs): MainnetDepositPayload {
    var envelope = raw;
    if (envelope.remainingBitsCount() < 1) {
        return MainnetDepositPayload { referrer: null };
    }

    val payloadByRef = envelope.loadBool();
    var payload = envelope;
    if (payloadByRef) {
        if (envelope.remainingRefsCount() < 1) {
            return MainnetDepositPayload { referrer: null };
        }
        payload = envelope.loadRef().beginParse();
    }

    if (payload.remainingBitsCount() < 34 || payload.loadUint(32) != 0x48414c56) {
        return MainnetDepositPayload { referrer: null };
    }

    val addressPrefix = payload.preloadUint(2);
    if (addressPrefix == 0) {
        return MainnetDepositPayload { referrer: null };
    }
    if (addressPrefix != 2 || payload.remainingBitsCount() < 267 || payload.preloadUint(3) != 4) {
        return MainnetDepositPayload { referrer: null };
    }
    return MainnetDepositPayload { referrer: payload.loadAddressOpt() };
}

fun initialMainnetPositionStorage(owner: address): MainnetPositionStorage {
    val empty = emptyTier().toCell();
    return MainnetPositionStorage {
        hub: contract.getAddress(),
        owner,
        cycleId: 1,
        completed: false,
        lastAccrualAt: 0,
        pendingActivationAt: 0,
        lastClaimAt: 0,
        lastDepositQueryId: 0,
        lastPayoutQueryId: 0,
        tier3: empty,
        tier4: empty,
        tier5: empty,
        totals: emptyTotals().toCell(),
    };
}

fun configuredJettonWalletAddress(storage: MainnetHubStorage, owner: address): address {
    val jetton = storage.jetton.load();
    val walletCode = jetton.jettonWalletCode;
    assert (walletCode != null) throw MainnetHubErrors.InvalidJettonConfiguration;
    return mainnetJettonWalletAddress(jetton.expectedJettonMaster, owner, walletCode!);
}

fun sendClaimAccepted(
    storage: MainnetHubStorage,
    queueId: uint64,
    claim: MainnetQueuedPayout,
): void {
    val accepted = createMessage({
        bounce: BounceMode.NoBounce,
        value: ton("0.02"),
        dest: positionAddress(storage, claim.recipient),
        body: MainnetClaimIntentAccepted {
            positionQueryId: claim.positionQueryId,
            queueId,
            cycleId: claim.cycleId,
            owner: claim.recipient,
            amount: claim.amount,
        },
    });
    accepted.send(SEND_MODE_REGULAR);
}

fun sendPayoutConfirmation(
    storage: MainnetHubStorage,
    queueId: uint64,
    claim: MainnetQueuedPayout,
): void {
    val confirmation = createMessage({
        bounce: BounceMode.RichBounce,
        value: ton("0.08"),
        dest: positionAddress(storage, claim.recipient),
        body: ConfirmMainnetPayout {
            queryId: queueId,
            amount: claim.amount,
        },
    });
    confirmation.send(SEND_MODE_BOUNCE_ON_ACTION_FAIL);
}

fun positionState(storage: MainnetHubStorage, owner: address): ContractState {
    return ContractState {
        code: storage.jetton.load().positionCode,
        data: initialMainnetPositionStorage(owner).toCell(),
    };
}

fun positionAddress(storage: MainnetHubStorage, owner: address): address {
    return AutoDeployAddress { stateInit: positionState(storage, owner) }.calculateAddress();
}

fun MainnetHubStorage.loadPayouts(self): MainnetHubPayoutState {
    return self.runtime.load().payouts.load();
}

fun MainnetHubStorage.savePayouts(mutate self, payouts: MainnetHubPayoutState): void {
    var runtime = self.runtime.load();
    runtime.payouts = payouts.toCell();
    self.runtime = runtime.toCell();
}

fun MainnetHubStorage.enqueuePayout(
    mutate self,
    payout: MainnetQueuedPayout,
): uint64 {
    var payouts = self.loadPayouts();
    payouts.nextQueueId += 1;
    val queueId = payouts.nextQueueId;
    payouts.claimsById.set(queueId, payout);
    if (payout.kind == MAINNET_PAYOUT_USER_REWARD) {
        payouts.claimDebt += payout.amount;
    }
    self.savePayouts(payouts);
    return queueId;
}

fun MainnetHubStorage.enqueueRefund(
    mutate self,
    recipient: address,
    amount: coins,
): uint64 {
    var totals = self.totals.load();
    totals.refundsQueued += amount;
    self.totals = totals.toCell();
    return self.enqueuePayout(
        MainnetQueuedPayout {
            recipient,
            kind: MAINNET_PAYOUT_REFUND,
            cycleId: 0,
            positionQueryId: 0,
            amount,
            tokenTransferred: false,
        },
    );
}

fun onInternalMessage(in: InMessage) {
    val msg = lazy MainnetHubMessage.fromSlice(in.body);

    match (msg) {
        UpgradeMainnetHub => {
            val storage = lazy MainnetHubStorage.load();
            val authorities = storage.authorities.load();
            assert (!storage.upgradesFrozen) throw MainnetHubErrors.UpgradesPermanentlyFrozen;
            assert (
                in.senderAddress == authorities.upgradeController
            ) throw MainnetHubErrors.NotUpgradeController;
            contract.setCodePostponed(msg.newCode);
        }
        UpgradeMainnetPosition => {
            val storage = lazy MainnetHubStorage.load();
            val authorities = storage.authorities.load();
            assert (!storage.upgradesFrozen) throw MainnetHubErrors.UpgradesPermanentlyFrozen;
            assert (
                in.senderAddress == authorities.upgradeController
            ) throw MainnetHubErrors.NotUpgradeController;

            val upgrade = createMessage({
                bounce: BounceMode.RichBounce,
                value: ton("0.05"),
                dest: positionAddress(storage, msg.owner),
                body: ApplyMainnetPositionUpgrade { newCode: msg.newCode },
            });
            upgrade.send(SEND_MODE_BOUNCE_ON_ACTION_FAIL);
        }
        FreezeMainnetUpgrades => {
            var storage = lazy MainnetHubStorage.load();
            assert (
                in.senderAddress == storage.authorities.load().upgradeController
            ) throw MainnetHubErrors.NotUpgradeController;
            assert (!storage.upgradesFrozen) throw MainnetHubErrors.UpgradesPermanentlyFrozen;
            storage.upgradesFrozen = true;
            storage.save();
        }
        SetMainnetDepositsPaused => {
            var storage = lazy MainnetHubStorage.load();
            assert (
                in.senderAddress == storage.authorities.load().guardian
            ) throw MainnetHubErrors.NotGuardian;
            storage.depositsPaused = msg.paused;
            storage.save();
        }
        InitializeMainnetJettonWallet => {
            var storage = lazy MainnetHubStorage.load();
            assert (
                in.senderAddress == storage.authorities.load().guardian
            ) throw MainnetHubErrors.NotGuardian;
            var jetton = storage.jetton.load();
            assert (
                jetton.expectedJettonWallet == null
            ) throw MainnetHubErrors.JettonWalletAlreadyInitialized;
            assert (
                msg.master == jetton.expectedJettonMaster &&
                msg.wallet ==
                mainnetJettonWalletAddress(msg.master, contract.getAddress(), msg.walletCode)
            ) throw MainnetHubErrors.InvalidJettonConfiguration;
            jetton.expectedJettonWallet = msg.wallet;
            jetton.jettonWalletCode = msg.walletCode;
            storage.jetton = jetton.toCell();
            storage.save();
        }
        MainnetTransferNotification => {
            var storage = lazy MainnetHubStorage.load();
            val jetton = storage.jetton.load();
            assert (
                jetton.expectedJettonWallet != null
            ) throw MainnetHubErrors.JettonWalletNotInitialized;
            assert (
                in.senderAddress == jetton.expectedJettonWallet
            ) throw MainnetHubErrors.InvalidJettonWallet;

            val payload = parseDepositPayload(msg.forwardPayload);
            var runtime = storage.runtime.load();
            if (runtime.lastNotificationBySender.exists(msg.sender)) {
                assert (
                    msg.queryId > runtime.lastNotificationBySender.mustGet(msg.sender)
                ) throw MainnetHubErrors.DuplicateNotification;
            }
            runtime.lastNotificationBySender.set(msg.sender, msg.queryId);

            var totals = storage.totals.load();
            totals.totalReceived += msg.amount;
            storage.totals = totals.toCell();
            storage.runtime = runtime.toCell();

            if (
                storage.depositsPaused || msg.amount < MINIMUM_DEPOSIT ||
                msg.amount > MAXIMUM_EXTERNAL_DEPOSITS
            ) {
                storage.enqueueRefund(msg.sender, msg.amount);
                storage.save();
                return;
            }

            val developerFee = msg.amount * HUB_DEVELOPER_FEE_BASIS_POINTS / HUB_BASIS_POINTS;
            val validReferrer = payload.referrer != null && payload.referrer != msg.sender;
            val referralFee = validReferrer
                ? msg.amount * HUB_REFERRAL_FEE_BASIS_POINTS / HUB_BASIS_POINTS
                : 0;
            val referrer = validReferrer ? payload.referrer : null;

            runtime = storage.runtime.load();
            runtime.nextPositionQueryId += 1;
            val positionQueryId = runtime.nextPositionQueryId;
            runtime
                .pendingByQuery
                .set(
                    positionQueryId,
                    MainnetPendingDeposit {
                        owner: msg.sender,
                        amount: msg.amount,
                        developerFee,
                        referralFee,
                        referrer,
                    },
                );

            totals = storage.totals.load();
            totals.pendingDeposits += msg.amount;
            storage.totals = totals.toCell();
            storage.runtime = runtime.toCell();
            val state = positionState(storage, msg.sender);
            storage.save();

            val creditMessage = createMessage({
                bounce: BounceMode.RichBounce,
                value: ton("0.10"),
                dest: AutoDeployAddress { stateInit: state },
                body: MainnetCreditPositionDeposit {
                    queryId: positionQueryId,
                    amount: msg.amount,
                },
            });
            creditMessage.send(SEND_MODE_BOUNCE_ON_ACTION_FAIL);
        }
        MainnetPositionDepositAccepted => {
            var storage = lazy MainnetHubStorage.load();
            var runtime = storage.runtime.load();
            assert (
                runtime.pendingByQuery.exists(msg.queryId)
            ) throw MainnetHubErrors.PendingDepositNotFound;
            val pending = runtime.pendingByQuery.mustGet(msg.queryId);
            assert (
                in.senderAddress == positionAddress(storage, pending.owner) &&
                msg.owner == pending.owner &&
                msg.amount == pending.amount
            ) throw MainnetHubErrors.InvalidPositionAck;

            runtime.pendingByQuery.delete(msg.queryId);
            var totals = storage.totals.load();
            var fees = totals.fees.load();
            totals.pendingDeposits -= pending.amount;
            totals.positionCredited += pending.amount;
            fees.developerFeeAccrued += pending.developerFee;
            fees.referralFeeAccrued += pending.referralFee;
            totals.poolAccounted += pending.amount - pending.developerFee - pending.referralFee;
            if (pending.referrer != null && pending.referralFee > 0) {
                val current = runtime.referralBalances.exists(pending.referrer!)
                    ? runtime.referralBalances.mustGet(pending.referrer!)
                    : 0 as coins;
                runtime
                    .referralBalances
                    .set(
                        pending.referrer!,
                        current + pending.referralFee,
                    );
            }
            totals.fees = fees.toCell();
            storage.totals = totals.toCell();
            storage.runtime = runtime.toCell();
            storage.save();
        }
        MainnetPositionClaimIntent => {
            var storage = lazy MainnetHubStorage.load();
            assert (
                in.senderAddress == positionAddress(storage, msg.owner) && msg.amount > 0
            ) throw MainnetHubErrors.InvalidClaimIntent;
            var payouts = storage.loadPayouts();
            val key = claimIntentKey(in.senderAddress, msg.cycleId, msg.positionQueryId);
            var queueId: uint64 = 0;
            val claim = MainnetQueuedPayout {
                recipient: msg.owner,
                kind: MAINNET_PAYOUT_USER_REWARD,
                cycleId: msg.cycleId,
                positionQueryId: msg.positionQueryId,
                amount: msg.amount,
                tokenTransferred: false,
            };

            if (payouts.processedIntents.exists(key)) {
                queueId = payouts.processedIntents.mustGet(key);
            } else {
                storage.savePayouts(payouts);
                queueId = storage.enqueuePayout(claim);
                payouts = storage.loadPayouts();
                payouts.processedIntents.set(key, queueId);
                storage.savePayouts(payouts);
                storage.save();
            }

            sendClaimAccepted(storage, queueId, claim);
        }
        RequestMainnetDeveloperFeePayout => {
            var storage = lazy MainnetHubStorage.load();
            val authorities = storage.authorities.load();
            assert (
                in.senderAddress == authorities.developerFeeCollector
            ) throw MainnetHubErrors.NotDeveloperFeeCollector;
            var totals = storage.totals.load();
            var fees = totals.fees.load();
            assert (fees.developerFeeAccrued > 0) throw MainnetHubErrors.NoDeveloperFee;
            val amount = fees.developerFeeAccrued;
            fees.developerFeeAccrued = 0;
            fees.developerFeeQueued += amount;
            totals.fees = fees.toCell();
            storage.totals = totals.toCell();
            storage.enqueuePayout(
                MainnetQueuedPayout {
                    recipient: authorities.developerFeeCollector,
                    kind: MAINNET_PAYOUT_DEVELOPER_FEE,
                    cycleId: 0,
                    positionQueryId: 0,
                    amount,
                    tokenTransferred: false,
                },
            );
            storage.save();
        }
        RequestMainnetReferralFeePayout => {
            var storage = lazy MainnetHubStorage.load();
            var runtime = storage.runtime.load();
            assert (
                runtime.referralBalances.exists(in.senderAddress)
            ) throw MainnetHubErrors.NoReferralFee;
            val amount = runtime.referralBalances.mustGet(in.senderAddress);
            assert (amount > 0) throw MainnetHubErrors.NoReferralFee;
            runtime.referralBalances.delete(in.senderAddress);
            storage.runtime = runtime.toCell();

            var totals = storage.totals.load();
            var fees = totals.fees.load();
            assert (fees.referralFeeAccrued >= amount) throw MainnetHubErrors.InvalidPayoutReceipt;
            fees.referralFeeAccrued -= amount;
            fees.referralFeeQueued += amount;
            totals.fees = fees.toCell();
            storage.totals = totals.toCell();
            storage.enqueuePayout(
                MainnetQueuedPayout {
                    recipient: in.senderAddress,
                    kind: MAINNET_PAYOUT_REFERRAL_FEE,
                    cycleId: 0,
                    positionQueryId: 0,
                    amount,
                    tokenTransferred: false,
                },
            );
            storage.save();
        }
        DispatchMainnetPayout => {
            var storage = lazy MainnetHubStorage.load();
            assert (
                in.valueCoins >= MAINNET_MIN_DISPATCH_GRAM
            ) throw MainnetHubErrors.InsufficientDispatchGram;
            val jetton = storage.jetton.load();
            assert (
                jetton.expectedJettonWallet != null
            ) throw MainnetHubErrors.JettonWalletNotInitialized;
            var payouts = storage.loadPayouts();
            assert (payouts.activeQueueId == 0) throw MainnetHubErrors.PayoutAlreadyActive;
            assert (
                payouts.headQueueId <= payouts.nextQueueId &&
                payouts.claimsById.exists(payouts.headQueueId)
            ) throw MainnetHubErrors.NoQueuedPayout;
            val queueId = payouts.headQueueId;
            val claim = payouts.claimsById.mustGet(queueId);
            assert (!claim.tokenTransferred) throw MainnetHubErrors.InvalidPayoutReceipt;
            val totals = storage.totals.load();
            val fees = totals.fees.load();
            if (claim.kind == MAINNET_PAYOUT_USER_REWARD) {
                assert (
                    totals.poolAccounted >= payouts.reservedUserPayouts + claim.amount
                ) throw MainnetHubErrors.InsufficientPoolLiquidity;
                payouts.reservedUserPayouts += claim.amount;
            } else if (claim.kind == MAINNET_PAYOUT_DEVELOPER_FEE) {
                assert (
                    fees.developerFeeQueued >= claim.amount
                ) throw MainnetHubErrors.InvalidPayoutReceipt;
            } else if (claim.kind == MAINNET_PAYOUT_REFERRAL_FEE) {
                assert (
                    fees.referralFeeQueued >= claim.amount
                ) throw MainnetHubErrors.InvalidPayoutReceipt;
            } else if (claim.kind == MAINNET_PAYOUT_REFUND) {
                assert (
                    totals.refundsQueued >= claim.amount
                ) throw MainnetHubErrors.InvalidPayoutReceipt;
            } else {
                throw MainnetHubErrors.InvalidPayoutKind;
            }

            payouts.activeQueueId = queueId;
            storage.savePayouts(payouts);
            val jettonWallet = jetton.expectedJettonWallet!;
            storage.save();

            val transfer = createMessage({
                bounce: BounceMode.RichBounce,
                value: ton("0.75"),
                dest: jettonWallet,
                body: MainnetJettonTransfer {
                    queryId: queueId,
                    amount: claim.amount,
                    destination: claim.recipient,
                    responseDestination: contract.getAddress(),
                    customPayload: null,
                    forwardGramAmount: 0,
                    forwardPayload: beginCell().storeBool(false).endCell().beginParse(),
                },
            });
            transfer.send(SEND_MODE_BOUNCE_ON_ACTION_FAIL);
        }
        MainnetJettonExcesses => {
            var storage = lazy MainnetHubStorage.load();
            var payouts = storage.loadPayouts();
            assert (
                payouts.activeQueueId != 0 && msg.queryId == payouts.activeQueueId &&
                payouts.claimsById.exists(msg.queryId)
            ) throw MainnetHubErrors.InvalidPayoutReceipt;
            var claim = payouts.claimsById.mustGet(msg.queryId);
            assert (
                !claim.tokenTransferred &&
                in.senderAddress == configuredJettonWalletAddress(storage, claim.recipient)
            ) throw MainnetHubErrors.InvalidPayoutReceipt;

            var totals = storage.totals.load();
            var fees = totals.fees.load();
            claim.tokenTransferred = true;
            if (claim.kind == MAINNET_PAYOUT_USER_REWARD) {
                assert (
                    payouts.reservedUserPayouts >= claim.amount &&
                    payouts.claimDebt >= claim.amount &&
                    totals.poolAccounted >= claim.amount
                ) throw MainnetHubErrors.InvalidPayoutReceipt;
                payouts.claimsById.set(msg.queryId, claim);
                payouts.reservedUserPayouts -= claim.amount;
                payouts.claimDebt -= claim.amount;
                totals.poolAccounted -= claim.amount;
                totals.totalPayoutsPaid += claim.amount;
            } else if (claim.kind == MAINNET_PAYOUT_DEVELOPER_FEE) {
                assert (
                    fees.developerFeeQueued >= claim.amount
                ) throw MainnetHubErrors.InvalidPayoutReceipt;
                fees.developerFeeQueued -= claim.amount;
                fees.developerFeePaid += claim.amount;
                payouts.claimsById.delete(msg.queryId);
                payouts.headQueueId += 1;
                payouts.activeQueueId = 0;
            } else if (claim.kind == MAINNET_PAYOUT_REFERRAL_FEE) {
                assert (
                    fees.referralFeeQueued >= claim.amount
                ) throw MainnetHubErrors.InvalidPayoutReceipt;
                fees.referralFeeQueued -= claim.amount;
                fees.referralFeePaid += claim.amount;
                payouts.claimsById.delete(msg.queryId);
                payouts.headQueueId += 1;
                payouts.activeQueueId = 0;
            } else if (claim.kind == MAINNET_PAYOUT_REFUND) {
                assert (
                    totals.refundsQueued >= claim.amount
                ) throw MainnetHubErrors.InvalidPayoutReceipt;
                totals.refundsQueued -= claim.amount;
                totals.refundsPaid += claim.amount;
                payouts.claimsById.delete(msg.queryId);
                payouts.headQueueId += 1;
                payouts.activeQueueId = 0;
            } else {
                throw MainnetHubErrors.InvalidPayoutKind;
            }
            storage.savePayouts(payouts);
            totals.fees = fees.toCell();
            storage.totals = totals.toCell();
            storage.save();

            if (claim.kind == MAINNET_PAYOUT_USER_REWARD) {
                sendPayoutConfirmation(storage, msg.queryId, claim);
            }
        }
        MainnetPositionPayoutRecorded => {
            var storage = lazy MainnetHubStorage.load();
            var payouts = storage.loadPayouts();
            assert (
                payouts.activeQueueId == msg.queueId && payouts.claimsById.exists(msg.queueId)
            ) throw MainnetHubErrors.InvalidPayoutReceipt;
            val claim = payouts.claimsById.mustGet(msg.queueId);
            assert (
                claim.kind == MAINNET_PAYOUT_USER_REWARD && claim.tokenTransferred &&
                in.senderAddress == positionAddress(storage, claim.recipient) &&
                msg.owner == claim.recipient &&
                msg.amount == claim.amount
            ) throw MainnetHubErrors.InvalidPayoutReceipt;

            val key = claimIntentKey(
                in.senderAddress,
                claim.cycleId,
                claim.positionQueryId,
            );
            payouts.processedIntents.delete(key);
            payouts.claimsById.delete(msg.queueId);
            payouts.headQueueId += 1;
            payouts.activeQueueId = 0;
            storage.savePayouts(payouts);
            storage.save();
        }
        RetryMainnetPayoutConfirmation => {
            val storage = lazy MainnetHubStorage.load();
            val payouts = storage.loadPayouts();
            assert (
                payouts.activeQueueId != 0 && payouts.claimsById.exists(payouts.activeQueueId)
            ) throw MainnetHubErrors.PayoutConfirmationNotReady;
            val claim = payouts.claimsById.mustGet(payouts.activeQueueId);
            assert (
                claim.kind == MAINNET_PAYOUT_USER_REWARD && claim.tokenTransferred
            ) throw MainnetHubErrors.PayoutConfirmationNotReady;
            sendPayoutConfirmation(storage, payouts.activeQueueId, claim);
        }
        else => {
            assert (in.body.isEmpty()) throw MainnetHubErrors.InvalidHubMessage;
        }
    }
}

fun onBouncedMessage(in: InMessageBounced) {
    val rich = lazy RichBounceBody.fromSlice(in.bouncedBody);
    val bounced = lazy MainnetHubBounceMessage.fromCell(rich.originalBody);

    match (bounced) {
        MainnetCreditPositionDeposit => {
            var storage = lazy MainnetHubStorage.load();
            var runtime = storage.runtime.load();
            assert (
                runtime.pendingByQuery.exists(bounced.queryId)
            ) throw MainnetHubErrors.PendingDepositNotFound;
            val pending = runtime.pendingByQuery.mustGet(bounced.queryId);
            assert (
                in.senderAddress == positionAddress(storage, pending.owner) &&
                bounced.amount == pending.amount
            ) throw MainnetHubErrors.InvalidPositionBounce;
            runtime.pendingByQuery.delete(bounced.queryId);

            var totals = storage.totals.load();
            totals.pendingDeposits -= pending.amount;
            storage.totals = totals.toCell();
            storage.runtime = runtime.toCell();
            storage.enqueueRefund(pending.owner, pending.amount);
            storage.save();
        }
        MainnetJettonTransfer => {
            var storage = lazy MainnetHubStorage.load();
            val jetton = storage.jetton.load();
            var payouts = storage.loadPayouts();
            assert (
                jetton.expectedJettonWallet != null &&
                in.senderAddress == jetton.expectedJettonWallet &&
                payouts.activeQueueId == bounced.queryId &&
                payouts.claimsById.exists(bounced.queryId)
            ) throw MainnetHubErrors.InvalidPayoutBounce;
            val claim = payouts.claimsById.mustGet(bounced.queryId);
            assert (
                !claim.tokenTransferred && bounced.amount == claim.amount &&
                bounced.destination == claim.recipient
            ) throw MainnetHubErrors.InvalidPayoutBounce;
            if (claim.kind == MAINNET_PAYOUT_USER_REWARD) {
                assert (
                    payouts.reservedUserPayouts >= claim.amount
                ) throw MainnetHubErrors.InvalidPayoutBounce;
                payouts.reservedUserPayouts -= claim.amount;
            }
            payouts.activeQueueId = 0;
            storage.savePayouts(payouts);
            storage.save();
        }
        ConfirmMainnetPayout => {
            val storage = lazy MainnetHubStorage.load();
            val payouts = storage.loadPayouts();
            assert (
                payouts.activeQueueId == bounced.queryId &&
                payouts.claimsById.exists(bounced.queryId)
            ) throw MainnetHubErrors.InvalidPayoutBounce;
            val claim = payouts.claimsById.mustGet(bounced.queryId);
            assert (
                claim.kind == MAINNET_PAYOUT_USER_REWARD && claim.tokenTransferred &&
                in.senderAddress == positionAddress(storage, claim.recipient) &&
                bounced.amount == claim.amount
            ) throw MainnetHubErrors.InvalidPayoutBounce;
        }
        else => {
            throw MainnetHubErrors.InvalidPayoutBounce;
        }
    }
}

get fun hubGuardian(): address {
    return MainnetHubStorage.load().authorities.load().guardian;
}

get fun hubUpgradeController(): address {
    return MainnetHubStorage.load().authorities.load().upgradeController;
}

get fun developerFeeCollector(): address {
    return MainnetHubStorage.load().authorities.load().developerFeeCollector;
}

get fun expectedJettonWallet(): address? {
    return MainnetHubStorage.load().jetton.load().expectedJettonWallet;
}

get fun expectedJettonMaster(): address {
    return MainnetHubStorage.load().jetton.load().expectedJettonMaster;
}

get fun jettonWalletCodeHash(): uint256? {
    val walletCode = MainnetHubStorage.load().jetton.load().jettonWalletCode;
    return walletCode == null ? null : walletCode!.hash();
}

get fun positionCodeHash(): uint256 {
    return MainnetHubStorage.load().jetton.load().positionCode.hash();
}

get fun hubDepositsPaused(): bool {
    return MainnetHubStorage.load().depositsPaused;
}

get fun hubUpgradesFrozen(): bool {
    return MainnetHubStorage.load().upgradesFrozen;
}

get fun positionAddressFor(owner: address): address {
    val storage = MainnetHubStorage.load();
    return positionAddress(storage, owner);
}

get fun totalReceived(): coins {
    return MainnetHubStorage.load().totals.load().totalReceived;
}

get fun positionCredited(): coins {
    return MainnetHubStorage.load().totals.load().positionCredited;
}

get fun developerFeeAccrued(): coins {
    return MainnetHubStorage.load().totals.load().fees.load().developerFeeAccrued;
}

get fun developerFeeQueued(): coins {
    return MainnetHubStorage.load().totals.load().fees.load().developerFeeQueued;
}

get fun developerFeePaid(): coins {
    return MainnetHubStorage.load().totals.load().fees.load().developerFeePaid;
}

get fun referralFeeAccrued(): coins {
    return MainnetHubStorage.load().totals.load().fees.load().referralFeeAccrued;
}

get fun referralFeeQueued(): coins {
    return MainnetHubStorage.load().totals.load().fees.load().referralFeeQueued;
}

get fun referralFeePaid(): coins {
    return MainnetHubStorage.load().totals.load().fees.load().referralFeePaid;
}

get fun referralBalanceFor(owner: address): coins {
    val referrals = MainnetHubStorage.load().runtime.load().referralBalances;
    return referrals.exists(owner) ? referrals.mustGet(owner) : 0 as coins;
}

get fun poolAccounted(): coins {
    return MainnetHubStorage.load().totals.load().poolAccounted;
}

get fun refundsQueued(): coins {
    return MainnetHubStorage.load().totals.load().refundsQueued;
}

get fun refundsPaid(): coins {
    return MainnetHubStorage.load().totals.load().refundsPaid;
}

get fun pendingDeposits(): coins {
    return MainnetHubStorage.load().totals.load().pendingDeposits;
}

get fun queuedClaimDebt(): coins {
    return MainnetHubStorage.load().loadPayouts().claimDebt;
}

get fun reservedPayouts(): coins {
    return MainnetHubStorage.load().loadPayouts().reservedUserPayouts;
}

get fun totalPayoutsPaid(): coins {
    return MainnetHubStorage.load().totals.load().totalPayoutsPaid;
}

get fun payoutQueueHead(): uint64 {
    return MainnetHubStorage.load().loadPayouts().headQueueId;
}

get fun payoutQueueTail(): uint64 {
    return MainnetHubStorage.load().loadPayouts().nextQueueId;
}

get fun activePayoutId(): uint64 {
    return MainnetHubStorage.load().loadPayouts().activeQueueId;
}

get fun accountingBalanced(): bool {
    val totals = MainnetHubStorage.load().totals.load();
    val fees = totals.fees.load();
    return totals.totalReceived ==
    totals.poolAccounted + fees.developerFeeAccrued + fees.developerFeeQueued +
    fees.developerFeePaid +
    fees.referralFeeAccrued +
    fees.referralFeeQueued +
    fees.referralFeePaid +
    totals.refundsQueued +
    totals.refundsPaid +
    totals.pendingDeposits +
    totals.totalPayoutsPaid;
}
