Skip to main content

STXN Application SDK — v1.0 (Apps + CallBreaker)

Audience: Teams building next‑gen web3 applications that want to leverage STXN’s clean interfaces, DAG‑based optimal execution, and solver marketplace to reach users at scale.

Scope: How to model calls, create and push UserObjectives via ISmartExecute, optionally integrate IApprover hooks, track lifecycle, and consume outputs.

What’s in v2: No Laminated Proxy/smart-account dependency. Apps push objectives directly to CallBreaker. Execution supports DAG/order control, return-value verification, and a return bus for inter‑call data flow.


0) Quick Start

Install (via Git submodule)

# Add STXN contracts repo as a submodule
git submodule add https://github.com/smart-transaction/stxn-smart-contracts-v2.git lib/stxn
cd lib/stxn && forge build

Minimal push (TypeScript, viem/ethers‑style):

Note: A single CallBreaker is deployed per chain. All applications on that chain interact with the same CallBreaker instance, whether their integration is contract‑based or through a frontend/UI. All pushes are recorded and executed directly on‑chain.

// Note: no npm package available yet. You will need to manually call CallBreaker.
// Example using viem/ethers to build and send a tx directly.

// Build a CallObject (ERC-20 transfer)
const transferCalldata = encodeTransfer(recipient, amount);

const uo = {
appId: toBytes("app.demo.v1"),
nonce: BigInt(Date.now()),
tip: 0n,
chainId: BigInt(8453),
maxFeePerGas: 0n,
maxPriorityFeePerGas: 0n,
sender: userAddress,
callObjects: [
{
salt: 0n,
amount: 0n,
gas: 0n, // 0 lets executor choose; or set a cap
addr: tokenAddress,
callvalue: transferCalldata,
returnvalue: "0x", // fill if you know exact return bytes
skippable: false,
verifiable: true,
exposeReturn: false,
},
],
};

const additional = [];

// Send directly to CallBreaker contract
const tx = await publicClient.writeContract({
address: CALLBREAKER_ADDRESS,
abi: ISmartExecuteABI,
functionName: "pushUserObjective",
args: [uo, additional],
value: 0n,
});

1) Architecture (app-facing)

  1. Application builds a UserObjective (one or more CallObjects) and pushes it to CallBreaker using ISmartExecute.pushUserObjective (can be made gasless for the user; optional msg.value forwarded to pre-approver).
  2. Solvers pick up UserObjectivePushed events off-chain, plan execution (DAG/order), and later submit executeAndVerify with user signatures and return data (covered in Solver Guide).
  3. CallBreaker verifies signatures, executes calls in the provided order, checks return values, exposes return data as requested, and settles gas+tips from user balances to the solver.
  4. (Optional) Your app’s IApprover hooks can enforce policies: preapprove at push, postapprove after execution.

2) Core Types & Interfaces

2.1 Solidity data types (push‑time)

// SPDX-License-Identifier: BSL-1.
pragma solidity 0.8.30;

struct CallObject {
uint256 salt; // randomness/idempotency
uint256 amount; // ETH to send
uint256 gas; // per-call gas limit
address addr; // target contract
bytes callvalue; // calldata
bytes returnvalue; // expected return (if verifiable)
bool skippable; // cam be skipped by callbreaker if optimal
bool verifiable; // compare actual vs expected
bool exposeReturn; // expose bytes for later calls
}

struct UserObjective {
bytes appId; // app id for solver selection
uint256 nonce; // anti-replay
uint256 tip; // incentive to solver
uint256 chainId; // target chain
uint256 maxFeePerGas; // caps
uint256 maxPriorityFeePerGas; // caps
address sender; // user address
CallObject[] callObjects; // ordered calls
}

struct AdditionalData { bytes32 key; bytes value; }

interface ISmartExecute {
function pushUserObjective(
UserObjective calldata _userObjective,
AdditionalData[] calldata _additionalData
) external payable returns (bytes32 requestId);
}

Execution‑time: The solver submits a version of UserObjective that includes a user signature. CallBreaker verifies the signature against getMessageHash(abi.encode(nonce, sender, abi.encode(callObjects))) during executeAndVerify.

2.2 App hooks

pragma solidity 0.8.30;

import {UserObjective} from "src/interfaces/ISmartExecute.sol";

interface IApprover {
function preapprove(UserObjective calldata _userObjective)
external payable returns (bool);

function postapprove(
UserObjective[] calldata _userObjective,
bytes[] calldata _returnData
) external returns (bool);
}

