TypeScript SDK for integrating with the Scallop lending protocol on Sui.
Current package: @scallop-io/sui-scallop-sdk v5.x. ESM package, Node >=22, peer dependencies @mysten/sui@>=2.22.0, @scallop-io/sui-kit@~2.2.0, @tanstack/query-core@>=5.95.2.
pnpm add @scallop-io/sui-scallop-sdk @mysten/sui @scallop-io/sui-kit @tanstack/query-coreRoot export:
import {
Scallop,
ScallopClient,
ScallopBuilder,
ScallopQuery,
ScallopUtils,
ScallopConstants,
ScallopAddress,
} from '@scallop-io/sui-scallop-sdk';Subpath exports:
import { ScallopClient } from '@scallop-io/sui-scallop-sdk/client';
import { ScallopBuilder } from '@scallop-io/sui-scallop-sdk/builder';
import { ScallopQuery } from '@scallop-io/sui-scallop-sdk/query';
import type { ScallopTxBlock } from '@scallop-io/sui-scallop-sdk/types';
import { ScallopError } from '@scallop-io/sui-scallop-sdk/errors';
import { consoleLogger } from '@scallop-io/sui-scallop-sdk/logger';Supported subpaths:
@scallop-io/sui-scallop-sdk@scallop-io/sui-scallop-sdk/client@scallop-io/sui-scallop-sdk/query@scallop-io/sui-scallop-sdk/builder@scallop-io/sui-scallop-sdk/errors@scallop-io/sui-scallop-sdk/logger@scallop-io/sui-scallop-sdk/types
Scallop is the convenience factory. It owns one initialized ScallopClient and exposes factory methods for the other facades.
Scallop
-> ScallopClient write facade; signs/sends user actions
-> ScallopBuilder tx-block builder; owns SuiKit + TransactionExecutor
-> ScallopQuery read facade; delegates to repositories
-> ScallopUtils
-> ScallopConstants
-> ScallopAddress
Important details:
ScallopIndexermodel was removed. Query/indexer access is internal to repositories.ScallopConstantscomposesScallopAddress; useconstants.addressfor the address adapter.- Back-compatible address forwarders remain on constants:
get,set,getAddresses,getAllAddresses,switchCurrentAddresses.readandisSeededare not forwarded — call them onconstants.address. - Write-path signer/executor lives on
builder.executor; raw SuiKit lives onbuilder.suiKit.
Mainnet example:
import { Scallop } from '@scallop-io/sui-scallop-sdk';
const sdk = new Scallop({
addressId: '695fcdc084f790c04eb068dc',
network: 'mainnet',
fullnodeUrl: 'https://fullnode.mainnet.sui.io:443',
secretKey: process.env.SECRET_KEY,
pythEndpoints: ['https://pyth.dourolabs.app/hermes'],
});
const client = await sdk.createScallopClient();
const query = await sdk.createScallopQuery();
const builder = await sdk.createScallopBuilder();
const utils = await sdk.createScallopUtils();
const constants = await sdk.getScallopConstants();Read-only example:
const sdk = new Scallop({
addressId: '695fcdc084f790c04eb068dc',
network: 'mainnet',
fullnodeUrl: 'https://fullnode.mainnet.sui.io:443',
walletAddress: '0x...',
pythEndpoints: ['https://pyth.dourolabs.app/hermes'],
});
const query = await sdk.createScallopQuery();
const pools = await query.getMarketPools();readTransport: 'graphql' does not replace gRPC — Core reads and all writes still go over gRPC. fullnodeUrl (a gRPC fullnode) is optional here and defaults to the mainnet fullnode; set it to point Core reads and writes at your own node. The flag only makes balance reads and the heavy dynamic-field repos prefer native GraphQL queries. Point GraphQL at its own endpoint with graphqlUrl (defaults to mainnet), or inject a preconfigured graphqlClient:
const sdk = new Scallop({
addressId: '695fcdc084f790c04eb068dc',
network: 'mainnet',
readTransport: 'graphql',
fullnodeUrl: 'https://fullnode.mainnet.sui.io:443', // gRPC node for Core reads + writes (optional; defaults to mainnet)
graphqlUrl: 'https://graphql.mainnet.sui.io/graphql', // GraphQL endpoint (optional; defaults to mainnet)
walletAddress: '0x...',
pythEndpoints: ['https://pyth.dourolabs.app/hermes'],
});The transport options are mutually exclusive at the type level: with readTransport: 'grpc' (or omitted — grpc is the default) graphqlUrl / graphqlClient are rejected, and with readTransport: 'graphql' an injected gRPC suiClient is rejected.
Manual construction is supported. Call .init() before use:
import { ScallopQuery } from '@scallop-io/sui-scallop-sdk/query';
const query = new ScallopQuery({
addressId: '695fcdc084f790c04eb068dc',
network: 'mainnet',
fullnodeUrl: 'https://fullnode.mainnet.sui.io:443',
walletAddress: '0x...',
});
await query.init();Common required options:
addressId: Scallop API address config id.network: Sui network, usually'mainnet'.fullnodeUrl: Sui RPC URL.walletAddress: required for read-only/wallet-scoped queries when no signer is supplied.secretKeyormnemonics: required for signing viaScallopClient.
Common optional options:
readTransport: read transport,'grpc'(default) or'graphql'. The Core read path and all writes always use gRPC (viafullnodeUrl, which defaults to mainnet) regardless of this setting.'graphql'only makes the GraphQL-capable reads prefer GraphQL: balance reads viaGraphQLDataSource, plus some heavy dynamic-field reads (e.g. pool addresses, xOracle, veSCA) that use single nested GraphQL queries instead of the gRPC multi-call fan-out. The transport is selected strictly — there is no automatic fallback between GraphQL and gRPC, so a failing read on the selected transport propagates its error (fail-loud) rather than silently degrading.graphqlUrl/graphqlClient: Sui GraphQL endpoint / preconfiguredSuiGraphQLClient. Only accepted withreadTransport: 'graphql'(rejected at the type level otherwise);graphqlClienttakes precedence overgraphqlUrl, and the endpoint defaults to mainnet when neither is given.pythEndpoints: Pyth Hermes endpoints for price-update flows. Defaulthttps://pyth.dourolabs.app/hermes.pythApiKey: Pyth (Hermes) API access token. The hosted Pyth endpoint now requires a key. When set, Pyth coin prices are read directly from the Pyth API (sent as the HermesaccessToken); when omitted, prices are read from the Scallop indexer instead. Either way,getPythCoinPrice(s)falls back to on-chain feed objects if the API source fails.queryClient/queryClientConfig: custom@tanstack/query-corecache. Wallet-balance reads are memoised through this cache with a 5s TTL (staleTime/gcTime). On the gRPC transport, coin-amount readers share onelistBalancessnapshot per node + address, so a balance read within ~5s of a supply/withdraw can return the pre-write amount. If you need read-your-write freshness right after a mutation, invalidate thegetAllCoinBalances(and per-coingetCoinBalance) query-key prefixes on yourqueryClient, or shorten the TTL viaqueryClientConfig.priceTimeout: cache lifetime (ms) for the full Pyth price-feed list. Default5_000. Within this window, single/subset price reads are served from one cached full-list fetch instead of re-hitting the Pyth API; a longer value cuts API traffic at the cost of price staleness.logger: SDK logger. Default is silentnoopLogger; passconsoleLoggerto opt into console output.strictInit: whentrue,init()throwsScallopConfigErrorif required config is missing.tokensPerSecond: RPC read rate limit.usePythPullModel,useOnChainXOracleList,sponsoredFeeds: tx-builder oracle behavior.
The SDK builds its own transport clients by default, but you can inject your own:
suiClient(ClientWithCoreApifrom@mysten/sui): overrides the Sui RPC client used for on-chain reads. When omitted, the SDK builds aSuiGrpcClientfromnetwork+fullnodeUrl. Accepted byScallopUtils/ScallopQuery/ScallopBuilder/ScallopClient/Scallop.httpClient(AxiosInstance): overrides the HTTP client used for Scallop API / address-config fetches. When omitted, the SDK creates an Axios instance from the APIurl+timeout. Accepted anywhereScallopAddressconfig flows (Scallop,ScallopConstants,ScallopAddress).client(ScallopClient): only on the top-levelScallopconstructor — reuse an already-builtScallopClientinstead of constructing a new one. Unrelated to the two transport clients above.
import { Scallop } from '@scallop-io/sui-scallop-sdk';
import { SuiClient } from '@mysten/sui/client';
import axios from 'axios';
const sdk = new Scallop({
addressId: '695fcdc084f790c04eb068dc',
network: 'mainnet',
fullnodeUrl: 'https://fullnode.mainnet.sui.io:443',
walletAddress: '0x...',
suiClient: new SuiClient({ url: 'https://fullnode.mainnet.sui.io:443' }), // custom Sui RPC client
httpClient: axios.create({ timeout: 15_000 }), // custom HTTP client for API/address fetches
});Note: prior to v4.3.0 these were all named
clientand collided into an unusable intersection type. UsesuiClient/httpClient(and the top-levelclient) on v4.3.0+.
const marketPools = await query.getMarketPools();
const suiPool = await query.getMarketPool('sui');
const collaterals = await query.getMarketCollaterals();
const obligations = await query.getObligations('0xOwner');
const obligation = await query.queryObligation('0xObligationId');
const portfolio = await query.getUserPortfolio({ walletAddress: '0xOwner' });
const prices = await query.getPythCoinPrices({ coinNames: ['sui', 'usdc'] });
const allPrices = await query.getAllCoinPrices();
const stakeAccounts = await query.getAllStakeAccounts('0xOwner');
const tvl = await query.getTvl();Read source selection is available on supported facade methods via legacy-compatible flags:
await query.getMarketPools(undefined, { source: 'rpc' }); // on-chain RPC
await query.getMarketPools(undefined, { source: 'indexer' }); // API/indexer only
await query.getMarketPools(undefined, { source: 'indexer-first' }); // API/indexer with RPC fallback
await query.getMarketPools(undefined, { indexer: true }); // same as indexer-firstRepository internals normalize those values to onchain, api, or api-first.
ScallopClient methods sign and execute by default. Pass false where supported to receive an unsigned transaction instead.
const result = await client.openObligation();
await client.supply('sui', 1_000_000_000);
await client.depositCollateral('sui', 1_000_000_000);
await client.withdraw('sui', 1_000_000_000);
await client.withdrawCollateral('sui', 1_000_000_000);
await client.borrow(
'usdc',
1_000_000,
true,
'0xObligationId',
'0xObligationKey'
);
await client.repay(
'usdc',
1_000_000,
true,
'0xObligationId',
'0xObligationKey'
);
const tx = await client.supply('sui', 1_000_000_000, false);Other write helpers include:
- lending:
supply,withdraw,flashLoan - collateral:
depositCollateral,withdrawCollateral - borrow:
openObligation,borrow,repay - spool:
createStakeAccount,stake,unstake,claim,supplyAndStake,unstakeAndWithdraw - veSCA / incentives:
stakeObligation,unstakeObligation,claimBorrowIncentive,claimAllUnlockedSca - migration/test helpers:
migrateAllMarketCoin,mintTestCoin
Use ScallopBuilder for custom transaction composition.
const tx = builder.createTxBlock();
await tx.supplyQuick('sui', 1_000_000_000);
await tx.depositCollateralQuick('sui', 1_000_000_000);
const result = await builder.executor.signAndSendTxn(tx);ScallopTxBlock exposes both flat methods and module-grouped methods. References are identity-equal:
tx.supplyQuick === tx.core.supplyQuick; // true
tx.stake === tx.spool.stake; // trueModules:
tx.core: lending, collateral, borrow, liquidations, flash loanstx.spool: staking market coinstx.vesca: veSCA lock/split/merge/redeem flowstx.borrowIncentive: obligation staking and incentive claimstx.referral: referral binding/revenue flowstx.loyalty: loyalty reward claimstx.sCoin: sCoin mint/burn
Method conventions:
- normal methods are synchronous Move-call wrappers and return
TransactionResult. *Quickmethods are async helpers that fetch required coins/objects/oracle updates, call normal methods, and return leftovers where needed.- canonical lending names are
supply/supplyQuick/depositCollateral/depositCollateralQuick. - legacy
deposit*andaddCollateral*names are deprecated.
const constants = await sdk.getScallopConstants();
const corePackage = constants.get('core.packages.protocol.id');
const allAddresses = constants.getAddresses();
const addressAdapter = constants.address;
const addressId = addressAdapter.addressId;constants.whitelist and constants.poolAddresses are frozen snapshots after init().
SDK internals throw typed errors:
ScallopRpcError: Sui RPC / gRPC failuresScallopIndexerError: Scallop API/indexer HTTP failuresScallopParseError: invalid or unexpected payloadScallopConfigError: config validation failureScallopTransactionBuildError: tx construction failure
import { ScallopError } from '@scallop-io/sui-scallop-sdk/errors';
import { consoleLogger } from '@scallop-io/sui-scallop-sdk/logger';
const sdk = new Scallop({
addressId: '695fcdc084f790c04eb068dc',
network: 'mainnet',
fullnodeUrl: 'https://fullnode.mainnet.sui.io:443',
walletAddress: '0x...',
logger: consoleLogger,
});
try {
await query.getMarketPools();
} catch (error) {
if (error instanceof ScallopError) {
// error.cause and error.context are available where provided
}
}pnpm install
pnpm run build
pnpm run test:typecheck
pnpm run test:unitUseful scripts:
pnpm run build # production build
pnpm run build:dev # development build
pnpm run test # typecheck + all tests
pnpm run test:typecheck # TypeScript checks for tests
pnpm run test:no-console # no console.* in SDK internals
pnpm run test:unit # network-free unit tests
pnpm run test:repo # repository unit tests only
pnpm run test:integration # integration tests; needs network + local env setup
pnpm run test:watch # watch mode
pnpm run lint:fix
pnpm run format:fix
pnpm run doc # generate typedoc output into docs/Integration/query/full test runs require local environment variables such as SECRET_KEY. Do not commit secrets.
- Contributor architecture:
llm-docs/SDK_STRUCTURE.md - Client guide:
document/client.md - Query guide:
document/query.md - Address guide:
document/address.md - Builder guide:
document/builder.md - Utils guide:
document/utils.md - Constants guide:
document/constants.md