sheardocs

Developers

TypeScript SDK

ShearClient: PDA derivation, unsigned instruction builders, finalized account reads and integer math matching the program.

@shear/sdk is the TypeScript client for the Shear program. Its ShearClient derives PDAs, builds unsigned instructions from the bundled Anchor IDL and reads finalized accounts. It never signs or sends a transaction; the caller does that with the owner’s key.

  • ES module for Node.js. loadIdl() reads idl/shear.json from the package with node:fs.
  • Built on @coral-xyz/anchor 0.31.1, @solana/web3.js 1.98.4 and @solana/spl-token 0.4.15.
  • Amounts are bigint in base units. See units.

Setup#

ts
import anchor from '@coral-xyz/anchor';
import { Connection, Keypair } from '@solana/web3.js';
import { ShearClient } from '@shear/sdk';

const connection = new Connection(process.env.SOLANA_RPC_URL!, 'finalized');
const owner = Keypair.fromSecretKey(secretKey);
const client = new ShearClient(connection, new anchor.Wallet(owner));
const vault = client.vault();

The constructor creates an Anchor Program with finalized commitment and confirmed preflight commitment. The wallet only fills the provider; the builders never use it to sign.

Constants#

ExportValueMeaning
SHEAR_PROGRAM_IDGG8Db3XMAfAaa4RT38e5uaeFSDjztEyaATpR6B3AxMcnProgram ID. client.programId is read from the IDL address and matches it.
TSLA_X_MINTXsDoVfqeBukxuZHWhdvWHBhgEHjGNst4MLodqsJHzoBTSLAx mint, Token-2022.
USDC_MINTEPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1vUSDC mint, legacy SPL Token.
PYTH_RECEIVERrec2HHDDnjLfj4kE7VyEtFA1HPGQLK33259532cRyHpPyth receiver program.
TSLA_FEED16dad506d7db8da01c87581c87ca897a012a153557d4d578c3b9c9e1bc0632f1Pyth feed ID as a hex string.
TOKEN_PROGRAM_IDTokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DARe-exported from @solana/spl-token.
TOKEN_2022_PROGRAM_IDTokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEbRe-exported from @solana/spl-token.
SystemProgram—Re-exported from @solana/web3.js.
U64_MAX2^64 - 1Largest u64.
BPS10_000nBasis-point denominator.
MULTIPLIER_SCALE1_000_000_000nMultiplier scale.
PRICE_SCALE1_000_000nUSD mark scale.
UNSTAKE_SECONDS604_800Wait from requestUnstake to the earliest withdrawStake.
STAKE_DECIMALS6Decimals the program requires of the bidder stake mint.

ShearClient#

MemberReturnsMeaning
new ShearClient(connection, wallet, idl?)ShearClientidl defaults to loadIdl().
program, programId, connection, walletreadonlyAnchor program, ID from the IDL, and the constructor inputs.
pda(seed, ...keys)PublicKeyPDA for a string seed followed by public keys or buffers.
vault(mint?)PublicKey["vault", mint]. mint defaults to TSLA_X_MINT.
round(vault, index)PublicKey["round", vault, u64le(index)]. index is a bigint.
position(vault, owner)PublicKey["position", vault, owner].
session(vault, close)PublicKey["session", vault, i64le(close)]. close is a bigint of Unix seconds.
vaultPdas(vault?)Record<string, PublicKey>vault, selfProgram, programData, underlyingMint, usdcMint, shareMint, custody, premium, assetProgram, tokenProgram, systemProgram.
roundPdas(round)Record<string, PublicKey>round, collateral, deposits, reserve, bids, optionMint.
positionPdas(vault, owner, round?)Record<string, PublicKey>owner, position, positionShares, bidder; with round, also ticket and bid.
stakeConfig(vault)PublicKey["stake-config", vault].
stakePdas(vault, owner)Record<string, PublicKey>owner, bidder, escrow (["stake", bidder]) and stakeConfig.
instruction(name, args, accounts)Promise<TransactionInstruction>Unsigned instruction for a camelCase IDL name.
fetchAccount(address, expectedKind?)Promise<DecodedAccount>Finalized read. Checks the owner program, the discriminator, the data length and, if given, the account type.
fetchVault(address), fetchRound(address)Promise<DecodedAccount>fetchAccount with the type fixed.

Module functions#

