jb-terminal-wrapper

0
0
Source

Terminal wrapper pattern for extending JBMultiTerminal functionality. Use when: (1) need dynamic splits at pay time, (2) revnet can't modify ruleset data hooks, (3) want atomic pay + distribute operations, (4) need to intercept/redirect tokens before delivery, (5) implementing pay-time configuration, (6) cash out + bridge/swap in one tx, (7) cash out + stake redeemed funds. Covers IJBTerminal implementation, _acceptFunds pattern from JBSwapTerminalRegistry, beneficiary manipulation for both pay and cash out flows, and the critical mental model that wrappers are additive (not restrictive).

Install

mkdir -p .claude/skills/jb-terminal-wrapper && curl -L -o skill.zip "https://mcp.directory/api/skills/download/8228" && unzip -o skill.zip -d .claude/skills/jb-terminal-wrapper && rm skill.zip

Installs to .claude/skills/jb-terminal-wrapper

About this skill

Terminal Wrapper Pattern

Problem

Revnets and other projects often need extended payment functionality that can't be achieved through ruleset data hooks alone. Common needs include:

  • Dynamic splits specified at payment time
  • Token interception and redirection
  • Atomic multi-step operations (pay + distribute)
  • Client-specific features without breaking permissionless access

Context / Trigger Conditions

Apply this pattern when:

  • Building payment flows that need dynamic configuration
  • Working with revnets where ruleset hooks can't be edited
  • Need to bundle multiple operations atomically
  • Want to intercept tokens for further processing
  • Implementing "pay and do X" flows

Solution

Core Architecture

Create a custom IJBTerminal that wraps JBMultiTerminal. Use a shared _acceptFunds helper (pattern from JBSwapTerminalRegistry) to handle ETH/ERC20 consistently:

contract PayWithSplitsTerminal is IJBTerminal {
    using SafeERC20 for IERC20;

    IJBMultiTerminal public immutable MULTI_TERMINAL;
    IJBController public immutable CONTROLLER;

    constructor(IJBMultiTerminal _multiTerminal, IJBController _controller) {
        MULTI_TERMINAL = _multiTerminal;
        CONTROLLER = _controller;
    }

    function pay(
        uint256 projectId,
        address token,
        uint256 amount,
        address beneficiary,
        uint256 minReturnedTokens,
        string calldata memo,
        bytes calldata metadata
    ) external payable returns (uint256 beneficiaryTokenCount) {
        // 1. Parse custom metadata
        (JBSplit[] memory splits, bytes memory innerMetadata) = _parseMetadata(metadata);

        // 2. Configure splits if provided
        if (splits.length > 0) {
            _configureSplits(projectId, splits);
        }

        // 3. Accept funds (handles ETH/ERC20 uniformly)
        uint256 valueToSend = _acceptFunds(token, amount, address(MULTI_TERMINAL));

        // 4. Forward to underlying terminal
        beneficiaryTokenCount = MULTI_TERMINAL.pay{value: valueToSend}(
            projectId,
            token,
            amount,
            beneficiary,
            minReturnedTokens,
            memo,
            innerMetadata
        );

        // 5. Distribute reserved tokens
        CONTROLLER.sendReservedTokensToSplitsOf(projectId);

        return beneficiaryTokenCount;
    }

    /// @notice Accept funds from caller and prepare for forwarding.
    /// @dev Pattern from JBSwapTerminalRegistry - consolidates token handling.
    function _acceptFunds(
        address token,
        uint256 amount,
        address spender
    ) internal returns (uint256 valueToSend) {
        if (token == JBConstants.NATIVE_TOKEN) {
            return msg.value; // Forward ETH
        }

        // ERC20: pull from sender, approve spender
        IERC20(token).safeTransferFrom(msg.sender, address(this), amount);
        IERC20(token).forceApprove(spender, amount);
        return 0; // No ETH to forward
    }
}

Beneficiary Manipulation Pattern

Intercept tokens by setting beneficiary to the wrapper itself:

function payAndStake(
    uint256 projectId,
    address token,
    uint256 amount,
    uint256 minReturnedTokens,
    bytes calldata metadata
) external payable returns (uint256 tokenCount) {
    // Parse user's desired destination from metadata
    (address finalDestination, bytes memory stakingParams) = abi.decode(
        metadata,
        (address, bytes)
    );

    // Receive tokens to this contract
    tokenCount = MULTI_TERMINAL.pay{value: msg.value}(
        projectId,
        token,
        amount,
        address(this),  // <-- Wrapper receives tokens
        minReturnedTokens,
        "",
        ""
    );

    // Do something with the tokens
    IERC20 projectToken = IERC20(CONTROLLER.TOKENS().tokenOf(projectId));

    // Example: stake them somewhere on behalf of user
    _stakeTokens(projectToken, tokenCount, finalDestination, stakingParams);

    return tokenCount;
}

