@fepvenancio/stela-sdk 0.2.3 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,142 +1,391 @@
1
- # stela-sdk
1
+ # @fepvenancio/stela-sdk
2
2
 
3
3
  TypeScript SDK for the **Stela** P2P lending protocol on StarkNet.
4
4
 
5
- Stela enables peer-to-peer lending through on-chain inscriptions. Borrowers post collateral and request loans; lenders sign inscriptions to fund them. The protocol manages collateral locking via token-bound locker accounts, multi-lender share accounting through ERC1155 tokens, and automated liquidation of expired positions.
5
+ Stela enables peer-to-peer lending through on-chain inscriptions. Borrowers post collateral and request loans; lenders sign inscriptions to fund them. The protocol manages collateral locking via token-bound locker accounts, multi-lender share accounting through ERC1155 tokens, and automated liquidation of expired positions. An off-chain signing model allows gasless order creation and settlement through relayer bots.
6
+
7
+ ## Stack
8
+
9
+ - **TypeScript** (ESM + CJS dual build via tsup)
10
+ - **starknet.js v6** — RPC calls, SNIP-12 typed data, Poseidon hashing
11
+ - **Vitest** for testing
6
12
 
7
13
  ## Installation
8
14
 
9
15
  ```bash
10
- pnpm add stela-sdk starknet
16
+ npm install @fepvenancio/stela-sdk starknet
11
17
  ```
12
18
 
13
19
  ```bash
14
- npm install stela-sdk starknet
20
+ pnpm add @fepvenancio/stela-sdk starknet
15
21
  ```
16
22
 
23
+ `starknet` is a peer dependency (^6.0.0).
24
+
17
25
  ## Quick Start
18
26
 
19
- ### Read an Inscription
27
+ ### Using the SDK Facade
20
28
 
21
29
  ```typescript
22
- import { STELA_ADDRESS, type StoredInscription } from 'stela-sdk'
23
- import { RpcProvider, Contract } from 'starknet'
24
- import stelaAbi from 'stela-sdk/src/abi/stela.json'
30
+ import { StelaSdk } from '@fepvenancio/stela-sdk'
31
+ import { RpcProvider, Account } from 'starknet'
25
32
 
26
33
  const provider = new RpcProvider({ nodeUrl: 'https://starknet-sepolia.public.blastapi.io' })
27
- const contract = new Contract(stelaAbi, STELA_ADDRESS.sepolia, provider)
34
+ const account = new Account(provider, address, privateKey)
35
+
36
+ const sdk = new StelaSdk({ provider, account, network: 'sepolia' })
37
+
38
+ // Read an inscription
39
+ const inscription = await sdk.inscriptions.getInscription(1n)
40
+
41
+ // Check share balance
42
+ const shares = await sdk.shares.balanceOf(myAddress, 1n)
28
43
 
29
- const inscription: StoredInscription = await contract.get_inscription(inscriptionId)
44
+ // Query the indexer API
45
+ const list = await sdk.api.listInscriptions({ status: 'open' })
30
46
  ```
31
47
 
32
- ### Create an Inscription
48
+ ### Using Individual Clients
33
49
 
34
50
  ```typescript
35
- import { STELA_ADDRESS, type InscriptionParams, ASSET_TYPE_ENUM } from 'stela-sdk'
36
- import { Account, Contract } from 'starknet'
37
- import stelaAbi from 'stela-sdk/src/abi/stela.json'
38
-
39
- const contract = new Contract(stelaAbi, STELA_ADDRESS.sepolia, account)
40
-
41
- const params: InscriptionParams = {
42
- is_borrow: true,
43
- debt_assets: [{
44
- asset_address: '0x049d36...', // ETH address
45
- asset_type: 'ERC20',
46
- value: 1000000000000000000n, // 1 ETH
47
- token_id: 0n,
48
- }],
49
- interest_assets: [{
50
- asset_address: '0x049d36...',
51
- asset_type: 'ERC20',
52
- value: 50000000000000000n, // 0.05 ETH interest
53
- token_id: 0n,
54
- }],
55
- collateral_assets: [{
56
- asset_address: '0x053c91...',
57
- asset_type: 'ERC20',
58
- value: 2000000000000000000n, // 2x collateral
59
- token_id: 0n,
60
- }],
61
- duration: 604800n, // 7 days
62
- deadline: BigInt(Math.floor(Date.now() / 1000) + 86400), // 24h to fill
63
- multi_lender: false,
64
- }
51
+ import { InscriptionClient, ShareClient, STELA_ADDRESS } from '@fepvenancio/stela-sdk'
52
+ import { RpcProvider } from 'starknet'
53
+
54
+ const provider = new RpcProvider({ nodeUrl: 'https://starknet-sepolia.public.blastapi.io' })
65
55
 
66
- const txHash = await contract.create_inscription(params)
56
+ const inscriptions = new InscriptionClient({
57
+ stelaAddress: STELA_ADDRESS.sepolia,
58
+ provider,
59
+ })
60
+
61
+ const data = await inscriptions.getInscription(1n)
67
62
  ```
