Skip to content

Commit 817d31a

Browse files
author
Stack-Rocks
committed
Initial commit: StackPay stackpay-contracts scaffold
0 parents  commit 817d31a

7 files changed

Lines changed: 311 additions & 0 deletions

File tree

.github/workflows/ci.yml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
name: CI
2+
on:
3+
push:
4+
branches: [main]
5+
pull_request:
6+
jobs:
7+
test:
8+
runs-on: ubuntu-latest
9+
steps:
10+
- uses: actions/checkout@v4
11+
- uses: dtolnay/rust-toolchain@stable
12+
with:
13+
targets: wasm32-unknown-unknown
14+
- uses: stellar/stellar-cli@latest
15+
- run: make build
16+
- run: make test
17+
- run: make lint

Cargo.toml

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
[package]
2+
name = "stackpay-contracts"
3+
version = "0.1.0"
4+
edition = "2021"
5+
description = "Soroban smart contracts for StackPay — payment requests & settlement on Stellar."
6+
license = "MIT"
7+
8+
[lib]
9+
crate-type = ["cdylib"]
10+
doctest = false
11+
12+
[dependencies]
13+
soroban-sdk = { version = "22.0.0", features = ["testutils"] }
14+
15+
[dev-dependencies]
16+
soroban-sdk = { version = "22.0.0", features = ["testutils"] }
17+
18+
[profile.release]
19+
opt-level = 3
20+
debug = 0
21+
strip = "symbols"
22+
lto = true
23+
codegen-units = 1

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2026 Stack-Rocks
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

Makefile

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# StackPay — contracts
2+
build:
3+
cargo build --target wasm32-unknown-unknown --release
4+
5+
optimize: build
6+
stellar contract optimize \
7+
--wasm target/wasm32-unknown-unknown/release/stackpay_contracts.wasm \
8+
--wasm-out target/stackpay_contracts.optimized.wasm
9+
10+
test:
11+
cargo test
12+
13+
lint:
14+
cargo fmt --all -- --check
15+
cargo clippy --all-targets -- -D warnings
16+
17+
fmt:
18+
cargo fmt --all
19+
20+
.PHONY: build optimize test lint fmt

