Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .gitbook.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
root: ./docs

structure:
readme: README.md
summary: SUMMARY.md
44 changes: 44 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# What is Charter

> Charter is a treasury operations layer for Stellar-based organizations.
> Instead of a bare multisig wallet, funds are held under a policy contract
> with defined budget categories, spend limits, and approval thresholds.
> Contributors submit disbursement requests against a category; designated
> approvers sign off within the policy's rules; once the threshold is met,
> funds release automatically. Every category, request, and disbursement is
> publicly readable on-chain.
>
> Charter is two things: a `treasury` contract, one instance per
> organization, that holds the actual policy and funds; and a `factory`
> contract that deploys new treasury instances and keeps a public registry
> of every organization using Charter.

## Two contracts, one system

An organization's treasury and the factory that created it do different jobs.

The **treasury** is where an organization's money and rules live. One
treasury belongs to one organization. It holds a single token, tracks that
organization's budget categories and their caps, keeps the approver list and
the approval threshold, and records every disbursement request and its
approvals. Reads and writes for day-to-day operations go straight to the
treasury.

The **factory** deploys treasuries and keeps a registry of them. Every
treasury is deployed from the same reviewed contract code, so an outside
observer can confirm that every organization on Charter runs identical rules.
The factory is the only part of the system involved in creating a treasury;
after that, an organization interacts with its own treasury directly.

## Who this documentation is for

- **Organization admins** set up a treasury, define budget categories, and
manage the approver set. Start with [Getting Started](for-organization-admins/getting-started.md).
- **Approvers and requesters** submit disbursement requests and sign off on
them. Start with [Submitting a Request](for-approvers-and-requesters/submitting-a-request.md).
- **Developers and reviewers** evaluating the code will find the contract
reference under [Smart Contracts](smart-contracts/overview.md) and
integration details under the [Developer Guide](developer-guide/local-setup.md).

Charter runs on Stellar testnet and has not been audited. It is experimental
software; do not use it to custody real value.
41 changes: 41 additions & 0 deletions docs/SUMMARY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Table of contents

## Introduction

* [What is Charter](README.md)
* [The Problem](introduction/the-problem.md)
* [How It Works](introduction/how-it-works.md)

## Protocol

* [Request Lifecycle](protocol/request-lifecycle.md)
* [Category Mechanics](protocol/category-mechanics.md)
* [Approval Threshold Model](protocol/approval-threshold-model.md)

## Smart Contracts

* [Overview](smart-contracts/overview.md)
* [treasury](smart-contracts/treasury.md)
* [factory](smart-contracts/factory.md)

## For Organization Admins

* [Getting Started](for-organization-admins/getting-started.md)
* [Setting Up Categories and Approvers](for-organization-admins/setting-up-categories-and-approvers.md)
* [Reading the Public Ledger](for-organization-admins/reading-the-public-ledger.md)

## For Approvers and Requesters

* [Submitting a Request](for-approvers-and-requesters/submitting-a-request.md)
* [Approving, Rejecting, and Cancelling](for-approvers-and-requesters/approving-rejecting-and-cancelling.md)

## Developer Guide

* [Local Setup](developer-guide/local-setup.md)
* [Environment Variables](developer-guide/environment-variables.md)
* [SDK Reference](developer-guide/sdk-reference.md)
* [API Reference](developer-guide/api-reference.md)

## Contributing

* [How to Contribute](contributing/how-to-contribute.md)
87 changes: 87 additions & 0 deletions docs/contributing/how-to-contribute.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# How to Contribute

Contributions are welcome. Charter is two repositories — the application layer
and the contracts — and this page covers how to work in either. The conventions
here restate what each repository's `README` and `CONTRIBUTING` enforce; where
this page and a repository differ, the repository is the source of truth.

## The two repositories