Metadata Encoding (Client Side)

import { encodeAbiParameters, parseAbiParameters } from 'viem';

// For dynamic splits
const metadata = encodeAbiParameters(
  parseAbiParameters('(address preferredBeneficiary, uint256 percent, uint256 lockedUntil)[], bytes'),
  [
    [
      { preferredBeneficiary: '0x...', percent: 500000000n, lockedUntil: 0n }, // 50%
      { preferredBeneficiary: '0x...', percent: 500000000n, lockedUntil: 0n }, // 50%
    ],
    '0x' // Inner metadata for MultiTerminal
  ]
);

// For beneficiary redirection
const metadata = encodeAbiParameters(
  parseAbiParameters('address finalDestination, bytes stakingParams'),
  [userAddress, stakingCalldata]
);

Critical Mental Model

┌─────────────────────────────────────────────────────────────────┐
│                    WRAPPER IS ADDITIVE                          │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│   Client A ──► PayWithSplitsTerminal ──► JBMultiTerminal       │
│                    (gets special features)                      │
│                                                                 │
│   Client B ────────────────────────────► JBMultiTerminal       │
│                    (still works!)                               │
│                                                                 │
│   BOTH ARE VALID. The wrapper cannot block direct access.       │
│   This is a FEATURE, not a bug. Permissionless = good.          │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

Bad thinking: "I'll use a wrapper to block payments that don't meet criteria X" Reality: Users can always call JBMultiTerminal.pay() directly

Good thinking: "I'll use a wrapper to provide enhanced functionality for clients that opt in"

Use Cases

Use CaseHow Wrapper Helps
Pay Wrappers
Dynamic splits at pay timeParse splits from metadata, configure before pay
Pay + distribute reservedAtomic operation, no separate tx needed
Token interceptionReceive to self, then stake/lock/forward
Referral trackingParse referrer from metadata, record on-chain
Conditional logicCheck conditions before forwarding to MultiTerminal
Multi-hop paymentsReceive tokens, swap, pay another project
Cash Out Wrappers
Cash out + bridgeIntercept redeemed funds, bridge to another chain
Cash out + swapSwap redeemed ETH to stablecoin before delivery
Cash out + stakeStake redeemed funds in another protocol
Cash out + LPAdd redeemed funds to liquidity pool

Cash Out Wrapper Pattern

Same beneficiary-to-self trick works for cash outs:

/// @notice Cash out with automatic swap to different token.
function cashOutAndSwap(
    address holder,
    uint256 projectId,
    uint256 tokenCount,
    address tokenToReclaim,
    uint256 minTokensReclaimed,
    address tokenOut,       // Custom param: swap to this
    uint256 minAmountOut,   // Custom param: slippage
    address beneficiary,
    bytes calldata metadata
) external returns (uint256 amountOut) {
    // 1. Cash out to THIS contract (intercept funds)
    uint256 reclaimAmount = MULTI_TERMINAL.cashOutTokensOf(
        holder,
        projectId,
        tokenCount,
        tokenToReclaim,
        minTokensReclaimed,
        address(this),  // <-- Wrapper receives redeemed funds
        metadata
    );

    // 2. Swap redeemed tokens to desired output
    amountOut = _swap(tokenToReclaim, tokenOut, reclaimAmount, minAmountOut);

    // 3. Send swapped tokens to beneficiary
    _sendFunds(tokenOut, amountOut, beneficiary);

    return amountOut;
}

/// @notice Cash out with automatic bridging.
function cashOutAndBridge(
    address holder,
    uint256 projectId,
    uint256 tokenCount,
    address tokenToReclaim,
    uint256 minTokensReclaimed,
    address beneficiary,
    uint256 destChainId,
    bytes calldata metadata
) external returns (uint256 reclaimAmount) {
    // 1. Cash out to this contract
    reclaimAmount = MULTI_TERMINAL.cashOutTokensOf(
        holder,
        projectId,
        tokenCount,
        tokenToReclaim,
        minTokensReclaimed,
        address(this),
        metadata
    );

    // 2. Bridge funds to destination chain
    _bridgeFunds(tokenToReclaim, reclaimAmount, beneficiary, destChainId);

    return reclaimAmount;
}

