Skip to content
Open
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
125 changes: 125 additions & 0 deletions docs/deploy.md
Original file line number Diff line number Diff line change
@@ -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 <contract-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.
134 changes: 134 additions & 0 deletions docs/quickstart.md
Original file line number Diff line number Diff line change
@@ -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<number>;
};
```

## 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.
132 changes: 132 additions & 0 deletions docs/splits-vs-multisend.md
Original file line number Diff line number Diff line change
@@ -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.
Loading