Skip to content

Operations

Operations are steps that run one after another in a single call: swaps, withdrawals, transfers, DEX calls, registrations and storage deposits. If one fails, the whole call fails and nothing changes.

There are two ways to run them, and they differ in whose assets they use:

execute_operations Operations in a deposit
How Call execute_operations on the engine Put them in msg of ft_transfer_call, nft_transfer_call, mt_transfer_call, or in operations of deposit_near
Assets used Your inner balance Only the assets of this deposit
What’s left at the end Stays in your inner balance Must be zero, or the call fails
RegisterAssets, DeployDexCode Allowed Not allowed

See Operation for the fields of every operation.

The near CLI examples on this page run as alice.near after a storage deposit and registering near and nep141:usdt.tether-token.near. Together they’re one script.

This deposits 1 NEAR, swaps all NEAR in the inner balance for USDT, and withdraws the USDT to the wallet, in one call:

Terminal window
AMOUNT_OUT=$(near contract call-function as-read-only dex.intear.near simulate_swap_simple \
json-args '{
"dex_id": "slimedragon.near/xyk",
"message": "AQAAAEBCDwABAAAA3QAAAAA=",
"trader": "alice.near",
"asset_in": "near",
"asset_out": "nep141:usdt.tether-token.near",
"amount": {"ExactIn": "1000000000000000000000000"}
}' \
network-config mainnet now | jq -r '.[1]')
MIN_AMOUNT_OUT=$((AMOUNT_OUT * 99 / 100))
# Deposits the attached 1 NEAR, swaps all NEAR in the inner balance for USDT,
# and withdraws the USDT
near contract call-function as-transaction dex.intear.near execute_operations \
json-args "$(jq -n --arg min_amount_out "$MIN_AMOUNT_OUT" '{
"operations": [
{
"SwapSimple": {
"dex_id": "slimedragon.near/xyk",
"message": "AQAAAEBCDwABAAAA3QAAAAA=",
"asset_in": "near",
"asset_out": "nep141:usdt.tether-token.near",
"amount": "EntireBalanceIn",
"constraint": $min_amount_out
}
},
{
"Withdraw": {
"asset_id": "nep141:usdt.tether-token.near",
"amount": "PreviousSwapOutput",
"to": null,
"rescue_address": null
}
}
]
}')" \
prepaid-gas '150.0 Tgas' attached-deposit '1 NEAR' \
sign-as alice.near network-config mainnet sign-with-keychain send

At least 1 yoctoNEAR must be attached. If you attach more, the whole deposit is added to your NEAR inner balance before the operations run, so you can deposit NEAR and use it in the same call. NEAR must be registered for you in that case.

referrer is optional and applies to every swap and DEX call in the list.

With a non-empty msg, a deposit doesn’t go to your inner balance. Instead, the deposited assets are put aside for this call, and the operations can only use them. This is how you deposit, swap and withdraw in one transaction.

This sends USDT with ft_transfer_call, swaps it for exactly 0.1 NEAR, and withdraws both the NEAR and the USDT the swap didn’t use. msg is the list of operations as a JSON string, so it’s built with jq first:

Terminal window
AMOUNT_IN=$(near contract call-function as-read-only dex.intear.near simulate_swap_simple \
json-args '{
"dex_id": "slimedragon.near/xyk",
"message": "AQAAAEBCDwABAAAA3QAAAAA=",
"trader": "alice.near",
"asset_in": "nep141:usdt.tether-token.near",
"asset_out": "near",
"amount": {"ExactOut": "100000000000000000000000"}
}' \
network-config mainnet now | jq -r '.[0]')
# Send up to 1% more than the simulation. What the swap doesn't use comes back.
MAX_AMOUNT_IN=$((AMOUNT_IN * 101 / 100))
# Swaps USDT for exactly 0.1 NEAR, then withdraws the NEAR and the USDT that's left
MSG=$(jq -nc '[
{
"SwapSimple": {
"dex_id": "slimedragon.near/xyk",
"message": "AQAAAEBCDwABAAAA3QAAAAA=",
"asset_in": "nep141:usdt.tether-token.near",
"asset_out": "near",
"amount": {"Amount": {"ExactOut": "100000000000000000000000"}},
"constraint": null
}
},
{
"Withdraw": {
"asset_id": "near",
"amount": "PreviousSwapOutput",
"to": null,
"rescue_address": null
}
},
{
"Withdraw": {
"asset_id": "nep141:usdt.tether-token.near",
"amount": {"Full": {"at_least": null}},
"to": null,
"rescue_address": null
}
}
]')
near contract call-function as-transaction usdt.tether-token.near ft_transfer_call \
json-args "$(jq -n --arg amount "$MAX_AMOUNT_IN" --arg msg "$MSG" '{
"receiver_id": "dex.intear.near",
"amount": $amount,
"msg": $msg
}')" \
prepaid-gas '300.0 Tgas' attached-deposit '1 yoctoNEAR' \
sign-as alice.near network-config mainnet sign-with-keychain send

