Standalone, trait-based Akash deployment workflow engine. No framework coupling.
Core only (SDL parsing, manifest building, workflow engine):
[dependencies]
akash-deploy-rs = { version = "0.0.6" }Full client with gRPC, signing, and file storage:
[dependencies]
akash-deploy-rs = { version = "0.0.6", features = ["default-client", "file-storage"] }use akash_deploy_rs::*;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let mnemonic = std::env::var("TEST_MNEMONIC")?;
let client = AkashClient::new_from_mnemonic(
&mnemonic, "https://rpc.akashnet.net:443", "https://grpc.akashnet.net:443",
).await?;
let signer = KeySigner::new_mnemonic_str(&mnemonic, None)
.map_err(|e| format!("signer: {e:?}"))?;
let sdl = std::fs::read_to_string("deploy.yaml")?;
let mut state = DeploymentState::new("my-app", client.address()).with_sdl(&sdl);
let workflow = DeploymentWorkflow::new(&client, &signer, Default::default());
loop {
match workflow.advance(&mut state).await? {
StepResult::Continue => continue,
StepResult::NeedsInput(InputRequired::SelectProvider { bids }) => {
DeploymentWorkflow::<AkashClient>::select_provider(&mut state, &bids[0].provider)?;
}
StepResult::Complete => { break; }
StepResult::Failed(reason) => return Err(reason.into()),
_ => {}
}
}
Ok(())
}| Feature | Default | What it enables |
|---|---|---|
sdl-templates |
yes | ${VAR} template substitution in SDL files |
default-client |
no | AkashClient (layer-climb gRPC, HD signing, reqwest HTTP) |
file-storage |
no | FileDeploymentStore, FileBackedStorage |
log-streaming |
no | WsLogStream (WebSocket provider logs) |
rpc |
no | tonic gRPC stubs (Console API + Akash protos) |
Akash wire types live under src/gen/, generated by the in-tree prost toolchain from protos vendored out of sibling chain-sdk.
just gen-proto-fresh # sync-protos + modvendor (k8s) + gen-proto
cargo chec
just test-unitDetails: docs/proto-sync.md.
| python | no | PyO3 bindings |
flowchart TB
SDL[SDL YAML] --> MB[ManifestBuilder]
MB --> MG[ManifestGroup]
MG --> CJ[to_canonical_json]
CJ --> Hash[SHA256 Hash]
subgraph akash-deploy-rs
MB
CJ
JB[JwtBuilder]
CG[Certificate Generator]
WF[DeploymentWorkflow]
end
subgraph Consumer Implementation
Backend[AkashBackend Trait]
Storage[(Storage)]
HTTP[HTTP Client]
Signer[Key Signer]
end
WF --> Backend
Backend --> Storage
Backend --> HTTP
WF --> Signer
JB --> Signer
sequenceDiagram
participant App as Your App
participant WF as DeploymentWorkflow
participant Backend as AkashBackend
participant Chain as Akash Chain
participant Provider as Akash Provider
App->>WF: advance(state)
WF->>Backend: query_balance(owner)
Backend-->>WF: Balance OK
WF->>WF: generate_certificate()
WF->>Backend: broadcast_create_deployment(msg)
Backend-->>WF: dseq
WF->>Backend: query_bids(lease_id)
Backend-->>WF: Bids
Note over WF,App: User selects provider
WF->>Backend: broadcast_create_lease(bid)
Backend-->>WF: Lease created
WF->>WF: build_manifest + generate_jwt
WF->>Backend: send_manifest(provider, manifest)
Backend-->>WF: Accepted
WF->>Backend: query_provider_status(lease)
Backend-->>WF: Endpoints
WF-->>App: Complete
use akash_deploy_rs::{ManifestBuilder, to_canonical_json};
let builder = ManifestBuilder::new(&owner, dseq);
let groups = builder.build_from_sdl(sdl_yaml)?;
let json = to_canonical_json(&groups)?;Implement AkashBackend to bring your own transport and storage:
struct MyBackend { /* ... */ }
impl AkashBackend for MyBackend {
type Signer = MySigner;
// query_balance, broadcast_create_deployment, send_manifest, etc.
}Provider validation requires byte-identical JSON. ManifestBuilder handles all of these:
| Rule | Detail |
|---|---|
| CPU units | String millicores: "1000" not 1.0 |
| Memory/storage sizes | String bytes: "536870912" not int |
| Memory/storage field name | "size" (Go JSON tag), not "quantity" (proto) |
| Empty arrays | null not [] for command/args/env |
| Field names | camelCase: externalPort, httpOptions, readOnly |
| GPU attributes | Composite keys: vendor/nvidia/model/h100/ram/80Gi |
| Storage attributes | Sorted by key |
| Services | Sorted by name |
| Resource endpoints | Only global exposes; GroupSpec kind = 0 (SHARED_HTTP) |
Akash providers use self-attested ES256K JWTs. No challenge-response -- each request is validated against the on-chain public key. The workflow handles this during EnsureAuth.
let claims = JwtClaims::new("akash1youraddress...");
let jwt = JwtBuilder::new().build_and_sign(&claims, |msg| signer.sign(msg))?;Enabled by default. Use ${VAR} placeholders:
services:
web:
image: ${IMAGE}:${VERSION}
expose:
- port: ${PORT}
as: ${PORT}
to:
- global: truelet template = SdlTemplate::new(template_content)?;
template.validate(&defaults)?;
let processed = template.process(&variables, &defaults)?;| Command | What it runs | Requires |
|---|---|---|
just test |
All tests (unit + e2e) | Go toolchain |
just test-unit |
Unit tests only | -- |
just test-e2e |
Rust manifest/JWT vs Go provider validation | Go toolchain |
just test-jwt |
JWT signing vs Go AuthProcess() |
Go toolchain |
just test-manifest |
Manifest hash vs Go Manifest.Version |
Go toolchain |
just test-one <file> |
Single SDL through provider validation | Go toolchain |
just test-live |
Full deployment on live network | TEST_MNEMONIC |
just demo |
Live E2E deployment demo | TEST_MNEMONIC |
just coverage |
Coverage report | cargo-tarpaulin |
For live tests, copy tests/.env.example to tests/.env with your mnemonic.
This repo includes .claude/skills/akash-manifest-spec for agent-assisted development:
ln -s /path/to/akash-deploy-rs/.claude/skills/akash-manifest-spec ~/.claude/skills/Activates on manifest serialization, provider validation, hash mismatch, and canonical JSON topics.
MIT OR Apache-2.0