FunctionReturnsMeaning
loadIdl()IdlBundled IDL, cached after the first read.
decodeAccount(data, idl?)DecodedAccount | nullMatches the discriminator and returns { kind, data }, with kind such as Vault. Null for unknown data; throws on truncated data.
decodeEvent(base64, idl?){ name, data } | nullDecodes the payload of one Program data: log line.
jsonValue(value)unknownPublic keys to base58, BN and bigint to decimal strings, snake_case keys to camelCase.
u64le(value)Buffer8-byte little-endian u64 for PDA seeds.

Building instructions#

instruction(name, args, accounts) looks up name in the camelCase IDL, checks the argument count, converts bigint arguments to Anchor BN, and builds with accountsStrict. Anchor resolves no accounts, so the map must hold every account the instruction lists. Extra keys are ignored, which lets the helper maps be spread together.

Account names per instruction are under instruction accounts. The helpers do not derive the owner’s token accounts. TSLAx accounts use the Token-2022 program; USDC, share and call accounts use the legacy token program.

Deposit#

A position must exist before the first deposit; create it once with createPosition. The vault’s active flag decides which deposit instruction applies. See deposits.

Vault stateInstructionShares
active false, between roundsfundReady(amount, minShares)Minted at once at the custody ratio.
active true, round in Auction, Active or ObservedqueueDeposit(amount, minShares)Minted by process_ticket after settlement, at the snapshot ratio.

While the round is Disputed or Settled, both fail with WrongPhase. Deposits open again with fundReady once close_round has run.

ts
import { Transaction, sendAndConfirmTransaction, type TransactionInstruction } from '@solana/web3.js';
import { getAssociatedTokenAddressSync } from '@solana/spl-token';
import { TSLA_X_MINT, TOKEN_2022_PROGRAM_ID, depositShares } from '@shear/sdk';

const me = owner.publicKey;
const amount = 250_000_000n; // 2.5 TSLAx
const source = getAssociatedTokenAddressSync(TSLA_X_MINT, me, false, TOKEN_2022_PROGRAM_ID);
const ixs: TransactionInstruction[] = [];

if (!(await connection.getAccountInfo(client.position(vault, me)))) {
  ixs.push(await client.instruction('createPosition', [], {
    ...client.vaultPdas(vault),
    ...client.positionPdas(vault, me),
  }));
}

const { data: v } = await client.fetchVault(vault);
if (v.active === false) {
  const pdas = client.vaultPdas(vault);
  const custody = BigInt((await connection.getTokenAccountBalance(pdas.custody)).value.amount);
  const supply = BigInt((await connection.getTokenSupply(pdas.shareMint)).value.amount);
  const minShares = depositShares(amount, custody, supply); // fewer on chain fails with Slippage (6022)
  ixs.push(await client.instruction('fundReady', [amount, minShares], {
    ...pdas,
    ...client.positionPdas(vault, me),
    source,
  }));
} else {
  const round = client.round(vault, BigInt(v.roundIndex as string));
  const minShares = 0n; // checked against the settlement snapshot; below it the deposit is refunded
  ixs.push(await client.instruction('queueDeposit', [amount, minShares], {
    ...client.vaultPdas(vault),
    ...client.roundPdas(round),
    ...client.positionPdas(vault, me, round),
    source,
  }));
}

await sendAndConfirmTransaction(connection, new Transaction().add(...ixs), [owner]);

Withdraw#

Exits follow the same split. redeemReady(shares, minAssets) pays at once while the vault is inactive. requestWithdrawal(shares) locks shares during a round; after settlement, process_ticket pays the ticket owner’s TSLAx account. Anyone can send it, and the keeper does.

ts
// Vault inactive: burn shares, receive TSLAx now
await client.instruction('redeemReady', [shares, minAssets], {
  ...client.vaultPdas(vault),
  ...client.positionPdas(vault, me),
  destination: source,
});

// Round running: queue on the ticket (the context also takes deposits and source)
await client.instruction('requestWithdrawal', [shares], {
  ...client.vaultPdas(vault),
  ...client.roundPdas(round),
  ...client.positionPdas(vault, me, round),
  source,
});

Bid#

Bidding needs a Bidder entry with allowed set by the vault admin, or an active bidder stake. A wallet places one bid per round, while the round is in Auction and before auctionEnd. See bidding.

