Predict Sessions SDK
The @mysten/deepbook-v3/sessions subpath exports SessionsContract, which builds the owner-signed calls that grant and revoke a time-limited session key and the session-signed calls that trade DeepBook Predict positions for the account. Sessions explains the contract mechanism behind each call.
A session key carries authority over everything the account holds, as the Sessions contract page states. It cannot withdraw to an address, grant or revoke sessions, or outlive its expiry, but nothing caps notional, restricts which markets or pools it reaches, or bounds loss to adverse pricing. Fund an account that hands out session keys with only what you are willing to put at risk.
SessionsContract builds commands and never signs or executes them. Each call has a single signer:
- Owner key: The account owner signs
authorizeSessionandrevokeSession, because the contract derives owner authority from the transaction sender. - Session key: The session address signs the 4 Predict builders and the 5 DeepBook spot calls, because each wrapper checks the sender's grant on the account.
- Any sender: You simulate
sessionExpirationMsrather than execute it, so any sender address works.
Configuration
Build a SessionsContract for each network from the deployed IDs that getSessionsConfig returns:
import { SessionsContract, getSessionsConfig } from '@mysten/deepbook-v3/sessions';
const ids = getSessionsConfig('testnet');
const sessions = new SessionsContract(ids);
// Every builder takes the owner's wrapper ID.
const wrapperId = sessions.deriveAccountWrapperId(OWNER_ADDRESS);
// Every Predict builder also takes the Predict ProtocolConfig object ID.
const protocolConfig = ids.protocolConfig;
OWNER_ADDRESS is the address that owns the account. The same sessions instance serves the owner-signed and the session-signed calls, because the signer is whoever signs the transaction, not a property of the contract instance.
getSessionsConfig
Use getSessionsConfig to read the deployed sessions IDs for a network, so you never transcribe them. It returns the frozen deployment record for that network, typed SessionsConfig & SessionsSpotIds & SessionsPredictIds, and it throws an Error for any network other than Testnet or Mainnet. For a deployment of your own, construct a SessionsConfig and pass it to the SessionsContract constructor directly.
It takes this parameter:
network: The network to resolve. Its type isNetworkArg, the Sui client's network type, soclient.networkpasses directly.
packages/deepbook-v3/src/sessions.ts. You probably need to run `pnpm prebuild` and restart the site.SessionsContract
Use the SessionsContract constructor to bind every builder to a single deployment. It reads only the 4 SessionsConfig IDs, so the wider getSessionsConfig result passes directly, and each Predict builder takes protocolConfig as its own parameter.
It takes this parameter:
config: The deployed sessions and account IDs, usually thegetSessionsConfigresult.
packages/deepbook-v3/src/sessions.ts. You probably need to run `pnpm prebuild` and restart the site.SessionsConfig
SessionsConfig holds the 4 IDs every builder needs: the deepbook_sessions package, the shared SessionsConfig object, the account package, and the shared AccountRegistry. The account IDs are the same ones AccountContract takes on Predict Accounts SDK, because sessions is an account app on the same registry.
packages/deepbook-v3/src/sessions.ts. You probably need to run `pnpm prebuild` and restart the site.SessionsSpotIds
SessionsSpotIds holds 2 IDs that only DeepBook spot work needs: deepbookRegistry, which the 2 order-placement calls take as deepbook_registry, and deepbookCoreAccountPackageId, which you need to check that registry's authorization. No Predict builder takes either, so they sit outside SessionsConfig.
packages/deepbook-v3/src/sessions.ts. You probably need to run `pnpm prebuild` and restart the site.SessionsPredictIds
SessionsPredictIds holds the Predict ProtocolConfig object ID, which every Predict builder takes as protocolConfig.
packages/deepbook-v3/src/sessions.ts. You probably need to run `pnpm prebuild` and restart the site.MAX_SESSION_DURATION_MS
MAX_SESSION_DURATION_MS is the longest grant the contract accepts, 30 days in milliseconds. authorizeSession does not check durationMs against it, so a duration of 0, or one longer than the maximum, reaches the chain and aborts with EInvalidSessionDuration. Check the duration locally, as the authorize example does, to get the error before you sign.
packages/deepbook-v3/src/sessions.ts. You probably need to run `pnpm prebuild` and restart the site.MAX_SESSIONS_PER_ACCOUNT
MAX_SESSIONS_PER_ACCOUNT is the number of distinct session addresses an account can store, 20. Expired grants keep counting toward it until the owner revokes them, and the SDK does not check it, so authorizing a new address on a full account aborts with ESessionLimitExceeded. The list example reclaims the slots that expired grants hold.
packages/deepbook-v3/src/sessions.ts. You probably need to run `pnpm prebuild` and restart the site.ID derivation functions
An owner has 2 derived objects in the account registry, and SessionsContract computes both IDs offchain with no chain read. The shared AccountWrapper is the object every builder takes as wrapperId. The canonical account is the identity that session grants attach to, and the one that SessionAuthorized and SessionRevoked report as account_id. Accounts and Custody describes both objects.
deriveAccountWrapperId
Use deriveAccountWrapperId to compute the owner's AccountWrapper ID. It returns a string. It delegates to AccountContract with the account package and registry that the Predict facade also uses, so it returns the same ID as the wrapper derivation on Predict Accounts SDK.
It takes this parameter:
owner: The address that owns the account.
packages/deepbook-v3/src/sessions.ts. You probably need to run `pnpm prebuild` and restart the site.deriveAccountId
Use deriveAccountId to compute the owner's canonical account ID, a different derived object from the wrapper. It returns a string. Session grants attach to this object, and deriveSessionsFieldId derives from it.
It takes this parameter:
owner: The address that owns the account.
packages/deepbook-v3/src/sessions.ts. You probably need to run `pnpm prebuild` and restart the site.deriveSessionsFieldId
Use deriveSessionsFieldId to compute the object ID of the owner's DataKey<SessionsApp> dynamic field, which holds every grant. It returns a string. It derives the account ID from owner first, so pass the owner address, not an account or wrapper ID.
It takes this parameter:
owner: The address that owns the account.
packages/deepbook-v3/src/sessions.ts. You probably need to run `pnpm prebuild` and restart the site.Authorize a session
The owner grants a session key with a single owner-signed call. The contract computes the expiry from the clock at execution time, so a queued or retried transaction still gets its full duration.
authorizeSession
Use authorizeSession to grant session authority over the account until the execution-time clock plus durationMs. It returns a (tx: Transaction) => void function that adds the authorize_session call, and the owner must sign it. Re-authorizing an address that already holds a grant replaces its expiry in place and uses no additional slot, which is how you extend a running session.
It takes these parameters:
wrapperId: The owner'sAccountWrapperID, fromderiveAccountWrapperId.session: The address to authorize.durationMs: The grant length in milliseconds, greater than 0 and at mostMAX_SESSION_DURATION_MS.
packages/deepbook-v3/src/sessions.ts. You probably need to run `pnpm prebuild` and restart the site.Grant a session from the owner key
The following example checks the duration locally, then builds an owner-signed grant for a session address:
import {
MAX_SESSION_DURATION_MS,
SessionsContract,
getSessionsConfig,
} from '@mysten/deepbook-v3/sessions';
import { Transaction } from '@mysten/sui/transactions';
import { NETWORK } from './config.js';
// One contract instance drives every sessions call in these examples.
// `getSessionsConfig` returns the deployed sessions package, the shared
// `SessionsConfig` object, the account registry the app is authorized against,
// the Predict protocol config, and the two extra IDs the DeepBook spot wrappers
// need. It resolves both recorded networks; an unrecorded one throws.
export const SESSIONS_CONFIG = getSessionsConfig(NETWORK);
export const sessions = new SessionsContract(SESSIONS_CONFIG);
// The owner's shared `AccountWrapper` ID, derived from the owner address with no
// chain read. Every builder below takes it as `wrapperId`.
export function wrapperId(owner: string): string {
return sessions.deriveAccountWrapperId(owner);
}
// Authorize `session` to trade the owner's account until `now + durationMs`.
//
// A grant is trading authority over the whole account, not a scoped permission.
// The session key cannot withdraw to an address, cannot grant or revoke
// sessions, and cannot outlive its expiry, but within those limits it chooses
// the market, the size, and the price bounds, and the spot wrappers reach the
// account's entire Base, Quote, and DEEP balance. Fund an account that hands out
// ephemeral session keys with only what you are willing to put at risk.
//
// The owner must sign, because the contract derives owner authority from the
// transaction sender. Re-authorizing an address that already holds a grant
// replaces its expiry in place and consumes no additional slot.
export function authorizeSession(params: {
owner: string;
session: string;
durationMs: number;
}): Transaction {
const { owner, session, durationMs } = params;
// The chain asserts the same bounds and aborts `EInvalidSessionDuration`.
// Checking here turns a wasted transaction into a local error.
if (durationMs <= 0 || durationMs > MAX_SESSION_DURATION_MS) {
throw new Error(
`durationMs must be greater than 0 and at most ${MAX_SESSION_DURATION_MS} (30 days), got ${durationMs}.`,
);
}
const tx = new Transaction();
tx.setSender(owner);
tx.add(sessions.authorizeSession({ wrapperId: wrapperId(owner), session, durationMs }));
// Ready to sign with the owner's key. Nothing here signs it.
return tx;
}
// A short-lived grant for one trading run. Expiry is computed from the onchain
// clock at execution time, not from when you build the transaction, so a queued
// or retried transaction still gets its full duration.
export function authorizeForOneHour(owner: string, session: string): Transaction {
return authorizeSession({ owner, session, durationMs: 60 * 60 * 1000 });
}
List grants
No onchain call returns every grant at once. sessionExpirationMs answers for a single known address, so listing every grant means fetching the account's DataKey<SessionsApp> field and decoding it offchain. The Sessions page shows the stored SessionsData layout.
Listing takes 4 steps, and the SDK covers each one:
- Derive the canonical account ID from the owner with
deriveAccountId. - Derive the
DataKey<SessionsApp>field ID under that account withderiveSessionsFieldId. - Fetch that object with the core
getObject, and request its Binary Canonical Serialization (BCS) content withinclude: { content: true }. - Decode the bytes with the static
decodeSessions, then split the result withactiveSessionsandexpiredSessions.
deriveSessionsFieldId runs the first 2 steps together, because it derives the account ID from the owner address internally. The field does not exist until the owner's first authorize_session, so the fetch finds no object for an owner who has never granted a session. Treat only that not-found result as an empty list, because a transport failure says nothing about the account's grants.
That path has 3 traps:
- Account, not wrapper: The field hangs off the canonical account, not the wrapper. Deriving the field under the wrapper ID yields an ID that points at nothing.
- Plain dynamic field: The contract writes the slot with
dynamic_field::add, while the registry claims the account and wrapper IDs throughderived_object::claim, soderiveSessionsFieldIdusesderiveDynamicFieldID. Deriving the ID yourself withderiveObjectIDwraps the type tag in aDerivedObjectKeyand produces a different, empty address. - Whole-field decoding: Pass
decodeSessionsthe wholeField<DataKey<SessionsApp>, SessionsData>bytes, not the inner value.DataKeyis empty in source, but Move inserts a hiddendummy_field: bool, so a single zero byte sits between the field ID and the value, and decoding the value alone reads the field ID's first byte as the map length.
Split the result with the 2 helpers rather than by hand. The chain asserts now < expires_at_ms, so activeSessions keeps grants where nowMs < expiresAtMs and expiredSessions keeps grants where nowMs >= expiresAtMs. A filter written as nowMs > expiresAtMs leaves the grant that expires exactly at nowMs in neither list, so your cleanup skips it and the grant keeps its slot.
Expired grants keep their slots until the owner revokes them, so on a busy account, list the grants, revoke what expiredSessions returns, and then grant.
sessionExpirationMs
Use sessionExpirationMs to read the absolute expiry of a single session address. It returns a (tx: Transaction) => TransactionResult function that adds the session_expiration_ms call. Simulate the transaction and decode the returned BCS as Option<u64>: the stored expiry in milliseconds, or none for an address the owner never authorized or has revoked. The call has no version gate, and it does not classify the timestamp, so compare it with the current time yourself.
It takes these parameters:
wrapperId: The owner'sAccountWrapperID.session: The session address to look up.
packages/deepbook-v3/src/sessions.ts. You probably need to run `pnpm prebuild` and restart the site.SessionGrant
decodeSessions returns a SessionGrant for each stored address. session is the authorized address, and expiresAtMs is its absolute expiry in milliseconds since the Unix epoch. The grant is dead at that timestamp, not after it.
packages/deepbook-v3/src/sessions.ts. You probably need to run `pnpm prebuild` and restart the site.decodeSessions
Use the static SessionsContract.decodeSessions to decode an account's stored grants from the raw BCS content of its DataKey<SessionsApp> field object. It returns a SessionGrant[]. It re-encodes what it parsed and throws an Error when the length differs from the input, so truncated or padded bytes fail instead of decoding to an empty list, and an empty result means the account holds no grants.
It takes this parameter:
contents: The whole field object's BCS content, asclient.core.getObjectreturns it withinclude: { content: true }.
packages/deepbook-v3/src/sessions.ts. You probably need to run `pnpm prebuild` and restart the site.activeSessions
Use the static SessionsContract.activeSessions to keep the grants that are still live at nowMs. It returns a SessionGrant[] of the grants where nowMs < expiresAtMs.
It takes these parameters:
grants: The grants fromdecodeSessions.nowMs: The current time in milliseconds since the Unix epoch.
packages/deepbook-v3/src/sessions.ts. You probably need to run `pnpm prebuild` and restart the site.expiredSessions
Use the static SessionsContract.expiredSessions to keep the grants that are already dead at nowMs, which are the grants to revoke when you reclaim slots. It returns a SessionGrant[] of the grants where nowMs >= expiresAtMs, the exact complement of activeSessions.
It takes these parameters:
grants: The grants fromdecodeSessions.nowMs: The current time in milliseconds since the Unix epoch.
packages/deepbook-v3/src/sessions.ts. You probably need to run `pnpm prebuild` and restart the site.List and reclaim expired slots
The following example lists an account's grants, splits them at the current time, and builds a single owner-signed transaction that revokes every expired grant:
import type { SessionGrant } from '@mysten/deepbook-v3/sessions';
import { MAX_SESSIONS_PER_ACCOUNT, SessionsContract } from '@mysten/deepbook-v3/sessions';
import { ObjectError } from '@mysten/sui/client';
import { Transaction } from '@mysten/sui/transactions';
import { client } from './client.js';
import { sessions, wrapperId } from './sessions-authorize.js';
// Every grant an account holds.
//
// There is no bulk onchain read: `session_expiration_ms` answers one address at a
// time. Listing means fetching the account's `DataKey<SessionsApp>` dynamic field
// and decoding it locally. Two details make or break this:
//
// 1. The field hangs off the derived ACCOUNT address, not off the wrapper. They
// are different objects. `deriveSessionsFieldId` derives the account first,
// then the field, so pass the owner address and let it do both steps.
// 2. `decodeSessions` takes the whole `Field<DataKey, SessionsData>` bytes that
// `getObject` returns, not the inner value. It throws on truncated bytes
// rather than decoding to an empty list, so an empty result means the account
// really holds no grants.
//
// The field does not exist until the first `authorize_session`, so a missing
// object is an empty list rather than an error. Only a missing object: any other
// failure rethrows, because a transport error says nothing about the grants.
// Once attached the field stays attached, even after every grant is revoked.
export async function listGrants(owner: string): Promise<SessionGrant[]> {
const fieldId = sessions.deriveSessionsFieldId(owner);
let content: Uint8Array | undefined;
try {
const { object } = await client.core.getObject({
objectId: fieldId,
include: { content: true },
});
content = object.content;
} catch (error) {
// No sessions data attached to this account yet.
if (error instanceof ObjectError && error.reason === 'notFound') return [];
throw error;
}
if (!content) return [];
return SessionsContract.decodeSessions(content);
}
// Split a listing at the current time. Use the helpers rather than comparing by
// hand: the chain asserts `now < expiresAtMs`, so a grant is dead AT its expiry,
// and a filter written as `nowMs > expiresAtMs` leaves the grant expiring exactly
// at `nowMs` occupying a slot forever.
export async function grantsByState(
owner: string,
nowMs: number = Date.now(),
): Promise<{ active: SessionGrant[]; expired: SessionGrant[]; slotsFree: number }> {
const grants = await listGrants(owner);
return {
active: SessionsContract.activeSessions(grants, nowMs),
expired: SessionsContract.expiredSessions(grants, nowMs),
// Expired grants still occupy slots, so free slots count against every
// stored address, not just the live ones.
slotsFree: MAX_SESSIONS_PER_ACCOUNT - grants.length,
};
}
// Reclaim the slots expired grants are still holding.
//
// Time passing executes no Move code, so nothing prunes expired entries. They
// count toward the 20 stored addresses until the owner revokes them, and the
// twenty-first `authorize_session` aborts `ESessionLimitExceeded` even when every
// stored grant is long dead. Run this before granting on a busy account.
//
// Returns null when there is nothing to reclaim, so the caller does not sign an
// empty transaction.
export async function reclaimExpiredSlots(
owner: string,
nowMs: number = Date.now(),
): Promise<Transaction | null> {
const { expired } = await grantsByState(owner, nowMs);
if (expired.length === 0) return null;
const tx = new Transaction();
tx.setSender(owner);
for (const grant of expired) {
tx.add(sessions.revokeSession({ wrapperId: wrapperId(owner), session: grant.session }));
}
return tx;
}
Trade as a session key
The 4 Predict builders mirror the expiry_market entrypoints of the same name, but the session key signs them instead of the owner. Compared with the owner-signed call, you pass no Auth, because the wrapper generates app authorization internally, and each builder adds the accountRegistry and sessionsConfig arguments from its SessionsConfig. Predict still runs its own validation, and the Sessions page lists the aborts a session caller still meets. If the account registry does not authorize SessionsApp, every trading call aborts with EAppNotAuthorized, as the Sessions page explains under deauthorizing the app.
Every Predict builder follows these conventions:
- The session address signs as the transaction sender, so it needs SUI for gas unless another address sponsors the transaction.
priceris a programmable transaction block (PTB) result, not an object ID. AddloadLivePricerfrom@mysten/deepbook-v3/predictin an earlier command of the same transaction and pass its result.protocolConfigis the PredictProtocolConfigobject ID, which thegetSessionsConfigresult carries.lowerTickandhigherTickare absolute ticks. Predict Markets and Pricing SDK shows how to resolve a strike to ticks.- Quantities, costs, and probabilities are raw onchain integers, and the builders pass them through unchanged. Convert them with
usdcToRawandprobabilityToRawfrom@mysten/deepbook-v3/predict, which DeepBook Predict SDK describes.
mintExactQuantity
Use mintExactQuantity to mint a position of an exact payout quantity as the session key. It returns a (tx: Transaction) => TransactionResult function whose result is the new order ID, a u256. The builder requires both caps and gives them no default. The contract asserts that each value is at most its cap, so a cap of u64::MAX never trips.
It takes these parameters:
expiryMarketId: The expiry market object ID.wrapperId: The owner'sAccountWrapperID, not an ID derived from the session address.protocolConfig: The PredictProtocolConfigobject ID.pricer: TheloadLivePricerresult from an earlier command in the same transaction.lowerTick: The lower bound of the position's strike range, as an absolute tick.higherTick: The upper bound of the position's strike range, as an absolute tick.quantity: The payout quantity to mint, in raw units.maxCost: The all-in cost ceiling, premium plus fees, in raw quote units.maxProbability: The ceiling on the fill probability, in raw fixed point.
packages/deepbook-v3/src/sessions.ts. You probably need to run `pnpm prebuild` and restart the site.mintExactAmount
Use mintExactAmount to mint by spending up to a premium budget and flooring the quantity received, as the session key. It returns a (tx: Transaction) => TransactionResult function whose result is the new order ID, a u256. The builder requires maxCost, and the contract requires it to be greater than 0.
It takes these parameters:
expiryMarketId: The expiry market object ID.wrapperId: The owner'sAccountWrapperID.protocolConfig: The PredictProtocolConfigobject ID.pricer: TheloadLivePricerresult from an earlier command in the same transaction.lowerTick: The lower bound of the position's strike range, as an absolute tick.higherTick: The upper bound of the position's strike range, as an absolute tick.maxPremium: The premium budget, in raw quote units.minQuantity: The smallest payout quantity you accept, in raw units.maxCost: The all-in cost ceiling, premium plus fees, in raw quote units.
packages/deepbook-v3/src/sessions.ts. You probably need to run `pnpm prebuild` and restart the site.redeemLive
Use redeemLive to close part or all of a live position at the pricer's mark as the session key. It returns a (tx: Transaction) => TransactionResult function whose result is an Option<u256>, the replacement order ID when a partial close leaves quantity open. Store that replacement ID, because the next close must target it. minProbability and minProceeds are close-side floors, and the builder sends 0, which disables a floor, for each one you omit.
redeemLive sends 0 for an omitted minProbability or minProceeds, and a floor of 0 lets the session key close the position at whatever price the mark gives. Pass real floors on every close a session key signs.
It takes these parameters:
expiryMarketId: The expiry market object ID.wrapperId: The owner'sAccountWrapperID.protocolConfig: The PredictProtocolConfigobject ID.pricer: TheloadLivePricerresult from an earlier command in the same transaction.orderId: The ID of the order to close.closeQuantity: The payout quantity to close, in raw units.minProbability: The lowest fill probability you accept, in raw fixed point. The builder sends0when you omit it.minProceeds: The lowest proceeds you accept for the whole close, in raw quote units. The builder sends0when you omit it.
packages/deepbook-v3/src/sessions.ts. You probably need to run `pnpm prebuild` and restart the site.redeemSettled
Use redeemSettled to claim a settled position in full as the session key. It returns a (tx: Transaction) => void function. It takes no pricer, because settlement has already fixed the price, and no quantity, because a settled claim closes the order in full.
It takes these parameters:
expiryMarketId: The expiry market object ID.wrapperId: The owner'sAccountWrapperID.protocolConfig: The PredictProtocolConfigobject ID.orderId: The ID of the settled order to claim.
packages/deepbook-v3/src/sessions.ts. You probably need to run `pnpm prebuild` and restart the site.Slippage bounds differ from the facade
The session builders and the @mysten/deepbook-v3/predict facade differ on slippage protection in both directions, and both differences make the session key holder set bounds explicitly:
- Required mint caps: The session mint builders require their caps.
mintExactQuantitytakesmaxCostandmaxProbabilityas required parameters, andmintExactAmountrequiresmaxCost. The facade'stx.mint, on Predict Positions SDK, sendsU64_MAXfor a cap you omit, which is a cap that can never trip. - Close-side floors on
redeemLive: The session builder passesminProbabilityandminProceedsto the contract. The facade'stx.redeem, on Predict Positions SDK, sends0for both and has no option to raise them. Omitting a floor on the session builder also sends0, so pass real floors on anything a session key can reach.
Mint and close as the session key
The following example quotes a mint through the owner's facade, then mints, closes, and claims as the session key with explicit caps and floors:
import type { MarketDescriptor, MintQuote, Side } from '@mysten/deepbook-v3/predict';
import {
binaryRangeTicks,
getConfig,
loadLivePricer,
priceToRaw,
probabilityToRaw,
toGeneratedConfig,
usdcToRaw,
} from '@mysten/deepbook-v3/predict';
import { Transaction } from '@mysten/sui/transactions';
import { client } from './client.js';
import { NETWORK, UNDERLYING } from './config.js';
import { admissibleStrike, tradeableMarket } from './markets.js';
import { SESSIONS_CONFIG, sessions, wrapperId } from './sessions-authorize.js';
// The Predict session wrappers take the same market parameters as Predict itself,
// so they need the Predict deployment's IDs alongside the sessions IDs. The
// `/predict` subpath exports the projection and the pricer loader precisely so
// these wrappers can be composed.
const PREDICT = getConfig(NETWORK);
const GENERATED = toGeneratedConfig(PREDICT);
const FEEDS = PREDICT.underlyings[UNDERLYING];
// Mint a directional position as the session key.
//
// Three things differ from the owner-signed `/predict` flow:
//
// 1. The session key is the sender. The wrapper derives the caller from the
// sender, mints app authorization internally, and consumes it in the same
// call, so no `Auth` value is built or passed.
// 2. `pricer` is a PTB result, not an object ID. Load it with
// `loadLivePricer` in a preceding command of the same transaction.
// 3. `maxCost` and `maxProbability` are required. The `/predict` facade defaults
// a missing cap to `U64_MAX`, which is no cap at all; the session builder
// makes you name both. Quantities and costs are raw six-decimal USDC, and
// probabilities are raw fixed point at 1e9.
export async function mintAsSession(params: {
owner: string;
session: string;
side: Side;
// Maximum payout in USD, at $1 per contract. Must be a whole $0.01 lot.
quantity: number;
// Omit to trade at the market's onchain reference price, the window anchor.
targetStrikeUsd?: number;
}): Promise<{ tx: Transaction; quote: MintQuote; descriptor: MarketDescriptor }> {
const { owner, session, side, quantity, targetStrikeUsd } = params;
const market = await tradeableMarket();
// The session wrappers take absolute ticks, so resolve the strike to a number
// before converting. `tradeableMarket` already skips markets whose reference
// price is unseeded.
if (market.referencePrice === null) {
throw new Error(`Market ${market.id} has no reference price yet.`);
}
const strikeUsd =
targetStrikeUsd === undefined
? market.referencePrice
: admissibleStrike(market, targetStrikeUsd);
const { lowerTick, higherTick } = binaryRangeTicks(
priceToRaw(strikeUsd),
side,
priceToRaw(market.tickSize),
);
// Quote through the owner path first. The quote dry-runs the same market, the
// same account, and the same fee path, so its `cost` is the figure to size the
// cap against. Quote immediately before sending: on the short cadences the
// entry probability moves fast.
const descriptor: MarketDescriptor = {
underlying: UNDERLYING,
expiryMs: market.expiryMs,
marketId: market.id,
side,
strike: strikeUsd,
};
const quote = await client.predict.read.quoteMint(owner, descriptor, { quantity });
const tx = new Transaction();
// The session key signs, not the owner.
tx.setSender(session);
const pricer = tx.add(
loadLivePricer(GENERATED, {
expiryMarketId: market.id,
pythFeed: FEEDS.pythFeed,
blockScholesValueStore: FEEDS.blockScholesValueStore,
blockScholesSviStore: FEEDS.blockScholesSviStore,
}),
);
tx.add(
sessions.mintExactQuantity({
expiryMarketId: market.id,
wrapperId: wrapperId(owner),
protocolConfig: SESSIONS_CONFIG.protocolConfig,
pricer,
lowerTick,
higherTick,
quantity: usdcToRaw(quantity),
// All-in debit ceiling, one percent above the quoted cost.
maxCost: usdcToRaw(Math.ceil(quote.cost * 1.01 * 1e6) / 1e6),
// Independent ceiling on the fill price, 0 to 1 per $1 of payout.
maxProbability: probabilityToRaw(
Math.min(1, Number((quote.entryProbability * 1.02).toFixed(9))),
),
}),
);
return { tx, quote, descriptor };
}
// Close part or all of a live position as the session key.
//
// `minProbability` and `minProceeds` are close-side floors, and omitting either
// sends zero. Zero on a delegated key means the position closes at whatever the
// mark gives, so pass real floors on anything a session key can reach. The
// `/predict` facade sends zero for both and exposes no way to raise them, which
// is why the session builder is the one that can protect a close.
export async function closeAsSession(params: {
owner: string;
session: string;
expiryMarketId: string;
orderId: bigint;
// Payout quantity to close, in USD. Must be a whole $0.01 lot.
quantity: number;
// Minimum acceptable fill probability, 0 to 1.
minProbability: number;
// Minimum acceptable USDC proceeds for the whole close.
minProceeds: number;
}): Promise<Transaction> {
const { owner, session, expiryMarketId, orderId, quantity } = params;
const tx = new Transaction();
tx.setSender(session);
const pricer = tx.add(
loadLivePricer(GENERATED, {
expiryMarketId,
pythFeed: FEEDS.pythFeed,
blockScholesValueStore: FEEDS.blockScholesValueStore,
blockScholesSviStore: FEEDS.blockScholesSviStore,
}),
);
tx.add(
sessions.redeemLive({
expiryMarketId,
wrapperId: wrapperId(owner),
protocolConfig: SESSIONS_CONFIG.protocolConfig,
pricer,
orderId,
closeQuantity: usdcToRaw(quantity),
minProbability: probabilityToRaw(params.minProbability),
minProceeds: usdcToRaw(params.minProceeds),
}),
);
// A partial close retires the order ID and returns a replacement as
// `Option<u256>`. Read it from the transaction result and store it, or the
// next close targets an order that no longer exists.
return tx;
}
// Claim a settled position as the session key. No pricer, because the settlement
// price is fixed, and no quantity, because a settled claim closes the order in
// full.
export function claimSettledAsSession(params: {
owner: string;
session: string;
expiryMarketId: string;
orderId: bigint;
}): Transaction {
const { owner, session, expiryMarketId, orderId } = params;
const tx = new Transaction();
tx.setSender(session);
tx.add(
sessions.redeemSettled({
expiryMarketId,
wrapperId: wrapperId(owner),
protocolConfig: SESSIONS_CONFIG.protocolConfig,
orderId,
}),
);
return tx;
}
Revoke a session
Revocation removes a grant and frees its slot immediately, and the revoked address cannot trade from the next transaction on. It stops future calls only: it does not unwind positions, cancel resting orders, or reverse executed trades.
revokeSession
Use revokeSession to remove the grant for session. It returns a (tx: Transaction) => void function that adds the revoke_session call, and the owner must sign it. The call takes no SessionsConfig object and has no version gate, so it keeps working after a watermark bump retires the package version.
Revoking an address that holds no grant is a silent no-op: the call neither aborts nor emits SessionRevoked, so a successful transaction does not prove that the call removed a grant. Read sessionExpirationMs before and after when the difference matters. The Sessions page explains why the transaction result cannot confirm a revocation.
It takes these parameters:
wrapperId: The owner'sAccountWrapperID.session: The session address to revoke.
packages/deepbook-v3/src/sessions.ts. You probably need to run `pnpm prebuild` and restart the site.Revoke and confirm
The following example builds an owner-signed revocation, reads a session's expiry through a simulation, and records whether a grant existed before the revocation:
import { bcs } from '@mysten/sui/bcs';
import { Transaction } from '@mysten/sui/transactions';
import { client } from './client.js';
import { sessions, wrapperId } from './sessions-authorize.js';
// Revoke one grant. The owner signs, the slot frees immediately, and the address
// stops being able to trade from the next transaction on.
//
// Revocation takes no `SessionsConfig` and is not version gated, so it keeps
// working after the sessions package is retired by a version bump. It does not
// unwind anything the session already did: positions, resting spot orders, and
// executed trades all survive.
export function revokeSession(owner: string, session: string): Transaction {
const tx = new Transaction();
tx.setSender(owner);
tx.add(sessions.revokeSession({ wrapperId: wrapperId(owner), session }));
return tx;
}
// Read one address's absolute expiry, in milliseconds since the epoch.
//
// `session_expiration_ms` returns `Option<u64>`: `none` for an address that was
// never granted or has been revoked, and the stored timestamp otherwise. It does
// not classify the timestamp, so compare it with the current time yourself, and
// remember the comparison is strict: the grant is dead AT `expiresAtMs`.
//
// The call is a `public fun` with a return value rather than an `entry` call, so
// simulate it and decode the returned BCS instead of executing it.
export async function sessionExpirationMs(
owner: string,
session: string,
): Promise<bigint | null> {
const tx = new Transaction();
// A simulation needs a sender; any address works for a read.
tx.setSender(owner);
tx.add(sessions.sessionExpirationMs({ wrapperId: wrapperId(owner), session }));
const result = await client.core.simulateTransaction({
transaction: tx,
// Public non-entry functions are only inspectable with checks disabled.
checksEnabled: false,
include: { commandResults: true },
});
if (result.$kind === 'FailedTransaction') {
throw new Error(`session_expiration_ms simulation failed for ${session}.`);
}
const returned = result.commandResults?.[0]?.returnValues?.[0]?.bcs;
if (!returned) throw new Error('simulateTransaction returned no command results.');
const expiry = bcs.option(bcs.u64()).parse(returned);
return expiry === null ? null : BigInt(expiry);
}
// Whether `session` can trade right now. `false` covers both an address that
// never held a grant and one whose grant has expired or been revoked.
export async function isSessionActive(
owner: string,
session: string,
nowMs: number = Date.now(),
): Promise<boolean> {
const expiry = await sessionExpirationMs(owner, session);
return expiry !== null && BigInt(nowMs) < expiry;
}
// Confirm a revocation by reading, because the transaction result cannot.
//
// Revoking an address that holds no grant is a silent no-op: it does not abort
// and it emits no `SessionRevoked` event, so a successful transaction proves
// nothing about whether a grant was removed or was never there. Read before and
// after when the difference matters, for example when you are auditing a key you
// believe you granted.
export async function revokeAndConfirm(
owner: string,
session: string,
): Promise<{ tx: Transaction; hadGrant: boolean }> {
const before = await sessionExpirationMs(owner, session);
return { tx: revokeSession(owner, session), hadGrant: before !== null };
}
DeepBook spot wrappers
SessionsContract does not wrap the 5 DeepBook spot session calls, because the SDK does not model the surrounding workflow of discovering the account's embedded balance manager and reading its resting orders and locked balances. Reach them through the generated sessionsMoveCalls namespace instead:
placeLimitOrder: Places a DeepBook spot limit order for the account.placeMarketOrder: Places a DeepBook spot market order for the account.cancelLiveOrder: Cancels a single DeepBook spot order.cancelLiveOrders: Cancels several DeepBook spot orders.withdrawSettledAmounts: Sweeps settled DeepBook spot proceeds into account custody.
Whether these calls succeed depends on a DeepBook registry authorization that differs by network, and the Sessions page covers how to check it.
Each generated function takes an options object with these fields:
arguments: The Move arguments, in an object whose keys are the parameter names.typeArguments: The pool's base and quote coin types.config: ThesessionsPackageIdandsessionsConfigIDs. The function resolves the package address from it and fills in thesessionsConfigargument, so you can omit that argument.package: An explicit package ID, which overridesconfig.sessionsPackageId.
The functions also add the Clock, and the AccumulatorRoot that the order-placement calls take, so you never pass either. The following snippet cancels a spot order as the session key:
import { getSessionsConfig, sessionsMoveCalls } from '@mysten/deepbook-v3/sessions';
import { Transaction } from '@mysten/sui/transactions';
const ids = getSessionsConfig('testnet');
const tx = new Transaction();
tx.setSender(SESSION_ADDRESS);
tx.add(
sessionsMoveCalls.cancelLiveOrder({
config: { sessionsPackageId: ids.sessionsPackageId, sessionsConfig: ids.sessionsConfig },
arguments: {
pool: POOL_ID,
accountRegistry: ids.accountRegistry,
wrapper: WRAPPER_ID,
orderId: ORDER_ID,
},
typeArguments: [BASE_COIN_TYPE, QUOTE_COIN_TYPE],
}),
);
The functions take arguments by name, so argument positions matter only when you build the moveCall yourself. place_limit_order and place_market_order carry deepbook_registry between pool and account_registry, which puts account_registry at index 2 and sessions_config at index 4. The other 3 take no deepbook_registry, so they keep those arguments at indexes 1 and 3, matching the Predict wrappers.
Other exports
Besides SessionsContract and the spot bindings, the subpath exports the session_config bindings, 4 generated Move structs, and a set of deployment helpers.
sessionConfigMoveCalls
The generated sessionConfigMoveCalls namespace covers the session_config module. id and versionWatermark read the shared SessionsConfig object, and bumpVersionWatermark advances the package version floor, which only the SessionsAdminCap holder can call. The namespace also exports that module's BCS structs, and sessionConfigMoveCalls.SessionsConfig is the onchain object's layout, not the SessionsConfig interface. The Sessions page explains the version watermark.
Move structs
The subpath re-exports 4 generated BCS structs from the sessions module. Each one is a MoveStruct, so its parse method decodes the matching bytes:
SessionsApp: The app type the account registry must authorize before any trading call works.SessionsData: The grant map stored under the account, aVecMapfrom session address to expiry.decodeSessionsdecodes the dynamic field that wraps it.SessionAuthorized: The eventauthorize_sessionemits for a new grant or a re-authorization, withaccount_id,session, andexpires_at_ms.SessionRevoked: The eventrevoke_sessionemits when it removes a grant, with the same 3 fields.
packages/deepbook-v3/src/sessions.ts. You probably need to run `pnpm prebuild` and restart the site.Deployment helpers
The subpath re-exports these deployment helpers, so code that imports only @mysten/deepbook-v3/sessions can name the deployment its IDs came from and format a custody balance:
getDeploymentandgetUnitsTESTNET_DEPLOYMENTandTESTNET_UNITS- The
DeployedNetworkandNetworkArgtypes
DeepBook Predict SDK describes the deployment record they read.
packages/deepbook-v3/src/sessions.ts. You probably need to run `pnpm prebuild` and restart the site.