68
63
 
69
- ### Sign an Inscription (Lend)
64
+ ## API Reference
65
+
66
+ ### StelaSdk
67
+
68
+ Main facade that wires together all clients.
69
+
70
+ ```typescript
71
+ const sdk = new StelaSdk({
72
+ provider, // StarkNet RPC provider
73
+ account?, // Account for write operations (optional for read-only)
74
+ network?, // 'sepolia' | 'mainnet' (default: 'sepolia')
75
+ apiBaseUrl?, // Custom indexer API URL
76
+ stelaAddress?, // Override contract address
77
+ })
78
+
79
+ sdk.inscriptions // InscriptionClient
80
+ sdk.shares // ShareClient
81
+ sdk.locker // LockerClient
82
+ sdk.api // ApiClient
83
+ ```
84
+
85
+ ---
86
+
87
+ ### InscriptionClient
88
+
89
+ On-chain reads and transaction builders for the Stela protocol contract.
90
+
91
+ #### Read Methods
92
+
93
+ | Method | Returns | Description |
94
+ |--------|---------|-------------|
95
+ | `getInscription(id)` | `StoredInscription` | Fetch raw on-chain inscription data |
96
+ | `getLocker(id)` | `string` | Get the locker TBA address for an inscription |
97
+ | `getInscriptionFee()` | `bigint` | Current protocol inscription fee |
98
+ | `convertToShares(id, percentage)` | `bigint` | Convert a fill percentage to shares |
99
+ | `getNonce(address)` | `bigint` | Get the off-chain signing nonce for an address |
100
+ | `getRelayerFee()` | `bigint` | Current relayer fee (in BPS) |
101
+ | `getTreasury()` | `string` | Treasury contract address |
102
+ | `isPaused()` | `boolean` | Whether the protocol is paused |
103
+ | `isOrderRegistered(orderHash)` | `boolean` | Check if an off-chain order is registered |
104
+ | `isOrderCancelled(orderHash)` | `boolean` | Check if an order has been cancelled |
105
+ | `getFilledBps(orderHash)` | `bigint` | Get filled basis points for a signed order |
106
+ | `getMakerMinNonce(maker)` | `string` | Get minimum valid nonce for a maker |
107
+
108
+ #### Call Builders
109
+
110
+ Return a `Call` object for use with `account.execute()`. Bundle multiple calls (including ERC20 approvals) into a single transaction.
111
+
112
+ | Method | Description |
113
+ |--------|-------------|
114
+ | `buildCreateInscription(params)` | Create an inscription on-chain |
115
+ | `buildSignInscription(id, bps)` | Sign (fund) an inscription at a given BPS |
116
+ | `buildCancelInscription(id)` | Cancel an open inscription |
117
+ | `buildRepay(id)` | Repay a filled inscription |
118
+ | `buildLiquidate(id)` | Liquidate an expired inscription |
119
+ | `buildRedeem(id, shares)` | Redeem shares for underlying assets |
120
+ | `buildPrivateRedeem(request, proof)` | Redeem shares via the privacy pool with a ZK proof |
121
+ | `buildSettle(params)` | Settle an off-chain order (used by relayer bots) |
122
+ | `buildFillSignedOrder(order, sig, fillBps)` | Fill a signed order on-chain |
123
+ | `buildCancelOrder(order)` | Cancel a specific signed order |
124
+ | `buildCancelOrdersByNonce(minNonce)` | Bulk cancel orders below a nonce |
125
+
126
+ #### Execute Methods
127
+
128
+ Convenience wrappers that call `account.execute()` directly. Accept optional `approvals` for bundling ERC20 approves.
129
+
130
+ | Method | Description |
131
+ |--------|-------------|
132
+ | `execute(calls)` | Execute arbitrary calls via the connected account |
133
+ | `createInscription(params, approvals?)` | Create inscription with optional token approvals |
134
+ | `signInscription(id, bps, approvals?)` | Fund an inscription |
135
+ | `cancelInscription(id)` | Cancel an inscription |
136
+ | `repay(id, approvals?)` | Repay a loan |
137
+ | `liquidate(id)` | Liquidate an expired loan |
138
+ | `redeem(id, shares)` | Redeem ERC1155 shares |
139
+ | `privateRedeem(request, proof)` | Redeem via privacy pool |
140
+ | `fillSignedOrder(order, sig, fillBps, approvals?)` | Fill a signed order |
141
+ | `cancelOrder(order)` | Cancel a signed order |
142
+ | `cancelOrdersByNonce(minNonce)` | Bulk cancel by nonce |
143
+
144
+ ---
145
+
146
+ ### ShareClient
147
+
148
+ Read-only client for ERC1155 share token queries. Inscription IDs are used as ERC1155 token IDs.
149
+
150
+ | Method | Returns | Description |
151
+ |--------|---------|-------------|
152
+ | `balanceOf(account, inscriptionId)` | `bigint` | Share balance for an account on an inscription |
153
+ | `balanceOfBatch(accounts, ids)` | `bigint[]` | Batch query multiple balances |
154
+ | `isApprovedForAll(owner, operator)` | `boolean` | Check operator approval |
155
+
156
+ ---
157
+
158
+ ### LockerClient
159
+
160
+ Interact with collateral locker TBA (Token Bound Account) contracts.
161
+
162
+ | Method | Returns | Description |
163
+ |--------|---------|-------------|
164
+ | `getLockerAddress(id)` | `string` | Get locker TBA address for an inscription |
165
+ | `isUnlocked(id)` | `boolean` | Check if a locker is unlocked |
166
+ | `getLockerState(id)` | `LockerState` | Full locker state (address + unlock status) |
167
+ | `getLockerBalance(id, tokenAddress)` | `bigint` | ERC20 balance held by the locker |
168
+ | `getLockerBalances(id, tokenAddresses)` | `Map<string, bigint>` | Multiple ERC20 balances |
169
+ | `buildLockerExecute(lockerAddr, calls)` | `Call` | Build a governance call through the locker |
170
+ | `executeThrough(id, call)` | `{ transaction_hash }` | Execute a single call through the locker |
171
+ | `executeThroughBatch(id, calls)` | `{ transaction_hash }` | Execute multiple calls through the locker |
172
+
173
+ ---
174
+
175
+ ### ApiClient
176
+
177
+ HTTP client for the Stela indexer API. Provides typed access to indexed data.
70
178
 
