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
21 changes: 21 additions & 0 deletions offchain/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions offchain/tools/.just
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,19 @@ _default:
build: _check-cargo
cargo +stable build --package math --release
cargo +stable build --package llm-openai-chat-completion --release
cargo +stable build --package payments-stripe --release

# Check all native Nexus Tools
check: _check-cargo
cargo +stable check --package math
cargo +stable check --package llm-openai-chat-completion
cargo +stable check --package payments-stripe

# Run all tests in all native Nexus Tools
test: _check-cargo
cargo +stable test --package math
cargo +stable test --package llm-openai-chat-completion
cargo +stable test --package payments-stripe

# Run rustfmt on all native Nexus Tools
fmt-check: _check-cargo
Expand All @@ -33,11 +36,13 @@ fmt-check: _check-cargo

cargo +"$nightly" fmt --package math --check
cargo +"$nightly" fmt --package llm-openai-chat-completion --check
cargo +"$nightly" fmt --package payments-stripe --check

# Run clippy on all native Nexus Tools
clippy: _check-cargo
cargo +stable clippy --package math
cargo +stable clippy --package llm-openai-chat-completion
cargo +stable clippy --package payments-stripe

# Runs the `which` native Nexus Tool. Note that some Tools might need some
# environment variables to be set.
Expand Down
14 changes: 14 additions & 0 deletions offchain/tools/payments-stripe/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# payments-stripe — required environment variables.
#
# Copy to `.env` for local dev (this file MUST stay in source control;
# `.env` MUST be gitignored). In production these come from Cloud Run
# `secretKeyRef` bindings to GCP Secret Manager, configured by the
# operator out-of-band (the deploy pipeline does NOT provision the key).
#
# The tool exits 1 at startup if STRIPE_API_KEY is unset, empty, or does
# not start with sk_test_ / sk_live_ / rk_test_ / rk_live_.

STRIPE_API_KEY=sk_test_replace_with_real_key

# Optional. Defaults to "info".
RUST_LOG=info
41 changes: 41 additions & 0 deletions offchain/tools/payments-stripe/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
[package]
name = "payments-stripe"
description = "Stripe payments tools for Nexus"

edition.workspace = true
version.workspace = true
repository.workspace = true
homepage.workspace = true
license.workspace = true
readme.workspace = true
authors.workspace = true
keywords.workspace = true
categories.workspace = true

[[bin]]
name = "payments-stripe"
path = "src/main.rs"

[dependencies]
anyhow.workspace = true
dotenvy.workspace = true
env_logger.workspace = true
log.workspace = true
reqwest = { workspace = true, features = ["json"] }
schemars.workspace = true
serde.workspace = true
serde_json.workspace = true
thiserror.workspace = true
tokio.workspace = true
zeroize.workspace = true

# === Nexus deps ===
nexus-toolkit.workspace = true
nexus-sdk.workspace = true

[dev-dependencies]
mockito.workspace = true

[build-dependencies]
serde_json.workspace = true
toml = "0.8"
185 changes: 185 additions & 0 deletions offchain/tools/payments-stripe/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
# Stripe tools for Nexus

