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 waySignature()
– 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 leastthreshold
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 timestampFinite(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 intentReceive(token: Token)
– amount of a token receivedSend(token: Token)
– amount of a token sentArithmetic2(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 aRestriction
between two expressions*
,+
,-
,/
→ arithmetically combines two expressions into one
Additional fields often referenced:
- Counterparty – the account on the other side of the transaction
None
– any account"public_key"
– a specific account
- Token – token type
Token.BTC
,Token.ETH
, etc.
Common Intent Patterns
Swap Intent Pattern
# 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
Send(Token.ETH)
– amount of ETH sent* 2
– multiply by 2<=
– establish exchange relationReceive(Token.USDT)
– amount of USDT received* 100
– multiply by 100
Multi‑Signature Intent Pattern
This intent requires at least 2‑of‑3 signatures to authorize a transaction.
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
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
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
base_intent = Send(Token.ETH) * 0.1 <= Receive(Token.USDT) * 5
limited_intent = Finite(5, base_intent) # 5 uses only
Best Practices
- Use Explicit Bindings – prefer
Restriction
,All
,Any
for clarity over operator shorthands - Start Simple – begin with basic patterns (e.g., fixed swaps) before adding complexity (
Any
,Temporary
,Finite
) - Test Extensively – ensure intents behave as expected across diverse transactions
- Add Modifiers for Safety – use
Temporary
orFinite
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
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
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.