@kehtus/proportion-sdk
v9.0.0
Published
TypeScript SDK for Proportion Protocol on Hyperliquid
Readme
Proportion SDK
TypeScript modules for Proportion contracts on HyperEVM, an optional Envio GraphQL indexer, and separate clients for the two challenge services: cs-submitter (gasless relay, offer catalogue) and cs-paper (exam trading). The package uses ESM and requires Node.js 18 or later when used in Node.
Install
npm install @kehtus/proportion-sdk viemviem is a peer dependency. Use the same installed copy for SDK types and application wallet/public clients.
Configure the SDK
Every contract address is a pin of addresses; the SDK reads no environment variables and discovers no address. Pin the contracts the application uses: MAIN_VAULT, FACTORY, FORWARDER, USDC (MainVault's token), CHALLENGE_FACTORY, CHALLENGE_VAULT, FINANCE_MARKET.
import { ProportionSDK } from '@kehtus/proportion-sdk';
const sdk = new ProportionSDK({
rpcUrl: config.rpcUrl,
graphqlUrl: config.graphqlUrl,
addresses: { MAIN_VAULT: config.mainVault, USDC: config.usdc, CHALLENGE_VAULT: config.challengeVault, CHALLENGE_FACTORY: config.challengeFactory },
});
try {
const stats = await sdk.vault.getStats();
const ledger = await sdk.getPaperLedger(); // ChallengeVault.ledger(), checked against the ledger's VAULT()
} finally {
sdk.disconnect();
}- A malformed pin throws in the constructor. A module whose pin is missing rejects on use with "X is not configured".
getAddresses()returns a copy of the pins. - PaperLedger is never pinned:
sdk.getPaperLedger()takes it from the ChallengeVault. The challenges' USDC issdk.challengeVault.getUsdc(). - Without
graphqlUrl,sdk.indexeris absent; check it once at start-up.graphqlWsUrlis derived fromgraphqlUrlwhen omitted. Calldisconnect()to close indexer subscriptions. rpcUrldefaults to the public HyperEVM RPC; an injectedpublicClienttakes precedence. PasswalletClientfor signing and contract writes.
Challenges and the paper exam
A backer signs an offer (no gas) and publishes it to the cs-submitter catalogue. A trader buys a challenge from it; the exam is a paper account in PaperLedger under the same challengeId, traded with a session key and relayed by cs-paper.
- Offers:
signOffer,hashOffer,allocationGrid;challengeFactory.quoteprices one before checkout;ChallengeApiClient.listOffers/publishOfferis the catalogue. - Purchase:
await getPurchaseSessionKey(store, scope, previousAuthorities)reads the pending purchase key or creates and saves a fresh EOA before checkout (SessionKeyStore,MemorySessionKeyStorefor tests). The required history contains all keys the application knows became authority; keep it durable and current, recording activation on a successful receipt before another checkout or scope handoff. The helper rejects an activated key even while it remains in purchase scope, preserving the stored key for recovery. Buy directly or gasless (forwarder.signChallengePurchaseWithPermit→ChallengeApiClient.relayPurchaseWithPermit). Repeating an unresolved scope keeps the key, and every call checks the authority history. The application serializes these calls with checkout and receipt finalization, within a tab and across tabs/processes: two concurrent first calls would each save a key. AfterparsePurchaseReceipt, put the key under the challengeId scope, then delete the purchase scope. Delete it without moving the key only after explicitly confirming that no attempt can still buy with it; a timeout or pending response does not establish failure. A subsequent purchase gets a fresh EOA. Rotations usecreateSessionKey(); a former authority must never be reused. - Trading: build a trade (
marketOrder,limitOrder,stopLoss,cancel, …), sign it with the session key (signTrade) and send it withPaperRelayerClient.submitIntent.subscribestreams the account's state, intents and fills (frames sent while the socket was down are not replayed, so re-read the orders on the first state frame of a connection, not on every state frame);getAccountis the latest state for a key rotation (forwarder.signChallengeVaultCallwith actionsetSessionKey, relayed byChallengeApiClient.relaySetSessionKey) and for self-pay. When the relayer does not sponsor (503 sponsorship_unavailable), the trader can send the batch item (packItem) withPaperLedgerModule.selfExecute, which sets the relayer's gas bound: a wallet's estimate may fall short on the first trade of a UTC day, which also reads the oracle of every other position, and the gas would be spent without a trade. Exits (close-all and reduce-only) are sponsored in every lane. - Key rotation: both an intake
422 state_mismatchand an accepted setSessionKey forward whose status becomesrevertedmean stale state. ReadGET /paper/accounts/{id}and re-sign with the same new key that has not yet become authority. Keep the active key until activation is confirmed. Apendingforward (including gas deferral or send failure) keeps its requestId: pollgetForwarderStatus; purchase, withdraw and promote are never retried automatically. - Nonces and cancels: sign new orders from
nextNonceup (getAccount, every intake answer and every WS state frame carry it), in any order; intake takes nonces below nextNonce + 64 (else422 nonce_too_far). A nonce is dead below the state'snonceBase, the cancel-all floor, at 2^64 − 1, or oncePaperLedgerModule.nonceUsedis true.cancelin moderestis a soft cancel: cs-paper takes the order off without a transaction and answers 200 with it, while its signature stays valid on chain until its deadline. In modenowthe cancel goes on chain;cancelAllBelow(now only) raises the floor past its nonce.409 target_in_flightwhile the order is sent: retry after the outcome. - Stream: the first state frame of each connection is the account snapshot, including a lower revision after a proven resync: state deduplication restarts from it, and a fill at or below it counts as delivered, while a fill not yet seen still passes below a snapshot that did not roll the cursor back. Later frames apply at once: a state needs a newer revision, or at the same revision a later stage, another holder, another synced or another nextNonce; every intent and lane frame is delivered. Callback failures are isolated. Reconnect waits grow from 1 s to 30 s, each multiplied by a factor in [0.5, 1.5], and return to 1 s on that first state frame. Intent
revisionis its executing StateChanged revision, otherwise null (GET, WS, orders and intake responses). Intentreason(GET, WS, orders) is the relayer's cause of the outcome. The relayer's cancel (key_changed, account_closed, position_gone, reduce_only, soft_cancel) does not spend the nonce: the signature stays executable on chain until its deadline unless the ledger refuses it for another reason, such as SIGNATURE after key_changed, INACTIVE once the verdict is PASSED or FAILED (a forfeit, reject or refund may leave it ACTIVE) or POSITION_REF while its market has no position opened by its ref. - Truth:
PaperLedgerModulereads the chain (verdict,check,equityOf,simulate,nonceUsed);readQuotesreads the prices it trades at. The helpers ofpaper/math.tsonly estimate for the UI.challengeStatusturns stage, verdict and chain time into one status. - History: the indexer keeps challenges, paper accounts, trades, days and books.
latestState(a, b)picks the newer of cs-paper's and the indexer's view of an account.
Service clients
Construct them yourself; ProportionSDK holds neither.
import { ChallengeApiClient, PaperRelayerClient, PaperApiError } from '@kehtus/proportion-sdk';
const submitter = new ChallengeApiClient(config.submitterUrl);
const paper = new PaperRelayerClient(config.paperUrl);
try {
const accepted = await paper.submitIntent(signedTrade, 'now');
} catch (error) {
if (error instanceof PaperApiError && error.code === 'sponsorship_unavailable') {
// The relayer does not pay now: the trader may send the batch item with ledger.selfExecute,
// which sets the relayer's gas bound rather than a wallet estimate.
}
throw error;
}ChallengeApiClientrelays already-signedForwardRequests (purchase, setSessionKey, withdraw, forfeit, promote, funded actions), reads relay status and serves the offer catalogue.ChallengeApiHttpErroris an HTTP refusal;bodykeeps its extra fields.PaperRelayerClientspeaks cs-paper's/paper/*API. Writes are authorized by the trade signature; orders, release and the hedge report needpaperAuthHeader(a personal_sign valid up to 24 h).PaperApiErrorcarries the service code and the whole answer inbody.- Network errors pass through unchanged. An error alone does not prove whether a request was accepted: keep request IDs and transaction hashes, read the status before signing again. Purchases, withdrawals and promotions are never retried automatically.
FinanceMarket
Pin addresses.FINANCE_MARKET; it cannot be discovered from MainVault. sdk.financeMarket works through RPC without GraphQL, also with that pin alone.
Use getFinanceFillQuote(id) for { fee, remaining, excess, minimumTraderPay }. Pass the accepted remaining as the explicit cap to fillFinanceRequest(id, cap); do not replace it silently if the price changes before signing. For a new listing, calculateActivationEconomics(size, drawdownBps) reads the exact reserve and premium from the market's own MainVault. Trader escrow must cover the premium.
getReserveContribution(account) reads the contribution snapshot. getClaimableReserveRefund(recipient) and claimReserveRefund(recipient) handle the market's separate refund balance. Only TARGET returns the trader's share of surviving reserve; other close reasons credit the entire remainder to the builder. The premium is never refundable.
Referrals and paying actions
Every paying action carries the referrer the user was shown: challengeFactory.buyChallenge, buyChallengeWithPermit, vault.deposit, vault.activateFunded and the matching forwarder.sign* helpers take a required expectedReferrer. There is no optional argument and no implicit zero; an explicit zero address means "no referrer".
const shown = await sdk.vault.resolveReferrer(user, candidate); // review this and show it to the user
await sdk.vault.deposit(amount, shown); // pass the same frozen value when the user signsIf the referrer in force has changed by the time the call executes, it reverts with ReferralChanged. Review the referrer again before requesting a new signature. A purchase credits the referrer a fixed 5% of the challenge fee (referralCredited6 in the indexer); without a referrer the share goes to the backer's book.
Amounts and returned values
Contract monetary amounts use bigint; USDC amounts use six decimals, perp prices the raw scale (fromRawPrice prints one in USD). Keep exact amounts as integers or decimal strings: viem's parseUnits('123.456789', 6) produces 123456789n, and formatUnits(amount, 6) formats it without a JavaScript number. GraphQL monetary fields are decimal integer strings; cs-paper's decimal strings arrive as bigint.
Contract write methods return a transaction hash. Await and inspect its receipt before treating the operation as complete; for PaperLedger, a refused trade still lands in a successful transaction (a Rejected log), so read the account state, not the receipt status.