71
179
  ```typescript
72
- const MAX_BPS = 10_000n // 100%
73
- await contract.sign_inscription(inscriptionId, MAX_BPS)
180
+ const api = new ApiClient({ baseUrl: 'https://stela-dapp.xyz/api' })
74
181
  ```
75
182
 
76
- ## Locker Account
183
+ | Method | Returns | Description |
184
+ |--------|---------|-------------|
185
+ | `listInscriptions(params?)` | `ApiListResponse<InscriptionRow>` | List inscriptions with filters (status, address, page, limit) |
186
+ | `getInscription(id)` | `ApiDetailResponse<InscriptionRow>` | Get a single inscription |
187
+ | `getInscriptionEvents(id)` | `ApiListResponse<InscriptionEventRow>` | Get events for an inscription |
188
+ | `getTreasuryView(address)` | `ApiListResponse<TreasuryAsset>` | Treasury asset balances |
189
+ | `getLockers(address)` | `ApiListResponse<LockerInfo>` | Locker info for an address |
190
+ | `getShareBalances(address)` | `ApiListResponse<ShareBalance>` | Share balances for an address |
191
+
192
+ ---
77
193
 
78
- When collateral is locked, it is held in a token-bound locker account. The locker restricts asset transfers but allows governance interactions (voting, staking, etc.) through the `__execute__` interface.
194
+ ### Off-Chain Signing
195
+
196
+ Functions for creating SNIP-12 typed data for gasless order creation and settlement.
79
197
 