A set of stateless Nexus Tools that wrap the [Stripe REST
API](https://stripe.com/docs/api). Each tool is a single endpoint.

## Credential handling

- **The tool reads `STRIPE_API_KEY` once from the process environment at
startup.** Sources:
- Production (Cloud Run): mounted via `secretKeyRef` from GCP Secret
Manager, configured by the operator out-of-band (the deploy pipeline
does NOT provision the secret).
- Local dev: copy `.env.example` to `.env` and set
`STRIPE_API_KEY=sk_test_…`.
- **The credential never appears on any `Input` struct.** Tool inputs flow
through the Nexus DAG on Sui as plaintext — anything on `Input` is
effectively published on-chain. The skill's auditor will refuse to mark
the tool ready if any `Input` field is credential-shaped.
- The credential is wrapped in `zeroize::Zeroizing<String>` (heap-zeroed
on drop). The struct hand-implements `Debug` to print `<redacted>`.
- The tool exits 1 at startup if `STRIPE_API_KEY` is unset, empty, or does
not start with one of `sk_test_`, `sk_live_`, `rk_test_`, `rk_live_`.
- **Tests use `sk_test_…` keys only.** Real `sk_live_…` material must
never enter source, fixtures, or logs.

## Environment variables

| Variable | Required | Default | Description |
| --- | --- | --- | --- |
| `STRIPE_API_KEY` | **yes** | — | Stripe secret (`sk_test_…` for staging/test, `sk_live_…` for prod). Validated at startup; the tool refuses to boot without it. |
| `RUST_LOG` | no | `info` | env_logger filter. |
| `BIND_ADDR` | no | `127.0.0.1:8080` | The toolkit's HTTP bind address. |

## Idempotency

Every write endpoint accepts an optional `idempotency_key`. Generate a
UUID per logical retry-bucket in your DAG. Stripe guarantees identical
responses for identical idempotency keys; reusing the same key on retry
prevents double-charges. `idempotency_key` is per-call dedup data — NOT a
credential — so it stays on `Input`.

## FQN versioning

All six FQNs are threaded through `env!("TOOL_FQN_VERSION")` in
`build.rs`; CI sets `TOOL_FQN_VERSION` from the tool's subtree git hash,
so any source change auto-bumps the version on the next deploy. Local
builds default to `@1`.

---

# `xyz.taluslabs.payments.stripe.create-payment-intent@<TOOL_FQN_VERSION>`

Creates a [PaymentIntent](https://stripe.com/docs/api/payment_intents/create).

## Input

- **`idempotency_key`: [`String`] (optional)** — `Idempotency-Key` header value. Generate a UUID per retry-bucket.
- **`amount`: [`i64`]** — Amount in the smallest currency unit (e.g. cents for USD).
- **`currency`: [`String`]** — ISO-4217 currency code (lowercase, e.g. `usd`).
- **`customer`: [`String`] (optional)** — Existing Stripe customer id (`cus_…`).
- **`description`: [`String`] (optional)** — Free-form description shown on the Stripe Dashboard.

## Output Variants & Ports

**`ok`** — PaymentIntent created.

- **`ok.id`: [`String`]** — PaymentIntent id (`pi_…`).
- **`ok.client_secret`: [`String`]** — Used by clients to confirm the intent.
- **`ok.status`: [`String`]** — Stripe lifecycle status (`requires_payment_method`, `requires_confirmation`, etc.).
- **`ok.amount`: [`i64`]** — Echo of the requested amount.
- **`ok.currency`: [`String`]** — Echo of the requested currency.

**`err`** — Stripe rejected the request or the network failed.

- **`err.reason`: [`String`]** — Human-readable error description.
- **`err.kind`: [`String`]** — One of `invalid_request`, `card_error`, `validation_error`, `idempotency_error`, `rate_limit_exceeded`, `unauthorized`, etc.
- **`err.status_code`: [`u16`] (optional)** — HTTP status from Stripe, if any.

---

# `xyz.taluslabs.payments.stripe.get-payment-intent@<TOOL_FQN_VERSION>`

Retrieves a [PaymentIntent](https://stripe.com/docs/api/payment_intents/retrieve) by id.

## Input

- **`payment_intent_id`: [`String`]** — `pi_…` to look up.

## Output Variants & Ports

**`ok`**

- **`ok.id`: [`String`]**
- **`ok.status`: [`String`]**
- **`ok.amount`: [`i64`]**
- **`ok.currency`: [`String`]**
- **`ok.client_secret`: [`String`] (optional)** — present unless the intent has been confirmed.

**`err`** — See `create-payment-intent` for the variant shape.

---

# `xyz.taluslabs.payments.stripe.confirm-payment-intent@<TOOL_FQN_VERSION>`

[Confirms](https://stripe.com/docs/api/payment_intents/confirm) a PaymentIntent.

## Input

- **`idempotency_key`: [`String`] (optional)**
- **`payment_intent_id`: [`String`]** — `pi_…` to confirm.
- **`payment_method`: [`String`] (optional)** — `pm_…` to attach (e.g. `pm_card_visa` in test mode).
- **`return_url`: [`String`] (optional)** — Required for redirect-based payment methods.

## Output Variants & Ports

**`ok`**

- **`ok.id`: [`String`]**
- **`ok.status`: [`String`]**
- **`ok.next_action_type`: [`String`] (optional)** — Set when the PaymentIntent requires further user action.

**`err`** — See above.

---

# `xyz.taluslabs.payments.stripe.create-customer@<TOOL_FQN_VERSION>`

Creates a [Customer](https://stripe.com/docs/api/customers/create).

## Input

- **`idempotency_key`: [`String`] (optional)**
- **`email`: [`String`] (optional)**
- **`name`: [`String`] (optional)**
- **`description`: [`String`] (optional)**

## Output Variants & Ports

**`ok`**

- **`ok.id`: [`String`]** — `cus_…`.
- **`ok.email`: [`String`] (optional)**
- **`ok.created`: [`i64`]** — Unix timestamp.

**`err`** — See above.

---

# `xyz.taluslabs.payments.stripe.get-balance@<TOOL_FQN_VERSION>`

Reads the platform [Balance](https://stripe.com/docs/api/balance/balance_retrieve).

## Input

No input ports — credentials come from env.

## Output Variants & Ports

**`ok`**

- **`ok.available`: [`Vec<{ amount: i64, currency: String }>`]** — Funds available for payout.
- **`ok.pending`: [`Vec<{ amount: i64, currency: String }>`]** — Funds still settling.

**`err`** — See above.

---

# `xyz.taluslabs.payments.stripe.list-charges@<TOOL_FQN_VERSION>`

Lists [Charges](https://stripe.com/docs/api/charges/list).

## Input

- **`limit`: [`i64`] (optional)** — Page size (1–100, Stripe default 10).
- **`customer`: [`String`] (optional)** — Filter to a specific customer id.
- **`starting_after`: [`String`] (optional)** — Cursor for pagination (charge id from the previous page).

## Output Variants & Ports

**`ok`**

- **`ok.charges`: [`Vec<{ id, amount, currency, status, customer? }>`]**
- **`ok.has_more`: [`bool`]** — More results available with `starting_after = charges.last().id`.

**`err`** — See above.
50 changes: 50 additions & 0 deletions offchain/tools/payments-stripe/build.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
use std::{env, fs, path::PathBuf};

fn main() {
let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap());
let tools_json_path = manifest_dir.join("tools.json");
let cargo_toml_path = manifest_dir.join("Cargo.toml");

println!("cargo::rerun-if-changed={}", tools_json_path.display());
println!("cargo::rerun-if-changed={}", cargo_toml_path.display());

let tools_json: serde_json::Value = serde_json::from_str(
&fs::read_to_string(&tools_json_path)
.unwrap_or_else(|e| panic!("failed to read {}: {e}", tools_json_path.display())),
)
.expect("tools.json must be valid JSON");

let command = tools_json["command"]
.as_str()
.expect("tools.json must have command");

let cargo_toml: toml::Value = toml::from_str(
&fs::read_to_string(&cargo_toml_path)
.unwrap_or_else(|e| panic!("failed to read {}: {e}", cargo_toml_path.display())),
)
.expect("Cargo.toml must be valid TOML");

let bin_name = cargo_toml
.get("bin")
.and_then(|b| b.as_array())
.and_then(|bins| bins.first())
.and_then(|b| b.get("name"))
.and_then(|n| n.as_str());

match bin_name {
Some(name) if name == command => {}
Some(name) => panic!(
"Binary name mismatch: Cargo.toml [[bin]] name = \"{name}\" \
but tools.json command = \"{command}\". They must match."
),
None => panic!(
"Cargo.toml has no [[bin]] section but tools.json specifies \
command = \"{command}\". Add: [[bin]]\nname = \"{command}\""
),
}

// FQN version is set by CI via Docker build-arg; defaults to "1" locally.
let version = env::var("TOOL_FQN_VERSION").unwrap_or_else(|_| "1".to_string());
println!("cargo:rustc-env=TOOL_FQN_VERSION={version}");
println!("cargo:rerun-if-env-changed=TOOL_FQN_VERSION");
}
Loading
Loading