Comparison with Swap Terminal

Swap Terminal is a canonical example of this pattern:

User pays with USDC ──► SwapTerminal ──► Swaps to ETH ──► JBMultiTerminal
                        (wraps + transforms)

Your wrapper follows the same architecture but with different transformation logic.

Verification

  1. Deploy wrapper pointing to existing JBMultiTerminal
  2. Test that direct MultiTerminal payments still work (permissionless)
  3. Test that wrapper payments get enhanced behavior
  4. Verify atomic operations complete or revert together
  5. Test metadata parsing edge cases (empty, malformed)

Example

Complete implementation for pay-time splits:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import {IJBTerminal} from "@bananapus/core/src/interfaces/IJBTerminal.sol";
import {IJBMultiTerminal} from "@bananapus/core/src/interfaces/IJBMultiTerminal.sol";
import {IJBController} from "@bananapus/core/src/interfaces/IJBController.sol";
import {IJBSplits} from "@bananapus/core/src/interfaces/IJBSplits.sol";
import {JBSplit} from "@bananapus/core/src/structs/JBSplit.sol";
import {JBSplitGroup} from "@bananapus/core/src/structs/JBSplitGroup.sol";

contract DynamicSplitsTerminal is IJBTerminal {
    IJBMultiTerminal public immutable MULTI_TERMINAL;
    IJBController public immutable CONTROLLER;

    // Split group ID for reserved tokens
    uint256 constant RES

---

*Content truncated.*

You might also like

flutter-development

aj-geddes

Build beautiful cross-platform mobile apps with Flutter and Dart. Covers widgets, state management with Provider/BLoC, navigation, API integration, and material design.

9521,094

drawio-diagrams-enhanced

jgtolentino

Create professional draw.io (diagrams.net) diagrams in XML format (.drawio files) with integrated PMP/PMBOK methodologies, extensive visual asset libraries, and industry-standard professional templates. Use this skill when users ask to create flowcharts, swimlane diagrams, cross-functional flowcharts, org charts, network diagrams, UML diagrams, BPMN, project management diagrams (WBS, Gantt, PERT, RACI), risk matrices, stakeholder maps, or any other visual diagram in draw.io format. This skill includes access to custom shape libraries for icons, clipart, and professional symbols.

846846

ui-ux-pro-max

nextlevelbuilder

"UI/UX design intelligence. 50 styles, 21 palettes, 50 font pairings, 20 charts, 8 stacks (React, Next.js, Vue, Svelte, SwiftUI, React Native, Flutter, Tailwind). Actions: plan, build, create, design, implement, review, fix, improve, optimize, enhance, refactor, check UI/UX code. Projects: website, landing page, dashboard, admin panel, e-commerce, SaaS, portfolio, blog, mobile app, .html, .tsx, .vue, .svelte. Elements: button, modal, navbar, sidebar, card, table, form, chart. Styles: glassmorphism, claymorphism, minimalism, brutalism, neumorphism, bento grid, dark mode, responsive, skeuomorphism, flat design. Topics: color palette, accessibility, animation, layout, typography, font pairing, spacing, hover, shadow, gradient."

571699

godot

bfollington

This skill should be used when working on Godot Engine projects. It provides specialized knowledge of Godot's file formats (.gd, .tscn, .tres), architecture patterns (component-based, signal-driven, resource-based), common pitfalls, validation tools, code templates, and CLI workflows. The `godot` command is available for running the game, validating scripts, importing resources, and exporting builds. Use this skill for tasks involving Godot game development, debugging scene/resource files, implementing game systems, or creating new Godot components.

548492

nano-banana-pro

garg-aayush

Generate and edit images using Google's Nano Banana Pro (Gemini 3 Pro Image) API. Use when the user asks to generate, create, edit, modify, change, alter, or update images. Also use when user references an existing image file and asks to modify it in any way (e.g., "modify this image", "change the background", "replace X with Y"). Supports both text-to-image generation and image-to-image editing with configurable resolution (1K default, 2K, or 4K for high resolution). DO NOT read the image file first - use this skill directly with the --input-image parameter.

673466

fastapi-templates

wshobson

Create production-ready FastAPI projects with async patterns, dependency injection, and comprehensive error handling. Use when building new FastAPI applications or setting up backend API projects.

514280

Stay ahead of the MCP ecosystem

Get weekly updates on new skills and servers.