Thank you for helping improve the Conduit TypeScript SDK. This guide covers environment setup, code conventions, testing, and the PR process.
- Code of Conduct
- Getting Started
- Repository Layout
- Development Workflow
- Code Conventions
- Testing
- Commit Convention
- Pull Request Process
- Releasing
This project follows the Contributor Covenant Code of Conduct. By participating you agree to uphold it. Report unacceptable behaviour to conduct@conduit.sh.
| Tool | Version |
|---|---|
| Node.js | ≥ 20 |
| npm | ≥ 10 |
git clone https://github.com/conduit-protocol/conduit-sdk
cd conduit-sdk
npm install
# Build (TypeScript → ESM/CJS bundles)
npm run build
# Run tests
npm test
# Type check only (no emit)
npm run typecheck
# Lint
npm run lintTo run the examples against testnet you need a funded Stellar keypair:
# Generate and fund a testnet keypair with Friendbot
stellar keys generate test-key --network testnet --fund
# Export the secret key
stellar keys show test-keyThen set environment variables:
export CONDUIT_NETWORK=testnet
export CONDUIT_SECRET_KEY=S...
export CONDUIT_FACTORY=C... # from conduit-contracts deploy outputRun an example:
npx tsx examples/create-stream.tsconduit-sdk/
├── src/
│ ├── client.ts # ConduitClient — the public entry point
│ ├── streams.ts # StreamsModule — all stream operations
│ ├── factory.ts # FactoryModule — factory-level queries
│ ├── governor.ts # GovernorModule — protocol config reads
│ ├── soroban.ts # Low-level Soroban RPC helpers
│ ├── errors.ts # ConduitError class + ErrorCode enum
│ ├── utils.ts # Pure helpers: toStroops, fromStroops, etc.
│ ├── events.ts # Event subscription / polling logic
│ ├── contracts/
│ │ ├── stream-abi.ts # DripStream XDR / function spec
│ │ ├── factory-abi.ts # DripFactory XDR / function spec
│ │ └── governor-abi.ts # DripGovernor XDR / function spec
│ ├── tests/
│ │ ├── utils.test.ts # Unit tests for pure helpers
│ │ ├── errors.test.ts # ConduitError construction + fromContractError
│ │ ├── factory.test.ts # FactoryModule with mocked RPC
│ │ └── streams.test.ts # StreamsModule with mocked RPC
│ └── types/
│ └── index.ts # All exported TypeScript types
├── examples/
│ ├── create-stream.ts
│ ├── withdraw.ts
│ └── list-streams.ts
├── docs/
│ └── api.md # Full API reference
├── rollup.config.ts # Bundle config (ESM + CJS + types)
├── tsconfig.json
├── tsconfig.build.json # Stricter settings for production build
├── vitest.config.ts
└── package.json
main ← always releasable
└── feat/your-feature
- Fork and clone your fork.
- Create a branch from
main:git checkout -b feat/my-feature
- Make changes. Run
npm run buildfrequently — TypeScript errors surface quickly. - Add tests. See Testing.
- Run the full suite:
npm run typecheck npm run lint npm test npm run build - Push and open a PR.
- No
any. Useunknownand narrow explicitly, or use a concrete type. - No non-null assertions (
!) except in tests. Use optional chaining or an explicit null check. - Explicit return types on all public methods.
async/awaitthroughout — no.then()chains in new code.- Errors thrown to callers must be
ConduitErrorinstances (not rawError). Wrap unexpected errors:throw ConduitError.fromContractError(raw);
All on-chain token amounts are bigint (stroops). Never convert to number for arithmetic — Number(bigint) loses precision above 2^53.
// ✓
const half = amount / 2n;
// ✗ — loses precision
const half = Number(amount) / 2;All RPC calls go through src/soroban.ts. Do not call SorobanRpc directly from module files. This keeps the mock boundary clean for tests.
Read-only operations must use simulation only — never submit a transaction for a read:
// ✓ read-only: simulate and extract retval
const val = await this._simulateTx(tx);
// ✗ submitting a tx for a read wastes fees and sequence numbers
await this._sendAndPoll(server, tx);streams.tsorchestrates — it calls factory to resolve addresses, then calls stream contracts.factory.tsandgovernor.tsare thin wrappers — one function per contract call.soroban.tsis the only file that imports from@stellar/stellar-sdk. No other file should import stellar-sdk directly (this makes it easy to mock in tests).
All public types and classes must be re-exported from src/index.ts. Internal helpers (e.g. scValToI128) are not exported.
Tests use Vitest. All test files live in src/tests/.
Unit tests cover pure functions and modules with mocked RPC. Mock soroban.ts at the module level:
import { vi } from 'vitest';
vi.mock('../soroban.js', () => ({
buildContractCallTx: vi.fn().mockResolvedValue(/* mock tx */),
DEFAULT_RPC: { testnet: 'https://mock-rpc', mainnet: '', local: '' },
NETWORK_PASSPHRASE: { testnet: 'Test SDF Network ; September 2015', mainnet: '', local: '' },
}));Do not make real network calls in unit tests.
| Category | Requirement |
|---|---|
Pure utils (utils.ts) |
Every exported function, including edge cases (zero, max values, fractional stroops) |
| Error handling | Every ErrorCode value; fromContractError with malformed input |
| Module methods | Happy path + each thrown ConduitError code |
# All tests
npm test
# Watch mode
npm run test:watch
# Single file
npx vitest run src/tests/streams.test.ts
# With coverage
npx vitest run --coverageWe follow Conventional Commits:
<type>(<scope>): <short description>
Types: feat, fix, test, refactor, docs, chore, perf
Scopes: streams, factory, governor, soroban, utils, errors, events, types, build, deps, docs
Examples:
feat(streams): implement withdraw() with optional amount default
fix(utils): handle zero deposit in toStroops without division error
test(streams): add unit test for clawback simulation parsing
docs(api): document subscribe() pollInterval option
chore(deps): bump @stellar/stellar-sdk to 12.1.0
perf(factory): cache streamAddress resolution per session
feat/<issue-number>-short-slug # new feature
fix/<issue-number>-short-slug # bug fix
test/<issue-number>-short-slug # tests only
docs/<issue-number>-short-slug # docs only
refactor/<issue-number>-short-slug # internal refactor
Examples: fix/2-calculate-rate-zero-guard, feat/4-force-cancel-method
Every PR must be structured as exactly 5 logical commits (minimum). Commits must be in this order:
| # | Commit type | What it contains |
|---|---|---|
| 1 | test(<scope>): add failing test for <issue> |
Unit tests that reproduce the bug or describe the new behaviour — expected to fail on main |
| 2 | fix(<scope>) or feat(<scope>): minimal implementation |
The smallest code change that makes commit 1's tests green |
| 3 | test(<scope>): edge cases and error paths |
Tests for boundary values, each ErrorCode thrown, and related paths |
| 4 | docs(<scope>): update api.md and inline JSDoc |
JSDoc on new/changed public methods, docs/api.md entry, CHANGELOG.md entry |
| 5 | chore(<scope>): typecheck + lint pass |
Any TypeScript or ESLint fixes surfaced by the change; no functional changes |
Rules:
- Each commit must be independently
git cherry-pick-able — no inter-commit dependencies except the explicit test→fix ordering. - Commit messages must include a body explaining why. Subjects alone are not acceptable for code commits.
- Reference the issue in the fix/feat commit body:
Closes #2. - No squashing before review. Reviewers read the diff per-commit.
test(utils): add failing test — calculateRate returns 0n for small deposit
fix(utils): throw ConduitError when rate calculation truncates to 0n
test(utils): add edge cases — negative duration, exactly-1 stroop result
docs(utils): add JSDoc warning about BigInt truncation in calculateRate
chore(utils): fix lint warning in utils.ts after calculateRate change
- Branch name follows the naming convention above
- PR title:
fix(utils): guard calculateRate against 0n result (#2) - PR description has
Closes #<n>orFixes #<n> - Exactly 5 commits (minimum), each with an explanatory body
-
npm run typecheck— no errors -
npm run lint— no warnings -
npm test— all tests pass -
npm run build— build succeeds, no significant bundle size increase - New public API documented in
docs/api.md -
CHANGELOG.mdentry under[Unreleased]
- Mandatory owner review: Every PR requires approval from @jaydbrown before merging. No exceptions.
- PRs that change the public API (new exports, changed signatures, removed types) additionally require 1 further maintainer approval (2 approvals total) plus a
CHANGELOG.mdentry. - CI must be green (typecheck, lint, tests, build).
- Commit 1 is a test that fails on
mainbefore the fix - Commit 2 fix is minimal — no bundled unrelated changes
- No
anytypes introduced - No non-null assertions (
!) in production code (only in tests with a comment) - All new
ErrorCodevalues are tested -
bigintused for all on-chain amounts — noNumber()conversion on large values - Public API changes are documented in
docs/api.mdandCHANGELOG.md - 5-commit structure is clean
If your change modifies a public function signature, removes an export, or changes a type in a backwards-incompatible way:
- Mark the PR with the
breaking-changelabel. - Add a
BREAKING CHANGE:footer to the relevant commit message. - Increment the minor version in
package.json(we are pre-1.0; breaking changes are minor bumps).
Releases are managed by maintainers. The process:
- Update
CHANGELOG.md— move[Unreleased]items to the new version section. - Bump
package.jsonversion. - Tag:
git tag v0.x.y. - CI publishes to npm automatically on tag push.
Contributors do not need to manage releases.
By contributing you agree that your contributions will be licensed under the MIT License.