Skip to content

Intents

Understanding the Intent System

Intents are the cornerstone of Saline. An intent is a predicate or a combination of predicates that specifies what actions an account allows to happen. Intents enable powerful patterns like account abstraction, delegation, token swaps, multi‑signature authorization, and more.

Key Characteristics of Intents

  • Declarative: Intents declare what is permitted rather than how to do it
  • Composable: Complex authorization rules are built by combining simpler intents
  • Enforceable: The blockchain enforces intents at transaction execution time
  • Future‑proof: Intents work with transactions that may not exist yet (like future swap matches)

Intent Primitives and Composites

Saline provides several basic building blocks that can be combined to create complex intents.

Primitives

  • Restriction() – require two expressions be related in a given way
  • Signature() – require a signature from a given public key

Composites

  • All(conditions: list) – all sub‑intents/conditions must be fulfilled (logical AND)
  • Any(threshold: int, conditions: list) – at least threshold sub‑intents/conditions must be fulfilled (logical OR / M‑of‑N)

Modifiers

  • Temporary(expiry_timestamp: int, available_after: bool, intent) – intent is valid only up to a specific Unix timestamp
  • Finite(max_uses: int, intent) – intent can be used a maximum number of times

Expressions

Expressions are used within Restriction intents to evaluate conditions based on transaction details or account state:

  • Lit(value) – literal value (number, string, etc.)
  • Balance(token: str) – token balance for the account hosting the intent
  • Receive(token: Token) – amount of a token received
  • Send(token: Token) – amount of a token sent
  • Arithmetic2(op: ArithOp, lhs, rhs) – arithmetic operations (Add, Sub, Mul, Div) over expressions

Operator Syntax (Optional Shorthands)

For convenience, the SDK may overload some Python operators as shorthands for common intent constructions. Using the explicit binding classes (Restriction, All, Any, etc.) is recommended for clarity, especially for complex intents.

Potential Shorthands (check SDK docs/examples for exact support):

  • &All([...])
  • |Any(1, [...]) (logical OR)
  • <=, >=, <, > → create a Restriction between two expressions
  • *, +, -, / → arithmetically combines two expressions into one

Additional fields often referenced:

  1. Counterparty – the account on the other side of the transaction
    • None – any account
    • "public_key" – a specific account
  2. Token – token type
    • Token.BTC, Token.ETH, etc.

Common Intent Patterns

Swap Intent Pattern

python
# Swap 2 ETH for 100 USDT
intent = Send(Token.ETH) <= 2 & Receive(Token.USDT) >= 100

# Rate‑based swap: 2 ETH : 100 USDT
intent = Send(Token.ETH) * 2 <= Receive(Token.USDT) * 100

Breaking Down the Pattern

  1. Send(Token.ETH) – amount of ETH sent
  2. * 2 – multiply by 2
  3. <= – establish exchange relation
  4. Receive(Token.USDT) – amount of USDT received
  5. * 100 – multiply by 100

Multi‑Signature Intent Pattern

This intent requires at least 2‑of‑3 signatures to authorize a transaction.

python
sig1 = Signature("public_key_1")
sig2 = Signature("public_key_2")
sig3 = Signature("public_key_3")

multisig_intent = Any(2, [sig1, sig2, sig3])

Complete Swap Intent Example

python
from saline_sdk.account import Account
from saline_sdk.transaction.bindings import (
    NonEmpty, Transaction, SetIntent, Token,
    Send, Receive
)
from saline_sdk.transaction.tx import prepareSimpleTx
from saline_sdk.rpc.client import Client

# Create account
account = Account.from_mnemonic("your mnemonic").create_subaccount(label="swap_account")

# Parameters
give_token, give_amount = Token.ETH, 2
take_token, take_amount = Token.USDT, 100

# Build intent (operator syntax)
intent = Send(give_token) * give_amount <= Receive(take_token) * take_amount

# Install intent on‑chain
tx = Transaction(instructions=NonEmpty.from_list([SetIntent(account.public_key, intent)]))
signed_tx = prepareSimpleTx(account, tx)

client = Client()
result = await client.tx_commit(signed_tx)

Advanced Intent Patterns

Time‑Limited Intent

python
import time
base_intent = Send(Token.ETH) * 1 <= Receive(Token.USDT) * 50
expiry_time = int(time.time()) + 24*60*60  # 24 hours
limited_intent = Temporary(expiry_time, available_after=True, intent=base_intent)

Usage‑Limited Intent

python
base_intent = Send(Token.ETH) * 0.1 <= Receive(Token.USDT) * 5
limited_intent = Finite(5, base_intent)  # 5 uses only

Best Practices

  1. Use Explicit Bindings – prefer Restriction, All, Any for clarity over operator shorthands
  2. Start Simple – begin with basic patterns (e.g., fixed swaps) before adding complexity (Any, Temporary, Finite)
  3. Test Extensively – ensure intents behave as expected across diverse transactions
  4. Add Modifiers for Safety – use Temporary or Finite on sensitive operations

Witnesses

When using threshold intents, the verifier doesn't know ahead of time which sub-intents the transaction is meant to fulfill which can lead to extra computation (thus higher gas costs) checking unnecessary branches. This can be avoided by specifying a Witness.

For instance, for the intent

python
All(Send(Token.BTC) >= 0, Any(2, [sig0, sig1, sig2]))

if we submit a transaction sending BTC from this address and signed by sig0 and sig2, then we can skip the check for sig1 by using the witneses

python
AllW(AutoW(), AnyW({0: AutoW(), 2: AutoW()}))

When no witness is specified for a branching intent, we recursively default to AutoW() everywhere which does best-effort skipping of checks. Say the transaction was signed by sig0 and sig1. Since these show up first in the list, then we'd reach the threshold before doing any useless checks. However, it is good practice to always specify witnesses when thresholds are used since the verifier can in principle be run in parallel, in which case there is no guarantee all sub-intents are checked in order.

Note if the every branching in an intent is an instance of All, there is no need for witnesses as we know everything must be checked. Witnesses are only needed when we don't know what we do not need to check. The only situation an AllW is needed is when it's has at least one AnyW descendant.