80
198
  ```typescript
81
- import { type LockerState } from 'stela-sdk'
82
- import lockerAbi from 'stela-sdk/src/abi/locker.json'
83
-
84
- // Get locker address for an inscription
85
- const lockerAddress = await stelaContract.get_locker(inscriptionId)
86
-
87
- // Check if locker is unlocked
88
- const lockerContract = new Contract(lockerAbi, lockerAddress, provider)
89
- const isUnlocked = await lockerContract.is_unlocked()
90
-
91
- // Execute governance calls through the locker (borrower only, non-transfer calls)
92
- await lockerContract.__execute__([{
93
- to: governanceTokenAddress,
94
- selector: selectorFromName('vote'),
95
- calldata: [proposalId, voteDirection],
96
- }])
199
+ import {
200
+ getInscriptionOrderTypedData,
201
+ getLendOfferTypedData,
202
+ hashAssets,
203
+ serializeSignature,
204
+ deserializeSignature,
205
+ } from '@fepvenancio/stela-sdk'
97
206
  ```
98
207
 
99
- ## API Types
208
+ | Function | Description |
209
+ |----------|-------------|
210
+ | `getInscriptionOrderTypedData(params)` | Build SNIP-12 typed data for a borrower's InscriptionOrder |
211
+ | `getLendOfferTypedData(params)` | Build SNIP-12 typed data for a lender's LendOffer (supports `lenderCommitment` for privacy) |
212
+ | `hashAssets(assets)` | Poseidon hash of an asset array (matches Cairo's `hash_assets()`) |
213
+ | `serializeSignature(sig)` | Convert `string[]` signature to `{ r, s }` for storage |
214
+ | `deserializeSignature(stored)` | Convert `{ r, s }` back to `string[]` |
215
+
216
+ ---
217
+
218
+ ### Privacy Utilities
100
219
 
101
- The SDK exports types matching the Stela indexer API responses for use with fetch or API clients:
220
+ Functions for the privacy pool (shielded share commitments via Poseidon hashing).
102
221
 
103
222
  ```typescript
104
- import type { InscriptionRow, ApiListResponse, AssetRow } from 'stela-sdk'
223
+ import {
224
+ createPrivateNote,
225
+ computeCommitment,
226
+ computeNullifier,
227
+ hashPair,
228
+ generateSalt,
229
+ } from '@fepvenancio/stela-sdk'
230
+ ```
231
+
232
+ | Function | Returns | Description |
233
+ |----------|---------|-------------|
234
+ | `createPrivateNote(owner, inscriptionId, shares, salt?)` | `PrivateNote` | Generate a full private note (auto-generates salt if omitted) |
235
+ | `computeCommitment(owner, inscriptionId, shares, salt)` | `string` | Compute a Poseidon commitment hash |
236
+ | `computeNullifier(commitment, ownerSecret)` | `string` | Derive a nullifier from a commitment |
237
+ | `hashPair(left, right)` | `string` | Poseidon hash of two children (Merkle tree nodes) |
238
+ | `generateSalt()` | `string` | Random felt252 salt for commitment uniqueness |
239
+
240
+ #### Types
105
241
 
106
- const response = await fetch('https://api.stela.xyz/api/inscriptions?status=open')
107
- const data: ApiListResponse<InscriptionRow> = await response.json()
242
+ ```typescript
243
+ interface PrivateNote {
244
+ owner: string
245
+ inscriptionId: bigint
246
+ shares: bigint
247
+ salt: string
248
+ commitment: string
249
+ }
108
250
 
109
- for (const inscription of data.data) {
110
- console.log(inscription.id, inscription.status, inscription.assets.length)
251
+ interface PrivateRedeemRequest {
252
+ root: string // Merkle root the proof was generated against
253
+ inscriptionId: bigint
254
+ shares: bigint
255
+ nullifier: string // Prevents double-spend
256
+ changeCommitment: string // For partial redemption ('0' if full)
257
+ recipient: string
111
258
  }
112
259
  ```
113
260
 
114
- ## API Reference
261
+ ---
262
+
263
+ ### Math Utilities
264
+
265
+ Share calculation helpers that mirror the on-chain math.
266
+
267
+ ```typescript
268
+ import {
269
+ convertToShares,
270
+ scaleByPercentage,
271
+ sharesToPercentage,
272
+ calculateFeeShares,
273
+ } from '@fepvenancio/stela-sdk'
274
+ ```
275
+
276
+ | Function | Description |
277
+ |----------|-------------|
278
+ | `convertToShares(percentage, totalSupply, currentIssuedPercentage)` | Convert fill percentage to shares |
279
+ | `scaleByPercentage(value, percentage)` | Scale a value by basis points |
280
+ | `sharesToPercentage(shares, totalSupply, currentIssuedPercentage)` | Convert shares back to percentage |
281
+ | `calculateFeeShares(shares, feeBps)` | Calculate fee portion of shares |
282
+
283
+ ---
284
+
285
+ ### Event Parsing
286
+
287
+ Parse raw StarkNet events into typed SDK event objects.
288
+
289
+ ```typescript
290
+ import { parseEvent, parseEvents, SELECTORS } from '@fepvenancio/stela-sdk'
291
+ ```
292
+
293
+ | Export | Description |
294
+ |--------|-------------|
295
+ | `SELECTORS` | Map of event name to selector hash for all protocol events |
296
+ | `parseEvent(raw)` | Parse a single raw event into a typed `StelaEvent` |
297
+ | `parseEvents(raws)` | Parse an array of raw events |
298
+
299
+ Supported event types: `InscriptionCreated`, `InscriptionSigned`, `InscriptionCancelled`, `InscriptionRepaid`, `InscriptionLiquidated`, `SharesRedeemed`, `TransferSingle`, `OrderSettled`, `OrderFilled`, `OrderCancelled`, `OrdersBulkCancelled`, `PrivateSettled`, `PrivateSharesRedeemed`.
300
+
301
+ ---
302
+
303
+ ### Token Registry
304
+
305
+ Curated token list for StarkNet (mainnet + sepolia).
306
+
307
+ ```typescript
308
+ import { TOKENS, getTokensForNetwork, findTokenByAddress } from '@fepvenancio/stela-sdk'
309
+ ```
310
+
311
+ | Export | Description |
312
+ |--------|-------------|
313
+ | `TOKENS` | Full token list (ETH, STRK, USDC, USDT, WBTC, DAI, wstETH + testnet mocks) |
314
+ | `getTokensForNetwork(network)` | Filter tokens available on a specific network |
315
+ | `findTokenByAddress(address)` | Look up token info by contract address |
316
+
317
+ ---
318
+
319
+ ### Utility Functions
320
+
321
+ ```typescript
322
+ import {
323
+ toU256, fromU256, inscriptionIdToHex, toHex,
324
+ formatAddress, normalizeAddress, addressesEqual,
325
+ parseAmount, formatTokenValue,
326
+ formatDuration, formatTimestamp,
327
+ computeStatus,
328
+ } from '@fepvenancio/stela-sdk'
329
+ ```
330
+
331
+ | Function | Description |
332
+ |----------|-------------|
333
+ | `toU256(value)` | Convert bigint to `[low, high]` string pair for Cairo u256 |
334
+ | `fromU256({ low, high })` | Convert Cairo u256 back to bigint |
335
+ | `inscriptionIdToHex(id)` | Format inscription ID as hex string |
336
+ | `toHex(value)` | Convert bigint/number to hex string |
337
+ | `formatAddress(address)` | Shorten an address for display (`0x1234...abcd`) |
338
+ | `normalizeAddress(address)` | Strip leading zeros for consistent comparison |
339
+ | `addressesEqual(a, b)` | Case-insensitive address comparison |
340
+ | `parseAmount(value, decimals)` | Parse human-readable amount to bigint |
341
+ | `formatTokenValue(value, decimals)` | Format bigint token value for display |
342
+ | `formatDuration(seconds)` | Format seconds as human-readable duration |
343
+ | `formatTimestamp(timestamp)` | Format unix timestamp as date string |
344
+ | `computeStatus(input)` | Derive inscription status from on-chain fields |
345
+
346
+ ---
115
347
 
116
348
  ### Types
117
349
 
350
+ All exported types from the SDK:
351
+
118
352
  | Type | Description |
119
353
  |------|-------------|
354
+ | `Network` | `'sepolia' \| 'mainnet'` |
355
+ | `AssetType` | `'ERC20' \| 'ERC721' \| 'ERC1155' \| 'ERC4626'` |
356
+ | `InscriptionStatus` | `'open' \| 'partial' \| 'filled' \| 'repaid' \| 'liquidated' \| 'expired' \| 'cancelled'` |
357
+ | `Call` | StarkNet call object (contractAddress, entrypoint, calldata) |
120
358
  | `Asset` | Token within an inscription (address, type, value, token_id) |
121
359
  | `InscriptionParams` | Parameters for `create_inscription` |
122
360
  | `StoredInscription` | Raw on-chain inscription data |
123
361
  | `Inscription` | Parsed inscription with computed status |
124
- | `InscriptionRow` | API response row for inscription list |
125
- | `AssetRow` | API response row for inscription assets |
362
+ | `SignedOrder` | Signed order for the matching engine |
363
+ | `InscriptionRow` | API response row for inscriptions |
364
+ | `AssetRow` | API response row for assets |
126
365
  | `ApiListResponse<T>` | Paginated list response envelope |
127
366
  | `ApiDetailResponse<T>` | Single item response envelope |
128
- | `StelaEvent` | Discriminated union of all protocol events |
129
- | `LockerState` | Locker account state (address + unlock status) |
367
+ | `TreasuryAsset` | Treasury asset balance |
368
+ | `ShareBalance` | Share balance for an account |
369
+ | `LockerInfo` | Locker information from the API |
370
+ | `LockerState` | Locker address + unlock status |
130
371
  | `LockerCall` | Call to execute through a locker |
372
+ | `StelaEvent` | Discriminated union of all protocol events |
373
+ | `PrivateNote` | Private share note for the privacy pool |
374
+ | `PrivateRedeemRequest` | Request to privately redeem shares |
375
+ | `TokenInfo` | Token metadata (symbol, name, decimals, addresses) |
376
+ | `StatusInput` | Input for `computeStatus()` |
377
+ | `StoredSignature` | Serialized signature for storage (`{ r, s }`) |
378
+
379
+ ---
131
380
 
132
381
  ### Constants
133
382
 
134
383
  | Export | Description |
135
384
  |--------|-------------|
136
- | `STELA_ADDRESS` | Contract addresses per network |
385
+ | `STELA_ADDRESS` | Contract addresses per network (`{ sepolia, mainnet }`) |
137
386
  | `resolveNetwork(raw?)` | Validate/default network string |
138
- | `MAX_BPS` | 10,000n (100% in basis points) |
139
- | `VIRTUAL_SHARE_OFFSET` | 1e16n (share calculation offset) |
387
+ | `MAX_BPS` | `10_000n` (100% in basis points) |
388
+ | `VIRTUAL_SHARE_OFFSET` | `1e16n` (share calculation offset) |
140
389
  | `ASSET_TYPE_ENUM` | AssetType to numeric enum mapping |
141
390
  | `ASSET_TYPE_NAMES` | Numeric enum to AssetType mapping |
142
391
  | `VALID_STATUSES` | Array of all valid inscription statuses |
@@ -144,12 +393,31 @@ for (const inscription of data.data) {
144
393
 
145
394
  ### ABIs
146
395
 
396
+ The package ships raw ABI JSON files in `src/abi/`:
397
+
147
398
  | File | Contents |
148
399
  |------|----------|
149
- | `src/abi/stela.json` | Full Stela protocol ABI (IStelaProtocol + ERC1155 + Ownable) |
400
+ | `src/abi/stela.json` | Full Stela protocol ABI (IStelaProtocol + ERC1155 + Ownable + Privacy) |
150
401
  | `src/abi/erc20.json` | Minimal ERC20 ABI (approve, balanceOf, allowance) |
151
402
  | `src/abi/locker.json` | Locker account ABI (__execute__, is_unlocked) |
152
403
 
404
+ ## Development
405
+
406
+ ```bash
407
+ pnpm install
408
+ pnpm build # Build with tsup (ESM + CJS)
409
+ pnpm test # Run tests with vitest
410
+ pnpm test:watch # Watch mode
411
+ pnpm lint # Type-check with tsc --noEmit
412
+ ```
413
+
414
+ ## Publishing
415
+
416
+ ```bash
417
+ pnpm build
418
+ npm publish --access public
419
+ ```
420
+
153
421
  ## License
154
422
 
155
423
  MIT