With an exact output, the amount you send is already the most you pay, so the swap doesn’t need a constraint.

msg is either the list of operations, or an object with a referrer:

{ "operations": [ ... ], "referrer": "myapp.near" }

For NEAR, pass the same value as operations to deposit_near, with the NEAR attached.

Rules for operations in a deposit:

  • Everything must be used. At the end, every asset put aside must be at zero: swapped, withdrawn, transferred or attached to a DEX call. Otherwise the call fails with Sandboxed assets must be empty after execution. Did you forget to withdraw ...?. With an ExactOut swap, withdraw the rest of the input too.
  • On failure, the deposit goes back. The call fails as a whole, and the token contract returns the tokens to the sender.
  • No registrations or deployments. RegisterAssets and DeployDexCode fail with Operation only available in execute_actions.
  • The sender is the one the token contract reports. For NEP-141 it’s sender_id. For NFTs and multi tokens, sender_id must equal previous_owner_id, so tokens sent with an approval can’t run operations.
  • DEXs know the difference. A DEX called from a deposit sees the call as not authorized, and can refuse actions that need the trader’s permission. See DEXs on the engine.

A Withdraw in a deposit needs a place to refund the assets if the transfer fails. The engine refunds to an inner balance, in this order:

  1. the receiver (to), if the asset is registered for them,
  2. the sender, if the asset is registered for them,
  3. rescue_address, if the sender doesn’t have the asset registered.

When the operation runs, the sender or rescue_address must have the asset registered, or it fails with No rescue address provided and user doesn't have a registered balance for this asset.

To deposit, swap and keep the output in the engine, use a TransferAsset to yourself instead of a Withdraw. TransferAsset needs an exact amount, so this works best with an ExactOut swap. This swaps NEAR for exactly 1 USDT, keeps the USDT in the inner balance, and sends the rest of the NEAR back:

Terminal window
# Swaps the attached NEAR for exactly 1 USDT, keeps the USDT in the inner
# balance, and withdraws the NEAR that's left
near contract call-function as-transaction dex.intear.near deposit_near \
json-args '{
"operations": [
{
"SwapSimple": {
"dex_id": "slimedragon.near/xyk",
"message": "AQAAAEBCDwABAAAA3QAAAAA=",
"asset_in": "near",
"asset_out": "nep141:usdt.tether-token.near",
"amount": {"Amount": {"ExactOut": "1000000"}},
"constraint": null
}
},
{
"TransferAsset": {
"to": {"Account": "alice.near"},
"asset_id": "nep141:usdt.tether-token.near",
"amount": "1000000"
}
},
{
"Withdraw": {
"asset_id": "near",
"amount": {"Full": {"at_least": null}},
"to": null,
"rescue_address": null
}
}
]
}' \
prepaid-gas '150.0 Tgas' attached-deposit '1 NEAR' \
sign-as alice.near network-config mainnet sign-with-keychain send

SwapSimple takes an amount of:

Value Meaning
{ "Amount": { "ExactIn": "..." } } or { "Amount": { "ExactOut": "..." } } A fixed amount
"OutputOfLastIn" Exact input of the output of the last SwapSimple before it. Its asset_out must be this asset_in
"EntireBalanceIn" Exact input of everything you have of asset_in: your inner balance with execute_operations, what’s left of the deposit with operations in a deposit

Withdraw takes an amount of:

Value Meaning
{ "Exact": "..." } A fixed amount
{ "Full": { "at_least": "..." } } Everything you have of the asset. Fails if it’s less than at_least, which can be null
"PreviousSwapOutput" The output of the last SwapSimple before it. Its asset_out must be this asset_id

“The last SwapSimple” is the most recent one in the list, even if other operations come after it.