Register hooks (owner‑only on CallBreaker):

function setApprovalAddresses(bytes calldata appId, address pre, address post) external onlyOwner;

2.3 CallBreaker (app‑relevant surface)

Events

  • UserObjectivePushed(bytes32 requestId, uint256 sequenceCounter, bytes appId, uint256 chainId, uint256 blockNumber, UserObjective userObjective, AdditionalData[] mevTimeData)
  • Deposit(address sender, uint256 amount)
  • ApprovalAddressesSet(bytes appId, address pre, address post)
  • ValidatorAddressSet(bytes appId, address validator)
  • CallIndicesPopulated()
  • VerifyStxn()

Errors (subset)

  • PreApprovalFailed(bytes appId), PostApprovalFailed(bytes appId)
  • UnauthorisedSigner(address recovered, address expected)
  • CallFailed(), CallVerificationFailed(), LengthMismatch()
  • OutOfEther(), FlatIndexOutOfBounds(), CallNotFound()

Helpers

  • Return bus: getReturnValue(CallObject), getReturnValueHash(CallObject), hasReturnValue(CallObject), hasZeroLengthReturnValue(CallObject)
  • DAG utilities: expectFutureCall(CallObject), expectFutureCallAt(CallObject, index), getCurrentlyExecuting()
  • Balances: deposit() and senderBalances(address)

3) High‑Level Flow (Apps)

  1. Build a UserObjective with one or more CallObjects.
  2. Attach AdditionalData[] if you need metadata (MEV‑time, cross‑chain hints, partner ids, deadlines).
  3. Push with ISmartExecute.pushUserObjective(uo, additional); you receive a requestId.
  4. Observe UserObjectivePushed in your indexer.
  5. (Optional) Pre‑approve runs automatically if set for appId. If it returns false, the push reverts.
  6. Execution is triggered later by solvers (with user signatures).
  7. (Optional) Post‑approve runs after successful executeAndVerify.

Gas & Tips: Users fund balances via deposit(). After execution, CallBreaker charges each user gasUsed × min(maxFeePerGas, basefee + maxPriorityFeePerGas) + tip, transferring to the solver.


4) Building Objectives

4.1 Choosing call boundaries

  • Keep CallObjects focused (one function call each).
  • Use skippable=true for non‑critical steps (e.g., approve if already sufficient).
  • Set verifiable=true with a known returnvalue when you can; otherwise leave empty and rely on solver‑provided returnBytes (still verified).
  • Set exposeReturn=true to make the actual return available to later calls via the return bus.

4.2 DAG & order control

  • The solver submits an orderOfExecution vector to executeAndVerify, enabling parallelizable or dependency‑aware execution.
  • Utilities expectFutureCall / expectFutureCallAt help reason about positions.

4.3 Cross‑chain context

  • Use chainId in UserObjective to target the intended network (can be non‑EVM IDs by convention in your infra).
  • Put cross‑domain metadata in AdditionalData[] (destination program addresses, partner routing ids, etc.).

5) Hooks: preapprove & postapprove

When to use hooks

  • Enforce business policies (KYC tier, daily limits, venue allowlists).
  • Ensure funding/tipping constraints before accepting pushes.
  • Perform aggregate validations after execution (e.g., expected receipts, accounting logs).

Lifecycle

  • preapprove(UserObjective) runs inside pushUserObjective(...). It can consume msg.value sent by the pusher.
  • postapprove(UserObjective[], bytes[] returnData) runs at the end of executeAndVerify(...).
  • Register per‑app using setApprovalAddresses(appId, pre, post).

Keep hooks cheap and deterministic. Heavy checks belong off‑chain.


6) Observability & Indexing

Listen for UserObjectivePushed to ingest work items. Suggested index shape:

{
"requestId": "0x...",
"appId": "0x...",
"chainId": 8453,
"sender": "0x...",
"calls": [ { "addr": "0x...", "selector": "0x...", "salt": "..." } ],
"additional": [ { "key": "0x..", "value": "0x.." } ],
"blockNumber": 12345678,
"pushedAt": 1726789012
}

For post‑execution analytics, consume VerifyStxn() and your own postapprove events.


7) Recipes

7.A One‑click ERC‑20 → ERC‑20 swap

  1. approve(router, amount)skippable=true.
  2. router.swapExactTokensForTokens(...)verifiable=true. Provide returnvalue if exact bytes known; else leave empty and rely on solver return bytes.
  3. (Optional) exposeReturn=true, then read with getReturnValue in a subsequent call (e.g., to route payouts).

