From 76e20ce9d441ee10b3866cc1dbdc9111b68d75fa Mon Sep 17 00:00:00 2001 From: Olatope Olaleye Olajide Date: Sat, 18 Jul 2026 17:03:38 +0000 Subject: [PATCH 1/3] Add SDK quickstart doc and README link Closes #134 Add a five-minute SDK quickstart (install, read a split, create one, pay through it) and link it from the README. Include the new doc in the CI markdown-link check. --- .github/workflows/ci.yml | 2 +- README.md | 2 + docs/quickstart.md | 134 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 137 insertions(+), 1 deletion(-) create mode 100644 docs/quickstart.md diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b20a8c3..74fb99d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,7 +16,7 @@ jobs: - run: cargo clippy --all-targets -- -D warnings - run: cargo test - name: Check markdown links - run: npx -y markdown-link-check -q README.md docs/architecture.md docs/integrations.md docs/mainnet.md + run: npx -y markdown-link-check -q README.md docs/architecture.md docs/integrations.md docs/mainnet.md docs/quickstart.md - run: cargo build --release --target wasm32v1-none -p tributary-splitter contract-pedantic: diff --git a/README.md b/README.md index 556b6bd..839fc1d 100644 --- a/README.md +++ b/README.md @@ -97,6 +97,8 @@ app web dashboard (Vite + React, Freighter wallet) [docs/glossary.md](docs/glossary.md) defines core terms like split, share, controller, escrow and dust. +[docs/quickstart.md](docs/quickstart.md) is a five-minute SDK quickstart: install, read a split, create one, and pay through it. + ## Contributing Issues and pull requests are welcome. See [CONTRIBUTING.md](CONTRIBUTING.md) for how to get set up and what a good change looks like. diff --git a/docs/quickstart.md b/docs/quickstart.md new file mode 100644 index 0000000..ea83502 --- /dev/null +++ b/docs/quickstart.md @@ -0,0 +1,134 @@ +# SDK Quickstart + +This guide takes you from a clean machine to your first on-chain call with the +Tributary TypeScript SDK in about five minutes. It covers install, read, and +write (create + pay) against testnet. + +The SDK is a generated client for the [splitter contract](../README.md). It is +pre-wired to the testnet deployment, so you only need an account and a way to +sign transactions. + +## Prerequisites + +- Node.js 18 or newer +- A funded testnet Stellar account. Create one in the + [Freighter](https://freighter.app) wallet, then fund it at + [friendbot](https://lab.stellar.org/account/fund?$=network$id=testnet). +- A signing function. The snippets below use a minimal inline signer built on + `@stellar/stellar-sdk`; in a real app you would route this through a wallet + like Freighter or a backend key manager. + +## Install + +```bash +npm install tributary-sdk +``` + +Or build it from this repo checkout: + +```bash +cd sdk +npm install +npm run build +``` + +## Read-only call + +Reading a split does not require signing. Point the client at the testnet +network and call `get_split`: + +```ts +import { Client, networks } from "tributary-sdk"; + +const client = new Client({ + ...networks.testnet, + rpcUrl: "https://soroban-testnet.stellar.org", +}); + +// Splits are numbered from 0. +const { result } = await client.get_split({ id: 0n }); +console.log(result); +``` + +`result` is a `Split`: + +```ts +type Split = { + controller: string | null; + recipients: Array<{ tag: "Account"; values: [string] } | { tag: "Split"; values: [bigint] }>; + shares: Array; +}; +``` + +## Your first write: create a split + +Writes go through `signAndSend`, which simulates the transaction, then signs and +submits it. The example below builds a signer from a secret key. **Never ship a +hardcoded key** — this is for the quickstart only. + +```ts +import { Client, networks, contract, rpc } from "tributary-sdk"; +import { Keypair } from "@stellar/stellar-sdk"; + +const client = new Client({ + ...networks.testnet, + rpcUrl: "https://soroban-testnet.stellar.org", +}); + +const keypair = Keypair.fromSecret("S..."); // your testnet secret key +const publicKey = keypair.publicKey(); + +const signTransaction = async (tx: string) => { + const prepared = rpc.assembledTransactionFromXDR(tx, networks.testnet.networkPassphrase); + prepared.sign(keypair); + return prepared.toEnvelope().toXDR().toString("base64"); +}; + +// 60% / 40% split between two accounts. +const tx = await client.create_split({ + creator: publicKey, + recipients: [ + { tag: "Account", values: [publicKey] }, + { tag: "Account", values: ["G..."] }, + ], + shares: [6000, 4000], + controller: undefined, // undefined locks the split forever +}); + +const { result } = await tx.signAndSend({ signTransaction }); +console.log("created split id", result); // a bigint +``` + +`result` is the new split's id. Shares are basis points and must sum to exactly +`10_000`; `controller` is `undefined` to lock the split, or an address string to +make it editable later. + +## Pay through the split + +Pushing a payment through the split pays every recipient in one transaction. +Preview the per-recipient amounts first with `preview_payout`, then `pay`: + +```ts +// Exact per-recipient amounts for a 1000-unit payment, no funds moved. +const { result: preview } = await client.preview_payout({ id: result, amount: 1000n }); +console.log(preview); // e.g. [600n, 400n] + +const payTx = await client.pay({ + from: publicKey, + id: result, + token: "CAS3J7ZYVA3TBXEEZUWEXZYYH2F2LJGV7YQHVWPCLA7QZOKMVFSVWJQP", // a testnet asset + amount: 1000n, +}); +await payTx.signAndSend({ signTransaction }); +``` + +The payer must hold and have authorized the `token`. For a USDC-like asset on +testnet you typically need to set a trustline and grant the contract a spending +allowance first; the dashboard's Pay tab performs these steps for you. + +## Where to go next + +- [SDK README](../sdk/README.md) for the full method list and regenerating bindings. +- [Architecture](../docs/architecture.md) for storage layout, money paths, and error codes. +- [Glossary](../docs/glossary.md) for split, share, controller, escrow, and dust. +- [README](../README.md) for the two-minute web walkthrough and deployments. From ef3b566aff2969f3a37dd0206cfa64ea58e65232 Mon Sep 17 00:00:00 2001 From: Olatope Olaleye Olajide Date: Sat, 18 Jul 2026 17:32:34 +0000 Subject: [PATCH 2/3] docs: add deploy-your-own-splitter guide Walk through building the splitter wasm and deploying it to testnet with the Stellar CLI, referencing scripts/deploy.sh. Cover SDK regeneration, dashboard wiring, and troubleshooting. Link from README and add to the CI markdown-link-check job. Closes #143 --- .github/workflows/ci.yml | 2 +- README.md | 2 + docs/deploy.md | 125 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 128 insertions(+), 1 deletion(-) create mode 100644 docs/deploy.md diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 74fb99d..5f3d808 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,7 +16,7 @@ jobs: - run: cargo clippy --all-targets -- -D warnings - run: cargo test - name: Check markdown links - run: npx -y markdown-link-check -q README.md docs/architecture.md docs/integrations.md docs/mainnet.md docs/quickstart.md + run: npx -y markdown-link-check -q README.md docs/architecture.md docs/integrations.md docs/mainnet.md docs/quickstart.md docs/deploy.md - run: cargo build --release --target wasm32v1-none -p tributary-splitter contract-pedantic: diff --git a/README.md b/README.md index 839fc1d..7cd6a6a 100644 --- a/README.md +++ b/README.md @@ -99,6 +99,8 @@ app web dashboard (Vite + React, Freighter wallet) [docs/quickstart.md](docs/quickstart.md) is a five-minute SDK quickstart: install, read a split, create one, and pay through it. +[docs/deploy.md](docs/deploy.md) walks through building and deploying the contract to testnet with the Stellar CLI, then regenerating the SDK. + ## Contributing Issues and pull requests are welcome. See [CONTRIBUTING.md](CONTRIBUTING.md) for how to get set up and what a good change looks like. diff --git a/docs/deploy.md b/docs/deploy.md new file mode 100644 index 0000000..9579468 --- /dev/null +++ b/docs/deploy.md @@ -0,0 +1,125 @@ +# Deploy Your Own Splitter + +This guide walks through building the splitter contract from source and +deploying it to the Stellar testnet with the +[Stellar CLI](https://developers.stellar.org/docs/tools/cli). By the end you +will have your own contract instance, and the SDK regenerated to point at it. + +The whole flow is wrapped in [`scripts/deploy.sh`](../scripts/deploy.sh); this +page explains each step so you can adapt it. + +## Prerequisites + +- Stable Rust with the `wasm32v1-none` target. The checked-in + [`rust-toolchain.toml`](https://github.com/tributary-protocol/tributary/blob/main/rust-toolchain.toml) + selects the right toolchain automatically once you run any `cargo` command. +- The [Stellar CLI](https://developers.stellar.org/docs/tools/cli) (`stellar`), + installed and on your `PATH`. +- A funded testnet identity. The deploy script uses the identity named + `deployer` by default; create and fund one with: + + ```bash + stellar keys generate deployer --network testnet --fund + ``` + + Any identity name works — pass it as the first argument to `deploy.sh`. + +## 1. Build the wasm + +From the repo root: + +```bash +cargo build --release --target wasm32v1-none -p tributary-splitter +``` + +This produces `target/wasm32v1-none/release/tributary_splitter.wasm`. The +contract is `no_std` and uses typed errors, so the build has no host OS +dependencies. + +## 2. Deploy to testnet + +The simplest path is the script: + +```bash +./scripts/deploy.sh deployer +``` + +It builds the wasm, then deploys it under the `splitter` alias: + +```bash +stellar contract deploy \ + --wasm target/wasm32v1-none/release/tributary_splitter.wasm \ + --source deployer \ + --network testnet \ + --alias splitter +``` + +`--alias splitter` stores the resulting contract id locally, so later commands +can reference it by name instead of by the long id. After this succeeds, the +deployed contract id is in your local Stellar CLI config under that alias. + +## 3. Smoke-test it + +`scripts/demo.sh` runs a full create-and-pay cycle against whatever contract +the `splitter` alias points at, so it exercises your fresh deployment: + +```bash +./scripts/demo.sh deployer +``` + +It creates a 60/40 split between two fresh accounts, pays 1 XLM through it, then +pays 0.5 XLM via `pay_many`, and prints the resulting balances. A clean run +means your contract is live and accepting calls. + +## 4. Regenerate the SDK bindings + +The published SDK is pinned to the project's testnet deployment. To point your +local SDK at your own contract, regenerate the TypeScript bindings from your +instance: + +```bash +stellar contract bindings typescript \ + --contract-id "$(stellar contract id splitter --network testnet)" \ + --network testnet \ + --output-dir sdk --overwrite +``` + +Then rebuild the SDK so the new contract id and spec take effect: + +```bash +cd sdk +npm install +npm run build +``` + +If you maintain a fork, restore `sdk/README.md` and the package name in +`sdk/package.json` afterward, or keep your deployment as the default — that is +your call. + +## 5. (Optional) Point the dashboard at it + +The web app reads the contract id from the SDK's `networks` map. After +regenerating bindings, build the app and run it locally to drive your +deployment from the UI: + +```bash +cd app +npm install +npm run build +``` + +## Troubleshooting + +- **`target wasm32v1-none not found`** — the toolchain file should add it on + first build. If it did not, run `rustup target add wasm32v1-none`. +- **`insufficient balance` on deploy** — fund the deploying identity with + `stellar keys fund deployer --network testnet`. +- **`contract id splitter not found`** — the alias is created only on a + successful deploy. Re-run `scripts/deploy.sh` or pass `--id ` + explicitly to later `stellar contract` commands. + +## Where to go next + +- [SDK quickstart](quickstart.md) to make your first read and write calls. +- [Architecture](architecture.md) for storage layout, money paths, and error codes. +- [Glossary](glossary.md) for split, share, controller, escrow, and dust. From e936517cb1fceb36cdf6d85e30bf808aa98cb282 Mon Sep 17 00:00:00 2001 From: Olatope Olaleye Olajide Date: Sat, 18 Jul 2026 18:02:17 +0000 Subject: [PATCH 3/3] docs: add splits vs manual multi-send explainer Explains why atomic splits beat sending N separate transfers: all-or-nothing settlement, one signature/transaction, reusable and composable on-chain routing, contract-computed verifiable amounts, and inspectable routing. Includes a comparison table and guidance on when manual multi-send is still the right tool. Links the doc from the README. Closes #144 --- README.md | 2 + docs/splits-vs-multisend.md | 132 ++++++++++++++++++++++++++++++++++++ 2 files changed, 134 insertions(+) create mode 100644 docs/splits-vs-multisend.md diff --git a/README.md b/README.md index 7cd6a6a..2356d2b 100644 --- a/README.md +++ b/README.md @@ -99,6 +99,8 @@ app web dashboard (Vite + React, Freighter wallet) [docs/quickstart.md](docs/quickstart.md) is a five-minute SDK quickstart: install, read a split, create one, and pay through it. +[docs/splits-vs-multisend.md](docs/splits-vs-multisend.md) explains why atomic splits beat sending N separate transfers, and where manual multi-send still fits. + [docs/deploy.md](docs/deploy.md) walks through building and deploying the contract to testnet with the Stellar CLI, then regenerating the SDK. ## Contributing diff --git a/docs/splits-vs-multisend.md b/docs/splits-vs-multisend.md new file mode 100644 index 0000000..53461d0 --- /dev/null +++ b/docs/splits-vs-multisend.md @@ -0,0 +1,132 @@ +# Splits vs manual multi-send + +When you need to pay several parties, the obvious approach is to send each one a +transfer yourself: `pay A`, then `pay B`, then `pay C`. Tributary offers a +different primitive: register a split once, then route a single payment through +it and let the contract fan the money out. This page explains why the atomic +split is usually the better tool, and where the manual approach still makes +sense. + +## What "manual multi-send" means + +Manual multi-send is exactly what it sounds like: you, the payer, originate N +separate transfers. Each one is its own operation, each one needs its own +authorization, and each one settles on its own: + +```mermaid +flowchart LR + P[Payer] --> A[Recipient A] + P --> B[Recipient B] + P --> C[Recipient C] +``` + +You decide the amounts, you send them in some order, and the ledger records N +independent movements of money. + +## What a split does instead + +A split stores the routing table on-chain: a list of recipients and the basis +points each one gets, summing to exactly 10,000. You register it once with +`create_split`, then a single `pay` call moves the whole amount from you to every +recipient inside one transaction: + +```mermaid +flowchart LR + P[Payer] -- "pay(id, amount)" --> S[Tributary Contract] + S --> A[Recipient A] + S --> B[Recipient B] + S --> C[Recipient C] +``` + +`pay_many` extends this to several splits at once, still in one transaction and +one signature. + +## Why atomic splits win + +### All-or-nothing settlement + +The biggest difference is failure semantics. Manual multi-send is a sequence of +independent transfers. If the third one fails — a bad address, a broken +trustline, an allowance that lapsed — the first two have already moved. You are +left with a half-paid state and no clean way to reason about who got paid. + +A split pays everyone inside a single transaction. The payout for every +recipient runs as one atomic operation: either the whole split settles or none +of it does. There is no in-between state where A and B are paid but C is not. +For anything that is supposed to look like one logical payment — a payroll run, a +marketplace payout, a revenue share — atomicity is exactly the guarantee you +want. + +### One signature, one transaction + +Manual multi-send asks the payer to authorize N transfers. An atomic split is +one `pay` call, one authorization, one transaction. That is less wallet friction +for the person signing, and it is one network round-trip instead of N. With +`pay_many` you can fan out across several splits in that same single transaction. + +### The split is reusable and composable + +The routing table lives on-chain. Once a split exists, *anyone* can push a +payment through it — the split owner does not need to be present, and they do not +need to recompute the percentages each time. The same split can be reused for +every payout, so the "who gets what" decision is made once and then trusted +forever (or editable by a controller, if you set one). + +Splits also compose: a recipient can be another split. A "project" split can +feed "team" and "treasury" splits, which feed their own recipients, all settled +by a single `pay`. Doing the equivalent by hand means carefully re-deriving and +re-sending every nested amount on every payment — error-prone, and easy to get +out of sync with the intended routing. + +### Exact, verifiable amounts + +The contract computes each recipient's cut from fixed basis points and rounds +the dust to the last recipient, so the amount in always equals the amount out. +You can preview the exact per-recipient amounts with `preview_payout` before +sending anything — no arithmetic on your side, no drift between what you intended +and what landed. With manual multi-send, you are responsible for getting the +amounts to add up, every single time. + +### The routing is inspectable + +Because the split is stored, anyone can read it with `get_split` and confirm +exactly where their money will go before they pay. A manual multi-send is just a +sequence of transfers with no on-chain record tying them together as "the same +payment". For a marketplace or a DAO payout, being able to point at one split id +and say "this is the rule" is a real accountability win. + +## Where manual multi-send still fits + +A split is not free to create, and its routing is fixed (or controller-managed). +Manual multi-send is the better choice when: + +- **You pay a one-off, ad-hoc set of parties** and will never repeat the exact + routing. The fixed cost of registering a split is not worth it for a single, + throwaway payment. +- **Recipients and amounts vary every time** in ways that do not map to a stable + basis-point table. If you need fine-grained, per-payment control over exact + amounts, sending directly is simpler. +- **You must guarantee an independent outcome per party** — for example, you + want recipient A paid even if recipient B's transfer will definitely fail, and + you would rather A be settled than nothing at all. Atomicity is a feature of + splits; it is also a constraint. + +## Quick comparison + +| Aspect | Manual multi-send | Atomic split (`pay` / `pay_many`) | +| --- | --- | --- | +| Failure behavior | Partial: earlier transfers may already have settled | All-or-nothing per split | +| Authorizations | N signatures / N transactions | One signature / one transaction | +| Reuse | Recomputed every time | Registered once, reused by anyone | +| Composability | Must re-derive nested amounts by hand | Native: splits can route to other splits | +| Amount accuracy | Your responsibility to add up | Contract-computed, `preview_payout` to verify | +| Routing visibility | Scattered transfers | One readable split id | +| Best for | One-off, fully ad-hoc payments | Repeated, shared, composable payouts | + +## Bottom line + +Manual multi-send lets you hand-stitch payments together at the cost of partial +failures, repeated signing, and no shared routing. A split turns a recurring, +multi-party payment into a single atomic, reusable, inspectable rule. Reach for a +split whenever the *routing* (who gets what share) is stable and worth trusting; +keep manual sends for the genuinely one-off cases.