quantity
Calls in 8-decimal call atoms. 100_000_000n is one call on one TSLAx. At most the round’s offered.
limitPremiumMicro
Highest premium accepted, in micro-USDC per whole call. At least the vault’s reservePremiumMicro.

escrow = ceil(quantity × limitPremiumMicro / 10^8)

USDC moved into the round’s bid escrow by placeBid. After the auction the bid pays ceil(allocation × clearing / 10^8) and claimBid refunds the rest.
ts
import { getAssociatedTokenAddressSync } from '@solana/spl-token';
import { USDC_MINT, mulDivCeil } from '@shear/sdk';

const { data: v } = await client.fetchVault(vault);
const round = client.round(vault, BigInt(v.roundIndex as string));
const usdc = getAssociatedTokenAddressSync(USDC_MINT, me);

const quantity = 1_500_000_000n;      // 15 calls
const limitPremiumMicro = 4_250_000n; // 4.25 USDC per call
const escrow = mulDivCeil(quantity, limitPremiumMicro, 100_000_000n); // 63_750_000n = 63.75 USDC

const bid = await client.instruction('placeBid', [quantity, limitPremiumMicro], {
  ...client.vaultPdas(vault),
  ...client.roundPdas(round),
  ...client.positionPdas(vault, me, round),
  source: usdc,
});

Stake to bid#

stakeBidder locks the vault’s current stake amount from the wallet’s SHEAR account. requestUnstake starts the UNSTAKE_SECONDS wait; withdrawStake returns the stake once the wait has passed and every bid placed through it is claimed.

ts
import { getAssociatedTokenAddressSync } from '@solana/spl-token';

const config = await client.fetchAccount(client.stakeConfig(vault), 'stakeConfig');
const mint = new PublicKey(config.data.mint as string);
const shear = getAssociatedTokenAddressSync(mint, me);
const stake = client.stakePdas(vault, me);

const lock = await client.instruction('stakeBidder', [], { ...client.vaultPdas(vault), ...stake, mint, source: shear });
const ask = await client.instruction('requestUnstake', [], { vault, ...stake });
// Seven days later, with every staked bid claimed:
const back = await client.instruction('withdrawStake', [], { ...client.vaultPdas(vault), ...stake, destination: shear });

Claim#

InstructionSignerWhenPays
claimBidBidderRound past Auction.Unused escrow in USDC and the allocated calls.
claimPremiumShare ownerAny time.All credited premium in USDC.
claimOptionCall holderRound Settled or Closed.TSLAx for the calls burned.

The call token account must exist before claimBid. Calls use the legacy token program, so a standard associated token account works.

ts
import { createAssociatedTokenAccountIdempotentInstruction, getAssociatedTokenAddressSync } from '@solana/spl-token';
import { TSLA_X_MINT, TOKEN_2022_PROGRAM_ID, USDC_MINT } from '@shear/sdk';

const { optionMint } = client.roundPdas(round);
const options = getAssociatedTokenAddressSync(optionMint, me);
const usdc = getAssociatedTokenAddressSync(USDC_MINT, me);
const tslax = getAssociatedTokenAddressSync(TSLA_X_MINT, me, false, TOKEN_2022_PROGRAM_ID);
const accounts = {
  ...client.vaultPdas(vault),
  ...client.roundPdas(round),
  ...client.positionPdas(vault, me, round),
};

// Bidder: refund and calls, once the round has left Auction
const claimBid = [
  createAssociatedTokenAccountIdempotentInstruction(me, options, me, optionMint),
  await client.instruction('claimBid', [], { ...accounts, refund: usdc, options }),
];

// Share owner: all credited premium
const claimPremium = await client.instruction('claimPremium', [], { ...accounts, destination: usdc });

// Call holder: burn calls for TSLAx once the round is Settled
const calls = BigInt((await connection.getTokenAccountBalance(options)).value.amount);
const claimOption = await client.instruction('claimOption', [calls], { ...accounts, options, destination: tslax });

A call claim pays payout(claimed + quantity) - payout(claimed) against the whole round, so it can differ from coveredPayout for the same quantity by one atom. After a fallback settlement each call atom pays one TSLAx atom. See calls and premium.

Reading state#

fetchAccount reads at finalized commitment and returns decoded data with camelCase keys. 64-bit and wider integers arrive as decimal strings, so convert them with BigInt. The phase is an object with one key.