- **[`Ch-rter/app`](https://github.com/Ch-rter/app)** — the SDK, web app, and
indexer. TypeScript and Go.
- **[`Ch-rter/contract`](https://github.com/Ch-rter/contract)** — the treasury
and factory Soroban contracts. Rust.

## Finding something to work on

Start with the open issues labeled `good first issue`:

- [`app` good first issues](https://github.com/Ch-rter/app/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22)
- [`contract` good first issues](https://github.com/Ch-rter/contract/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22)

If you want to work on something not yet filed, open an issue first so the
approach can be agreed before you write code.

## Branch naming

Branch off `main` with a type prefix that matches the change:

```
feat/<short-desc>
fix/<short-desc>
docs/<short-desc>
chore/<short-desc>
```

## Commit format

Use [Conventional Commits](https://www.conventionalcommits.org/) with a scope,
for example:

```
feat(sdk): add batch approval helper
fix(indexer): decode contract events positionally
docs(web): document the read-only source account
```

## Pull requests

`main` is protected. A pull request must have an approving review and its status
checks must pass before it can merge; stale approvals are dismissed when new
commits land, and open review conversations must be resolved. The checks that
run on every push and PR:

- **Web (lint · typecheck · build)** — `npm ci`, `npm run lint`,
`npm run typecheck`, `npm run build:web`.
- **Indexer (vet · build)** — `go vet ./...` and `go build ./...` in `indexer/`.

Run those locally before you push and you will not be surprised by CI.

### Checklist before opening a PR

- [ ] `npm run lint` and `npm run typecheck` pass.
- [ ] `npm run build:web` succeeds (for web or SDK changes).
- [ ] `go vet ./...` is clean (for indexer changes).
- [ ] No `any` types introduced in TypeScript.
- [ ] Writes still go through `packages/sdk`; reads still come from the indexer.

That last point is the one architectural rule worth repeating: the web app never
calls a state-changing contract method directly and never reads display data
from Soroban RPC. Writes go through the SDK; reads come from the indexer. A
change that blurs those two paths will be asked to change, however well it works.

## Working on the contracts

Contract changes live in `Ch-rter/contract` and carry an extra responsibility:
the application layer documents contract behavior — function signatures, error
discriminants, events — from the contract source. If you change a function
signature, an error enum, or an event's shape, the [Smart
Contracts](../smart-contracts/overview.md) pages and the SDK's mirrored types
need to change with it. Note the change in your PR description so the
application-layer docs and `packages/sdk/src/types.ts` are updated to match.

## Reporting a security issue

Do not open a public issue for a vulnerability. Each repository's
`SECURITY.md` explains how to report one privately. See
[`app`'s SECURITY.md](https://github.com/Ch-rter/app/blob/main/SECURITY.md).
186 changes: 186 additions & 0 deletions docs/developer-guide/api-reference.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
# API Reference

The indexer serves a small, read-only REST API over the read models it folds
from on-chain events. Every response on this page is a real capture from the
live testnet indexer at:

```
https://charter-indexer.onrender.com
```

Every endpoint is `GET`, needs no authentication, and returns JSON. Amounts are
decimal **strings** in the token's smallest unit, never numbers — the reference
token has 7 decimals, so `"314159265"` means `31.4159265` tokens. Format them
with a bigint-safe helper; never parse them to a float.

> The live indexer runs on a free tier that spins down when idle. The first
> request after a quiet period can return `503` with a `Retry-After` header
> while it cold-starts; retry after a few seconds and it comes up.

## CORS

The API sets `Access-Control-Allow-Origin: *`, allows methods `GET, OPTIONS`,
and allows the `Content-Type` header. A preflight `OPTIONS` request returns
`204`. A browser app on any origin can read it directly.

## `GET /health`

Liveness plus database reachability.

```json
{"status":"ok"}
```

## `GET /orgs`

Every indexed organization, newest first. The list is wrapped in an `orgs` key.

```json
{
"orgs": [
{
"id": 1,
"name": "TestToken Org",
"treasuryAddress": "CBJQ6CZ3A6VRA3UJXRI3S6WXK3T5UH6EMHCA7TBGEPKFB6O2RMBJPI3P",
"adminAddress": "GDDWFYWXCSBI6RNS5TV2ZZSBYY35MDKHR2424O7RVL6LDC4DUTBTVR2Z",
"createdLedger": 4064653
},
{
"id": 0,
"name": "Charter Test Org 2",
"treasuryAddress": "CA5Y353OYIQLTVSQV77CJ5NEBL2UUAP3GE3NPYZOG2WCJZH2BRJ6BK2D",
"adminAddress": "GDN3D7XLV54KQ2QML6H3ZQ2OLUFQP7LAMDZWV7TXCHPK73GLHWZ7HDUK",
"createdLedger": 4063779
}
]
}
```

## `GET /orgs/{treasury}`

One organization by its treasury address. Unlike the list endpoints, this
returns the object **bare** — no wrapper key.

```
GET /orgs/CBJQ6CZ3A6VRA3UJXRI3S6WXK3T5UH6EMHCA7TBGEPKFB6O2RMBJPI3P
```

```json
{
"id": 1,
"name": "TestToken Org",
"treasuryAddress": "CBJQ6CZ3A6VRA3UJXRI3S6WXK3T5UH6EMHCA7TBGEPKFB6O2RMBJPI3P",
"adminAddress": "GDDWFYWXCSBI6RNS5TV2ZZSBYY35MDKHR2424O7RVL6LDC4DUTBTVR2Z",
"createdLedger": 4064653
}
```

A treasury address that is not indexed returns `404`.

## `GET /orgs/{treasury}/categories`

That treasury's budget categories, in category-id order, wrapped in a
`categories` key.

```json
{
"categories": [
{
"categoryId": 1,
"name": "Payroll",
"cap": "900000000",
"spent": "314159265",
"active": true
}
]
}
```

`cap` and `spent` are decimal strings. Here the Payroll category has a cap of
`900000000` (90 tokens) and has spent `314159265` (31.4159265 tokens), leaving
`585840735` (58.5840735 tokens) of room.

## `GET /orgs/{treasury}/requests`

That treasury's disbursement requests, newest first, wrapped in a `requests`
key. An optional `?status=` filter narrows the list.

```json
{
"requests": [
{
"requestId": 1,
"categoryId": 1,
"recipient": "GBBASI3ODOGYXGCMGBUNFYHY6E5LRVEW3PT5PPALIJEI63UOSBWK7QS5",
"amount": "314159265",
"memo": "",
"requester": "",
"status": "Executed",
"createdLedger": 4064715,
"approvals": ["GBBASI3ODOGYXGCMGBUNFYHY6E5LRVEW3PT5PPALIJEI63UOSBWK7QS5"]
}
]
}
```

### The `status` filter is case-sensitive

It accepts exactly `Pending`, `Executed`, `Rejected`, or `Cancelled` —
capitalized. A lowercase value like `?status=executed` is rejected:

```
GET /orgs/{treasury}/requests?status=executed
→ 400
{"error":"invalid status filter"}
```

## `GET /orgs/{treasury}/requests/{id}`

One request by its id, returned **bare** (no wrapper key).

```
GET /orgs/CBJQ6CZ3A6VRA3UJXRI3S6WXK3T5UH6EMHCA7TBGEPKFB6O2RMBJPI3P/requests/1
```

```json
{
"requestId": 1,
"categoryId": 1,
"recipient": "GBBASI3ODOGYXGCMGBUNFYHY6E5LRVEW3PT5PPALIJEI63UOSBWK7QS5",
"amount": "314159265",
"memo": "",
"requester": "",
"status": "Executed",
"createdLedger": 4064715,
"approvals": ["GBBASI3ODOGYXGCMGBUNFYHY6E5LRVEW3PT5PPALIJEI63UOSBWK7QS5"]
}
```

A request id that does not exist returns `404`.

## Two things this real response tells you

The captured request above is genuine testnet data, and it shows two properties
of the read model you need to account for when you build against this API. Both
come from the same cause: the indexer reconstructs requests from the events the
contract emits, and those events do not carry every field.

**1. An executed request shows one fewer approval than the number who signed.**
This request executed, yet `approvals` lists a single address. The approval that
meets the threshold executes the request in the same step, and the contract
emits a `RequestExecuted` event for it rather than a `RequestApproved` event.
The indexer builds `approvals` from `RequestApproved` events only, so the final,
decisive approval is not counted. Read an executed request as: the listed
approver(s), plus whoever's approval triggered execution.

**2. `requester` and `memo` can be empty even when they were set on-chain.** In
this capture both are `""`. The `RequestSubmitted` event carries only
`category_id`, `recipient`, and `amount` as data — not the requester address or
the memo. The indexer has no event field to populate those two from, so they
come back empty. If you need the requester or memo authoritatively, read the
request straight from the contract with `treasury.getRequest` (see the [SDK
Reference](sdk-reference.md)), whose `Request` type carries both.

Neither of these is a bug in the API — they are consequences of building a read
model from an event stream. They are documented here so you design around them
rather than trusting a field the event never carried.
Loading
Loading