7.B Escrow w/ attestation

  1. Escrow to merchant contract.
  2. Attestation/Oracle verify — verifiable=true.
  3. Release — non‑skippable.

7.C Batch payouts

  • N transfer calls. Mark recipients with zero balances as skippable=true to avoid reverting the whole batch.

7.D Flash‑liquidity / arbitrage (advanced)

  • User objective: approvals + target swap.
  • Solver objective: provide liquidity, check slippage (exposeReturn=true), unwind.
  • Use orderOfExecution to interleave user/solver calls safely.

7.E Cross‑chain intent

  • Set chainId to the destination context and pass target identifiers in AdditionalData[] (e.g., SolanaContractAddress, SolanaWalletAddress).

8) Return Bus & Large Values

  • When exposeReturn=true, CallBreaker stores the bytes in transient storage under the call’s hash key.
  • Read via getReturnValue(callObj); when the return is large, only a hash is stored → read via getReturnValueHash(callObj) and verify off‑chain.
  • Helpers hasReturnValue / hasZeroLengthReturnValue aid branching logic.

9) Funding & Settlement

Funding: Users send ETH to deposit(); transfers to the contract through receive/fallback are rejected. Settlement: After executeAndVerify, CallBreaker computes each user’s cost per UserObjective:

userCost = gasUsed × min(maxFeePerGas, basefee + maxPriorityFeePerGas) + tip

The amount is debited from senderBalances[user] and credited to the solver (the msg.sender of executeAndVerify). Reverts with OutOfEther() if the user’s balance is insufficient.


10) Security & Best Practices

  • Replay safety: Always increment/change nonce per push.
  • Verification: Prefer verifiable=true with known returnvalue. If unknown, supply empty and rely on solver return bytes (still matched).
  • Determinism: Keep hooks idempotent; a second call should not change outcomes.
  • App IDs: Use stable appId byte identifiers; store them server‑side with your product config.
  • Address hygiene: Treat all user‑provided addresses as untrusted; validate/allow‑list in pre‑approver.
  • Testing: Use Foundry helpers to generate signatures and build objectives consistently.

11) Testing & Local Dev

Foundry

forge build
forge test -vvv
forge test --match-test testExecuteAndVerifyWithUserReturns

Signature generation (example)

bytes32 messageHash = callBreaker.getMessageHash(
abi.encode(nonce, sender, abi.encode(callObjects))
);
(uint8 v, bytes32 r, bytes32 s) = vm.sign(pk, messageHash);
bytes memory signature = abi.encodePacked(r, s, v);

Integration test skeleton

CallBreaker cb = new CallBreaker(address(this));
vm.prank(user);
cb.deposit{value: 5 ether}();

// build and push
bytes32 req = cb.pushUserObjective(uo, new AdditionalData[](0));
// later: solver submits executeAndVerify(...)

12) Deployment & Configuration

  • Register hooks: setApprovalAddresses(appId, pre, post) (owner‑only).
  • MEV‑time validator: setValidatorAddress(appId, validator) (owner‑only).
  • Deterministic deploys: scripts can deploy with salts (see repo scripts).
  • Verification: use Foundry forge verify-contract/Blockscout tooling per network.

13) Reference: Execution‑time Signature

During executeAndVerify, each UserObjective is verified on‑chain as:

messageHash = getMessageHash( abi.encode( userObj.nonce,
userObj.sender,
abi.encode(userObj.callObjects) ) )
require( ecrecover(messageHash, v, r, s) == userObj.sender )

Ensure your off‑chain signing uses the same ABI encoding for CallObject[].


14) FAQs

Do users need smart accounts? No. EOAs are fine; signatures are verified at execution time.

Is push signed? No. Push is gasless and signature‑free; execution later requires signatures.

Where do tips go? To the solver after successful execution; they’re added to gas cost settlement.

How do we read an earlier call’s output? Set exposeReturn=true and read via getReturnValue (or hash variant for large values).

Can apps block or rewrite pushes? preapprove can veto acceptance at push; postapprove can veto after execution (use with care).


15) Change Log

  • v1.0 — End‑to‑end app integration guide aligned with v2 contracts: ISmartExecute, IApprover, CallBreaker DAG/return‑bus model, funding/settlement, hooks, and testing.