ts
const { data: v } = await client.fetchVault(vault);
const index = BigInt(v.roundIndex as string); // 0n before the first round

if (index > 0n) {
  const { data: r } = await client.fetchRound(client.round(vault, index));
  const phase = Object.keys(r.phase as object)[0];   // 'Auction' | 'Active' | 'Observed' | 'Settled' | 'Closed' | 'Disputed'
  const strike = BigInt(r.strike as string);         // micro-USD per whole TSLAx
  const auctionEnd = Number(r.auctionEnd as string); // Unix seconds
}

const { data: p } = await client.fetchAccount(client.position(vault, me), 'position');
const claimable = // micro-USDC
  BigInt(p.premiumCredit as string) +
  (BigInt(p.shares as string) * (BigInt(v.premiumIndex as string) - BigInt(p.premiumIndex as string)) +
    BigInt(p.premiumFraction as string)) / 10n ** 18n;

Token balances of custody, collateral, reserve, the share mint and the call mint are ordinary SPL reads on the addresses from vaultPdas and roundPdas. For many accounts at once, or for event history, use the indexer API.

Events#

decodeEvent decodes the base64 payload of one Program data: line. It does not check which program wrote the line, and a CPI can log bytes shaped like a Shear event. Decode only lines written while Shear is the active program, from successful transactions, as the indexer does.

ts
import { SHEAR_PROGRAM_ID, decodeEvent } from '@shear/sdk';

const tx = await connection.getTransaction(signature, { commitment: 'finalized', maxSupportedTransactionVersion: 0 });
const stack: string[] = [];
if (tx?.meta && tx.meta.err === null) {
  for (const line of tx.meta.logMessages ?? []) {
    const invoke = /^Program (\S+) invoke \[\d+\]$/.exec(line);
    if (invoke) { stack.push(invoke[1]!); continue; }
    if (/^Program \S+ (success|failed:.*)$/.test(line)) { stack.pop(); continue; }
    if (stack.at(-1) === SHEAR_PROGRAM_ID.toBase58() && line.startsWith('Program data: ')) {
      const event = decodeEvent(line.slice('Program data: '.length));
      if (event) console.log(event.name, event.data); // e.g. BidPlaced { round, owner, quantity, limit }
    }
  }
}

Math helpers#

Pure bigint functions that follow the program’s integer rounding. They throw RangeError for negative inputs, zero denominators and results outside u64.

FunctionRule
u64(value)Returns value if it fits in u64; throws otherwise.
mulDivFloor(a, b, d)floor(a × b / d).
mulDivCeil(a, b, d)ceil(a × b / d).
coveredPayout(amount, price, multiplier, strikeRawPrice)TSLAx atoms owed on amount call atoms. Raw price = floor(price × multiplier / 10^9); 0 if the raw price is at or below the strike, else floor(amount × (raw - strike) / raw). price is the 6-decimal Pyth mark.
depositShares(amount, assets, supply)amount for an empty vault, else floor(amount × supply / assets). Throws for a zero result or zero assets.
clearAuction(capacity, reserve, bids)Uniform-price clearing as on chain. Skips bids below reserve or with zero quantity; at most 16 bids.

clearAuction takes { quantity, limitPremium, owner } per bid, with owner as PublicKey.toBytes() for tie-breaking. It returns { clearingPremium, sold, allocations }, allocations in input order. Charges are not returned; each is mulDivCeil(allocation, clearingPremium, 100_000_000n). The SDK has no strike helper: on chain the strike is ceil(effective price × (10000 + otm_bps) / 10000).

ts
import { clearAuction, coveredPayout, MULTIPLIER_SCALE } from '@shear/sdk';

const result = clearAuction(12_050_000_000n, 1_000_000n, [
  { quantity: 8_000_000_000n, limitPremium: 4_250_000n, owner: bidderA.toBytes() },
  { quantity: 6_000_000_000n, limitPremium: 3_900_000n, owner: bidderB.toBytes() },
]);
// { clearingPremium: 3_900_000n, sold: 12_050_000_000n, allocations: [8_000_000_000n, 4_050_000_000n] }

// 10 calls, strike 262.629629 USD, close mark 280 USD, multiplier 1.0
coveredPayout(1_000_000_000n, 280_000_000n, MULTIPLIER_SCALE, 262_629_629n);
// 62_037_039n TSLAx atoms, 0.62037039 TSLAx