README.md

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
# stackpay-contracts 🪨
2+
3+
> Soroban smart contracts that power **StackPay** — on-chain **payment requests**, escrowed settlement, and payment proofs on [Stellar](https://stellar.org).
4+
5+
[![CI](https://github.com/Stack-Rocks/stackpay-contracts/actions/workflows/ci.yml/badge.svg)](https://github.com/Stack-Rocks/stackpay-contracts/actions)
6+
![Rust](https://img.shields.io/badge/lang-Rust-orange)
7+
![Soroban](https://img.shields.io/badge/Soroban-contracts-blue)
8+
![License: MIT](https://img.shields.io/badge/license-MIT-green)
9+
10+
This is the **on-chain layer** of StackPay, written in **Rust** and compiled to WebAssembly for **Soroban**.
11+
12+
---
13+
14+
## Table of contents
15+
- [Why StackPay (and why Stellar needs it)](#why-stackpay)
16+
- [Architecture](#architecture)
17+
- [Contract: PaymentRequest](#contract-paymentrequest)
18+
- [Core concepts](#core-concepts)
19+
- [Getting started](#getting-started)
20+
- [Building](#building)
21+
- [Testing](#testing)
22+
- [Local network](#local-network)
23+
- [Deployment](#deployment)
24+
- [Security](#security)
25+
- [Contributing](#contributing)
26+
- [License](#license)
27+
28+
---
29+
30+
## Why StackPay
31+
32+
Stellar is one of the best networks for moving money — but the ecosystem has no simple, standard way to **request** a payment and prove it settled. Sending is easy; *asking* someone to pay you (an invoice, a donation, a subscription, a split) still means copy-pasting addresses and hoping.
33+
34+
StackPay adds that missing primitive:
35+
- A **payment request** is a tiny on-chain record: who's owed, what asset, how much, and a memo.
36+
- The payer pays; the contract settles and records a **payment proof**.
37+
- The requester shares one link; the payer pays from any Stellar wallet.
38+
39+
Use cases: freelancer invoices, creator tips, DAO bounty payouts, event tickets, donation jars — all natively on Stellar.
40+
41+
## Architecture
42+
43+
```
44+
requester ──create_request──▶ PaymentRequest (Soroban)
45+
│ emits RequestCreated
46+
47+
payer ─────────pay──────────▶ contract pulls asset (token transfer)
48+
│ emits RequestPaid (payment proof)
49+
50+
stackpay-backend (indexes events)
51+
52+
53+
stackpay-frontend (dApp: links, status)
54+
```
55+
56+
- **PaymentRequest** holds requests and performs the asset transfer via the Stellar Asset Contract (SAC) token client.
57+
- Events are indexed off-chain by [`stackpay-backend`](https://github.com/Stack-Rocks/stackpay-backend) and surfaced in the dApp.
58+
59+
## Contract: PaymentRequest
60+
61+
| Function | Description |
62+
| --- | --- |
63+
| `create_request(payee, asset, amount, memo, expires_in)` | Create a new request; returns `request_id`. |
64+
| `pay(request_id)` | Payer transfers `amount` of `asset` to `payee`; marks `Paid`, emits `RequestPaid`. |
65+
| `cancel(request_id)` | Requester cancels an unpaid, unexpired request. |
66+
| `get_request(request_id)` | View: full request state. |
67+
| `status(request_id)` | View: `Open` / `Paid` / `Cancelled` / `Expired`. |
68+
69+
All amounts use Stroops (7 decimals) for XLM and the asset's own decimals. `asset` is the Stellar Asset Contract address for the asset being requested.
70+
71+
## Core concepts
72+
73+
- **Request**`{ id, payee, asset, amount, memo, status, created_at, expires_at }`.
74+
- **Payment proof** — once `pay` succeeds, the `RequestPaid` event + on-chain `Paid` status is the immutable receipt.
75+
- **Expiry** — requests can auto-expire; only unpaid, unexpired requests are payable.
76+
77+
## Getting started
78+
79+
Prerequisites:
80+
- [Rust](https://rustup.rs/) with `wasm32-unknown-unknown`.
81+
- [Stellar CLI](https://developers.stellar.org/docs/build/guides/cli) (includes Soroban support):
82+
83+
```bash
84+
rustup target add wasm32-unknown-unknown
85+
```
86+
87+
Clone & build:
88+
```bash
89+
git clone https://github.com/Stack-Rocks/stackpay-contracts.git
90+
cd stackpay-contracts
91+
make build
92+
```
93+
94+
## Building
95+
```bash
96+
make build # -> target/wasm32-unknown-unknown/release/stackpay_contracts.wasm
97+
make optimize # smaller wasm
98+
```
99+
100+
## Testing
101+
Unit tests use `soroban-sdk` testutils (in-memory ledger):
102+
```bash
103+
make test
104+
```
105+
Example:
106+
```rust
107+
#[test]
108+
fn pay_marks_paid() {
109+
// create request, pay with token client, assert status == Paid
110+
}
111+
```
112+
113+
## Local network
114+
```bash
115+
stellar network container start # local sandbox
116+
stellar contract deploy --wasm target/.../stackpay_contracts.wasm --network local
117+
```
118+
119+
## Deployment
120+
1. Build + optimize wasm.
121+
2. Deploy to **Testnet** for staging, then **Mainnet**.
122+
3. Publish the contract id to `stackpay-backend` via its `.env`.
123+
124+
## Security
125+
- Asset transfers use the official SAC token client (no custom token logic).
126+
- `require_auth` on every state-changing call.
127+
- Intended for audit before mainnet (see `docs/security.md`, WIP).
128+
129+
## Contributing
130+
Part of the **Stellar Wave Program** on Drips. Look for `Stellar Wave` / `Good first issue` labels. Run `make test && make lint` before a PR.
131+
132+
## License
133+
[MIT](./LICENSE).

src/lib.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
#![no_std]
2+
3+
//! StackPay — Soroban smart contracts.
4+
//! Single deployable contract: PaymentRequest.
5+
6+
mod request;
7+
8+
pub use request::{PaymentRequest, PaymentRequestClient};

src/request.rs

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
use soroban_sdk::{
2+
contract, contractimpl, contracttype, Address, Env, String, Symbol,
3+
};
4+
use soroban_sdk::token::Client as TokenClient;
5+
6+
#[contracttype]
7+
pub enum Status {
8+
Open,
9+
Paid,
10+
Cancelled,
11+
Expired,
12+
}
13+
14+
#[contracttype]
15+
pub struct Request {
16+
pub id: u64,
17+
pub payee: Address,
18+
pub asset: Address, // Stellar Asset Contract address
19+
pub amount: i128,
20+
pub memo: String,
21+
pub status: Status,
22+
pub created_at: u64,
23+
pub expires_at: u64,
24+
}
25+
26+
#[contract]
27+
pub struct PaymentRequest;
28+
29+
#[contractimpl]
30+
impl PaymentRequest {
31+
/// Create a payment request. `expires_in` is in seconds from now.
32+
pub fn create_request(
33+
env: &Env,
34+
payee: Address,
35+
asset: Address,
36+
amount: i128,
37+
memo: String,
38+
expires_in: u64,
39+
) -> u64 {
40+
payee.require_auth();
41+
let id = env.storage().instance().get(&Symbol::new(env, "next")).unwrap_or(0u64) + 1;
42+
let now = env.ledger().timestamp();
43+
let req = Request {
44+
id,
45+
payee,
46+
asset,
47+
amount,
48+
memo,
49+
status: Status::Open,
50+
created_at: now,
51+
expires_at: now + expires_in,
52+
};
53+
env.storage().instance().set(&Symbol::new(env, "next"), &id);
54+
env.storage().persistent().set(&id, &req);
55+
env.events().publish((Symbol::new(env, "request_created"), id), ());
56+
id
57+
}
58+
59+
/// Payer settles the request; asset is pulled from caller and sent to payee.
60+
pub fn pay(env: &Env, request_id: u64, payer: Address) {
61+
let mut req: Request = env.storage().persistent().get(&request_id).expect("unknown request");
62+
payer.require_auth();
63+
assert!(matches!(req.status, Status::Open), "not open");
64+
assert!(env.ledger().timestamp() <= req.expires_at, "expired");
65+
let token = TokenClient::new(env, &req.asset);
66+
token.transfer(&payer, &req.payee, &req.amount);
67+
req.status = Status::Paid;
68+
env.storage().persistent().set(&request_id, &req);
69+
env.events().publish((Symbol::new(env, "request_paid"), request_id), ());
70+
}
71+
72+
/// Requester cancels an unpaid, unexpired request.
73+
pub fn cancel(env: &Env, request_id: u64) {
74+
let mut req: Request = env.storage().persistent().get(&request_id).expect("unknown request");
75+
req.payee.require_auth();
76+
assert!(matches!(req.status, Status::Open), "not open");
77+
req.status = Status::Cancelled;
78+
env.storage().persistent().set(&request_id, &req);
79+
env.events().publish((Symbol::new(env, "request_cancelled"), request_id), ());
80+
}
81+
82+
pub fn get_request(env: &Env, request_id: u64) -> Request {
83+
env.storage().persistent().get(&request_id).expect("unknown request")
84+
}
85+
86+
pub fn status(env: &Env, request_id: u64) -> Status {
87+
env.storage().persistent().get(&request_id).map(|r: Request| r.status).unwrap_or(Status::Expired)
88+
}
89+
}

0 commit comments

Comments
 (0)