@esseyonsui/sui-sdk
TypeScript client for Essey on Sui: transaction builders, chain readers, the lending rule checks, sealed-room opening, owner transactions built unsigned, and the key-separation check. Testnet and localnet only.
0.1.11 is the published package; install that. This tree's package.json is 0.1.12 and is not on npm yet. EsseyOwner.sign allows the deployment's oraclePkg on a claim-default, and a shielded withdraw of the relayer fee or less is refused ZERO_WITHDRAW (from SDK 0.1.12 / MCP 0.1.16). Install 0.1.11 with "overrides": { "underscore": "1.13.8" } in the consuming project's package.json (site/content/sdk/install.md). [email protected] pulls bfj → jsonpath → [email protected] (GHSA-qpx9-hpmf-5gmw); npm applies the override only from that root. groth16.fullProve does not call bfj. There is no relayer kind repay_v2; a v2 loan repays a public coin with repayCoinV2Tx. 0.1.10 fixes v1 saleBidTx (Access and gate), requires a pool-bid credential, draws SUI deposits and v2 collateral from the address balance, and adds v2 cancel, expire, liquidate, claim-default, recoverRoomNote, package-keyed abort names, readingFor and suiFaucetError. 0.1.9 was the previous package version (ESM, Node 22+; npm run build writes dist/), on npm. 0.1.9 adds buildPoolBidV2 and buildOfferV2 (src/shielded/spend-v2.ts), which compose a v2 pool spend and its bid_v2 / loan_offer_v2 relayer body in one call and refuse, before sealing or proving, what the chain or the relayer would refuse; the v2 open and attest builders openSaleV2Tx, attestSellerV2Tx, openPrivateLoanV2Tx, attestBorrowerV2Tx; fetchSettleKeys for the Settle(16) key; and snarkjs 0.7.6 as a dependency, so snarkjsProver, settleProver and provedPassFor load it themselves (ProverPaths.snarkjs is optional). It also exports persistentMemberSecrets, walletGasMode and wireBidExt. fetchProofBundle downloads the TEST-ONLY membership and spend keys (@esseyonsui/proving-keys-testonly) and refuses any file whose hash is not the deployed verifier's; fetchSettleKeys does the same for @esseyonsui/settle-keys-testonly. Pool upkeep sizes a pool repay at the live pool::rate_bps and does not sign when that read is missing or not an integer from 0 through 100,000. Offer loans stay on their own rate. Install it with the MCP package (site/content/sdk/install.md). 0.1.3 reads
deployment.oraclePkg when pyth_oracle is its own package, as on the testnet cutover; leave it unset and the reader
stays ${pkg}::${reader}. 0.1.2 added TESTNET_PRIVATE / testnetPrivateMcp() and deployment.access on deposit,
post, fill and pool borrow. Inside this repository
packages/mcp imports it by name, resolved to src/ through the root tsconfig paths. The hosted backend in agents/ and services/ still uses its own copies of these modules;
switching it over is a separate change.
The two keys
An agent runs with exactly one key, its own. Everything an agent signs goes through EsseyAgent.execute
(src/agent.ts), which re-runs the key-separation check before every signature and refuses, with NOT_AGENT_CALL,
any transaction outside the agent lane (src/lane.ts: lendingFunctions, LANE_COMMANDS, the same list the server
sponsors). No call in the lane hands a loan's deed to an address, so every loan the agent signs for stays in its
treasury. A borrower runs a sealed room from their own wallet (Ptb.openRoom, openRoomBids, Ptb.settle), signed
outside EsseyAgent. Owner actions (create the account,
set the lender's mandate, deposit, renew, revoke, withdraw, collect a default, act on a borrower's loans) are built by
src/owner.ts and returned by unsignedTx (src/unsigned.ts) as bytes for the owner to sign on another device. No
function in this package takes an owner key.
SeparationCheck (src/separation.ts) refuses when:
| code | meaning |
|---|---|
KEYS_NOT_SEPARATE | the agent's own address owns the OwnerCap of a treasury it holds an AgentCap for |
OWNER_CAP_PUBLIC | that OwnerCap is shared or frozen, so any key can act as owner |
OWNER_CAP_UNVERIFIABLE | the OwnerCap cannot be found or read, or sits inside an object whose module decides who uses it |
AGENT_CAP_MISMATCH | the configured AgentCap is not held by this key, or belongs to another treasury |
It checks every treasury the key holds an AgentCap for, not only the configured ones. Each treasury has exactly one
OwnerCap, created in the transaction that emitted its TreasuryCreated event (move/essey_sui/sources/treasury.move
share); its id is cached and its owner is read again on every run. What it cannot see: an owner key kept in a
different address on the same machine.
Quick start (localnet)
import { connect, deploymentOf, EsseyAgent, loadAgentKey } from "@esseyonsui/sui-sdk";
const chain = await connect({ network: "localnet", rpcUrl: "http://127.0.0.1:9000" });
const agent = await EsseyAgent.start({
chain,
deployment: deploymentOf(JSON.parse(deploymentJson)),
signer: loadAgentKey("/path/to/agent.env", "localnet"),
account: { treasuryId, agentCapId, cash: { treasuryId: cashTreasuryId, capId: cashCapId } },
gas: "address-balance",
});
const landed = await agent.borrowFromOffer(offerId, 25_000_000_000n, 5_000_000n);
if (!landed.ok) console.log(landed.abort); // e.g. { module: "treasury", name: "ERateBelowFloor", code: 17, ... }
A borrower's desk calls are held on chain to the borrow rules its owner sets on the SUI cap (max rate, shortest
term, latest maturity; borrowerOwnerTx("deposit", ..., { limits, borrow })). A desk fill outside them aborts
treasury::ERateAboveCeiling (24), ETermTooShort (25) or EPastRepayBy (26); with no rules set,
ENoBorrowMandate (23). The rules also pin the one pool a pool borrower may use (borrow.poolId; null refuses
every pool); a desk borrow from any other pool aborts EPoolNotAllowed (27). They also name the cash treasury proceeds must land
in (the borrower's own cash side, taken from the treasury pair, never from agent input); a desk borrow into any
other aborts ECashNotAllowed (28). They carry an allowance, what the desk may still borrow (a
refinance uses only its new principal over the old debt); a borrow past it aborts EOverAllowance (29), repaying does
not restore it, the owner's next set does. EsseyAgent.allowance() reads what is left.
A refused transaction still lands: Landed.ok is false, digest is real, and abort names the Move constant
(src/aborts.ts MOVE_ABORTS, held equal to the Move sources by test/aborts.test.ts).
Public API (src/index.ts)
- Connection:
connect,resolveNetwork,assertChainId,TESTNET,MAINNET_CHAIN_ID,LOCALNET_RPC_URL; typesEsseyChain,EsseyNetwork,NetworkConfig. Mainnet is not a network value and a mainnet chain id is refused. - Keys:
loadAgentKey,createAgentKeyFile(KEY=VALUE file, mode 600, tagged with its network). - Agent:
EsseyAgentwithstart,execute,separation,postOffer,cancelOffer,borrowFromOffer,borrowFromPool,repay,refinance,addCollateral,claim,bidSealed,offer,loan,myLoans,limits,recipients,capSides,warnings; typesAgentAccount,AgentOptions,Warning. - Agent lane:
lendingFunctions,loanRoomFunctions,BORROWER_DESK,LANE_COMMANDS,outsideLane,assertAgentLane,normalizeTarget; typeLane. - Builders:
Ptb(every agent PTB, including the borrower deskdesk*calls); typesMe,PtbBook. - Readers:
Book(offers, loans, rooms, room offers, held loans, deeds, pool shares, quotes, owed, underwater, limits, mandates, balances, clock). - Owner (unsigned):
ownerTx,borrowerOwnerTx,renewTx,unsignedTx,OWNER_ACTIONS,BORROWER_OWNER_ACTIONS,LENDER_CAP_LIMIT; typesOwnerRefs,BorrowerOwnerRefs,ChainMandate,RenewSide,UnsignedTx. - Rules: everything in
src/rules.tsandsrc/policy.ts(drawFault,termsFault,allowedFills,parsePolicy,repayBound, ...). - Sealed rooms:
openRoomBids,rankOpenings,bidTerms,offerCommitment,consentDigest,encryptOffer,sealClient,OFFER_CT_LEN, ... - Sealed settlement v2 (sale_room_v2, private_room_v2; Sui testnet ids
TESTNET_SALE_ROOM_V2,TESTNET_PRIVATE_LOAN_V2):openSaleV2Tx,attestSellerV2Tx,bidV2Tx,poolBidV2Tx,finishV2Tx,slashBidV2Tx,openPrivateLoanV2Tx,attestBorrowerV2Tx,offerV2Tx,settleV2Tx,settleRelayedV2Tx,repayV2Tx,repayCoinV2Tx,slashOfferV2Tx; the composersbuildPoolBidV2,buildOfferV2(the finishedSpendplus thebid_v2/loan_offer_v2body;bidV2Body,loanOfferV2Body,poolSpendOf,ShieldedWallet.spendDeps);commitV2,plaintextV2,sealEntryV2,openRoomEntries,settlePlan,settleProver,fetchSettleKeys,settleProvingKeys(TEST-ONLY key@esseyonsui/settle-keys-testonly),readSaleRoomV2,readPrivateLoanV2; typesSaleRoomV2Config,PrivateLoanV2Config,SettleStatement,PoolBidV2Request,OfferV2Request. The host decrypts its room's entries locally and settles with a Groth16 proof; only the winner and the winning value go on chain. - Chain helpers:
simulate,landedOf,createdOf,oneCreated,parseAbort; typesLanded,GasMode,ChainEvent,EventSource. - Errors:
EsseyError(code: EsseyErrorCode),isEsseyError,ESSEY_ERROR_CODES. Codes are never renamed.NOT_AGENT_CALL:executerefused a transaction outside the agent lane; nothing was signed.
Events
Testnet reads events from Sui GraphQL. Localnet reads them from the fullnode's gRPC LedgerService.ListEvents: Sui
1.82 removed JSON-RPC from the fullnode (MystenLabs/sui#27907).
Examples (localnet)
scripts/sdk-e2e-local.sh starts a fresh localnet and runs them in order, then re-reads every printed digest:
examples/localnet.tspublishes Essey immutable (the UpgradeCap is destroyed in the publish PTB, as agents/deploy.ts publishImmutable does) and plays the owner's device (owner key separate from every agent key).examples/lend.tsposts offers; an offer under the mandate floor is refused on chain (treasury::ERateBelowFloor).examples/borrow-repay.tstries an offer over its borrow rule (refused on chain), then borrows and repays through the treasury desk.examples/sealed-room.ts: borrower-1's agent key is refused a room open (NOT_AGENT_CALL); the borrower's own wallet, a separate plain key thatexamples/localnet.tsfunds, opens the room, two lender agents bid sealed, the wallet settles the cheapest and repays. Localnet has no Seal key servers, so the lenders place random bytes of the sealed length; on testnet the same file uses Seal.examples/keys-together.tsshows the SDK and the MCP refusing an agent that owns its own OwnerCap.../mcp/examples/claude.tsdrives the MCP server the way Claude does.
Tests
npm test and npm run typecheck in this folder. test/ holds the original suites of the extracted modules with
their imports adjusted (the five tests that exercise the hosted sponsor lane or the self-serve form stay with the
originals), plus new tests for builders, readers, separation, the agent, networks, keys and events.