10 Commits

Author SHA1 Message Date
cb1fb0f0ec Merge pull request 'main' (#13) from main into development
Reviewed-on: #13
2025-08-28 14:05:57 +00:00
97c24d146b ... 2025-08-27 18:52:11 +02:00
6bff52e8b7 ... 2025-08-27 14:44:39 +02:00
7a999b7b6e Merge branch 'main' of git.ourworld.tf:herocode/db 2025-08-21 17:26:42 +02:00
095a4d0c69 ... 2025-08-21 17:26:40 +02:00
Timur Gordon
a7c978efd4 use git paths for deps instead 2025-08-21 14:35:07 +02:00
Timur Gordon
0b0d546b4e remove old cargo lock 2025-08-21 14:27:52 +02:00
Timur Gordon
2f5e18df98 reexport heroledger 2025-08-21 14:20:25 +02:00
Timur Gordon
77169c073c clean old deps 2025-08-21 14:17:37 +02:00
Timur Gordon
ce12f26a91 cargo fix & fix heroledger 2025-08-21 14:15:29 +02:00
130 changed files with 1749 additions and 1832 deletions

112
Cargo.lock generated
View File

@@ -65,13 +65,13 @@ checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
[[package]]
name = "async-trait"
version = "0.1.88"
version = "0.1.89"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5"
checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.104",
"syn 2.0.106",
]
[[package]]
@@ -123,9 +123,9 @@ dependencies = [
[[package]]
name = "bitflags"
version = "2.9.1"
version = "2.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967"
checksum = "6a65b545ab31d687cff52899d4890855fec459eb6afe0da6417b8a18da87aa29"
[[package]]
name = "bitvec"
@@ -168,7 +168,7 @@ dependencies = [
"proc-macro-crate",
"proc-macro2",
"quote",
"syn 2.0.104",
"syn 2.0.106",
]
[[package]]
@@ -213,18 +213,18 @@ checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a"
[[package]]
name = "cc"
version = "1.2.31"
version = "1.2.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3a42d84bb6b69d3a8b3eaacf0d88f179e1929695e1ad012b6cf64d9caaa5fd2"
checksum = "3ee0f8803222ba5a7e2777dd72ca451868909b1ac410621b676adf07280e9b5f"
dependencies = [
"shlex",
]
[[package]]
name = "cfg-if"
version = "1.0.1"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268"
checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9"
[[package]]
name = "cfg_aliases"
@@ -372,7 +372,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.104",
"syn 2.0.106",
]
[[package]]
@@ -452,9 +452,9 @@ dependencies = [
[[package]]
name = "hashbrown"
version = "0.15.4"
version = "0.15.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5"
checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
[[package]]
name = "heck"
@@ -494,7 +494,7 @@ dependencies = [
"proc-macro2",
"quote",
"serde",
"syn 2.0.104",
"syn 2.0.106",
]
[[package]]
@@ -545,7 +545,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661"
dependencies = [
"equivalent",
"hashbrown 0.15.4",
"hashbrown 0.15.5",
]
[[package]]
@@ -597,7 +597,7 @@ checksum = "03343451ff899767262ec32146f6d559dd759fdadf42ff0e227c7c48f72594b4"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.104",
"syn 2.0.106",
]
[[package]]
@@ -627,9 +627,9 @@ dependencies = [
[[package]]
name = "jsonb"
version = "0.5.3"
version = "0.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96cbb4fba292867a2d86ed83dbe5f9d036f423bf6a491b7d884058b2fde42fcd"
checksum = "a452366d21e8d3cbca680c41388e01d6a88739afef7877961946a6da409f9ccd"
dependencies = [
"byteorder",
"ethnum",
@@ -647,9 +647,20 @@ dependencies = [
[[package]]
name = "libc"
version = "0.2.174"
version = "0.2.175"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776"
checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543"
[[package]]
name = "libredox"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "391290121bad3d37fbddad76d8f5d1c1c314cfc646d143d7e07a3086ddff0ce3"
dependencies = [
"bitflags",
"libc",
"redox_syscall",
]
[[package]]
name = "lock_api"
@@ -760,6 +771,7 @@ dependencies = [
[[package]]
name = "ourdb"
version = "0.1.0"
source = "git+https://git.ourworld.tf/herocode/herolib_rust#aa0248ef17cb0117bb69f1d9f278f995bb417f16"
dependencies = [
"crc32fast",
"log",
@@ -792,9 +804,9 @@ dependencies = [
[[package]]
name = "percent-encoding"
version = "2.3.1"
version = "2.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
[[package]]
name = "phf"
@@ -906,9 +918,9 @@ dependencies = [
[[package]]
name = "proc-macro2"
version = "1.0.95"
version = "1.0.101"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778"
checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de"
dependencies = [
"unicode-ident",
]
@@ -1079,12 +1091,13 @@ checksum = "a5a11a05ee1ce44058fa3d5961d05194fdbe3ad6b40f904af764d81b86450e6b"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.104",
"syn 2.0.106",
]
[[package]]
name = "rhailib-macros"
version = "0.1.0"
source = "git+https://git.ourworld.tf/herocode/herolib_rust#aa0248ef17cb0117bb69f1d9f278f995bb417f16"
dependencies = [
"rhai",
"serde",
@@ -1143,9 +1156,9 @@ checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace"
[[package]]
name = "rustversion"
version = "1.0.21"
version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d"
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
[[package]]
name = "ryu"
@@ -1191,14 +1204,14 @@ checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.104",
"syn 2.0.106",
]
[[package]]
name = "serde_json"
version = "1.0.142"
version = "1.0.143"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "030fedb782600dcbd6f02d479bf0d817ac3bb40d644745b769d6a96bc3afc5a7"
checksum = "d401abef1d108fbd9cbaebc3e46611f4b1021f714a0597a71f41ee463f5f4a5a"
dependencies = [
"indexmap",
"itoa",
@@ -1247,9 +1260,9 @@ checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d"
[[package]]
name = "slab"
version = "0.4.10"
version = "0.4.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04dc19736151f35336d325007ac991178d504a119863a2fcb3758cdb5e52c50d"
checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589"
[[package]]
name = "smallvec"
@@ -1327,7 +1340,7 @@ dependencies = [
"proc-macro2",
"quote",
"rustversion",
"syn 2.0.104",
"syn 2.0.106",
]
[[package]]
@@ -1349,9 +1362,9 @@ dependencies = [
[[package]]
name = "syn"
version = "2.0.104"
version = "2.0.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40"
checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6"
dependencies = [
"proc-macro2",
"quote",
@@ -1387,7 +1400,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.104",
"syn 2.0.106",
]
[[package]]
@@ -1401,9 +1414,9 @@ dependencies = [
[[package]]
name = "tinyvec"
version = "1.9.0"
version = "1.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09b3661f17e86524eccd4371ab0429194e0d7c008abb45f7a7495b1719463c71"
checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa"
dependencies = [
"tinyvec_macros",
]
@@ -1442,7 +1455,7 @@ checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.104",
"syn 2.0.106",
]
[[package]]
@@ -1504,6 +1517,7 @@ dependencies = [
[[package]]
name = "tst"
version = "0.1.0"
source = "git+https://git.ourworld.tf/herocode/herolib_rust#aa0248ef17cb0117bb69f1d9f278f995bb417f16"
dependencies = [
"ourdb",
"thiserror",
@@ -1550,9 +1564,9 @@ checksum = "6d49784317cd0d1ee7ec5c716dd598ec5b4483ea832a2dced265471cc0f690ae"
[[package]]
name = "uuid"
version = "1.17.0"
version = "1.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3cf4199d1e5d15ddd86a694e4d0dffa9c323ce759fea589f00fef9d81cc1931d"
checksum = "f33196643e165781c20a5ead5582283a7dacbb87855d867fbc2df3f81eddc1be"
dependencies = [
"getrandom 0.3.3",
"js-sys",
@@ -1614,7 +1628,7 @@ dependencies = [
"log",
"proc-macro2",
"quote",
"syn 2.0.104",
"syn 2.0.106",
"wasm-bindgen-shared",
]
@@ -1636,7 +1650,7 @@ checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.104",
"syn 2.0.106",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
@@ -1662,11 +1676,11 @@ dependencies = [
[[package]]
name = "whoami"
version = "1.6.0"
version = "1.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6994d13118ab492c3c80c1f81928718159254c53c472bf9ce36f8dae4add02a7"
checksum = "5d4a4db5077702ca3015d3d02d74974948aba2ad9e12ab7df718ee64ccd7e97d"
dependencies = [
"redox_syscall",
"libredox",
"wasite",
"web-sys",
]
@@ -1692,7 +1706,7 @@ checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.104",
"syn 2.0.106",
]
[[package]]
@@ -1703,7 +1717,7 @@ checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.104",
"syn 2.0.106",
]
[[package]]
@@ -1856,5 +1870,5 @@ checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.104",
"syn 2.0.106",
]

1617
heromodels/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -10,11 +10,11 @@ serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
bincode = { version = "2", features = ["serde"] }
chrono = { version = "0.4", features = ["serde"] }
ourdb = { path = "../../herolib_rust/packages/data/ourdb" }
tst = { path = "../../herolib_rust/packages/data/tst" }
ourdb = { git = "https://git.ourworld.tf/herocode/herolib_rust", package = "ourdb" }
tst = { git = "https://git.ourworld.tf/herocode/herolib_rust", package = "tst" }
heromodels-derive = { path = "../heromodels-derive" }
heromodels_core = { path = "../heromodels_core" }
rhailib-macros = { path = "../../herolib_rust/rhailib/src/macros" }
rhailib-macros = { git = "https://git.ourworld.tf/herocode/herolib_rust", package = "rhailib-macros" }
rhai = { version = "1.21.0", features = [
"std",
"sync",
@@ -53,10 +53,10 @@ path = "examples/finance_example/main.rs"
name = "flow_example"
path = "examples/flow_example.rs"
[[example]]
name = "biz_rhai"
path = "examples/biz_rhai/example.rs"
required-features = ["rhai"]
# [[example]]
# name = "biz_rhai"
# path = "examples/biz_rhai/example.rs"
# required-features = ["rhai"]
[[example]]
name = "postgres_model_example"

View File

@@ -1,26 +1,26 @@
use circles_launcher::{new_launcher};
use heromodels::models::circle::circle::{new_circle};
use secp256k1::{Secp256k1, SecretKey, PublicKey};
use circles_launcher::new_launcher;
use heromodels::models::circle::circle::new_circle;
use rand::rngs::OsRng;
use secp256k1::{PublicKey, Secp256k1, SecretKey};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Generate valid secp256k1 keypairs for testing
let secp = Secp256k1::new();
let mut rng = OsRng;
let secret_key1 = SecretKey::new(&mut rng);
let public_key1 = PublicKey::from_secret_key(&secp, &secret_key1);
let pk1_hex = hex::encode(public_key1.serialize());
let secret_key2 = SecretKey::new(&mut rng);
let public_key2 = PublicKey::from_secret_key(&secp, &secret_key2);
let pk2_hex = hex::encode(public_key2.serialize());
let secret_key3 = SecretKey::new(&mut rng);
let public_key3 = PublicKey::from_secret_key(&secp, &secret_key3);
let pk3_hex = hex::encode(public_key3.serialize());
println!("Generated test public keys:");
println!(" PK1: {}", pk1_hex);
println!(" PK2: {}", pk2_hex);
@@ -36,4 +36,4 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
.save();
Ok(())
}
}

View File

@@ -1,6 +1,6 @@
use heromodels::models::heroledger::rhai::register_heroledger_rhai_modules;
use heromodels_core::db::hero::OurDB;
use rhai::{Dynamic, Engine};
use heromodels::models::heroledger::rhai::register_heroledger_rhai_modules;
use std::sync::Arc;
use std::{fs, path::Path};

View File

@@ -43,8 +43,10 @@ fn main() {
// Clean up any existing data to ensure consistent results
println!("Cleaning up existing data...");
let user_collection = db.collection::<User>().expect("can open user collection");
let comment_collection = db.collection::<Comment>().expect("can open comment collection");
let comment_collection = db
.collection::<Comment>()
.expect("can open comment collection");
// Clear all existing users and comments
if let Ok(existing_users) = user_collection.get_all() {
for user in existing_users {

View File

@@ -8,8 +8,8 @@ use std::{
collections::HashSet,
path::PathBuf,
sync::{
atomic::{AtomicU32, Ordering},
Arc, Mutex,
atomic::{AtomicU32, Ordering},
},
};

View File

@@ -119,4 +119,4 @@ impl Circle {
/// Creates a new circle builder
pub fn new_circle() -> Circle {
Circle::new()
}
}

View File

@@ -1,16 +1,17 @@
use crate::db::Db;
use rhailib_macros::{
register_authorized_create_by_id_fn, register_authorized_delete_by_id_fn, register_authorized_get_by_id_fn,
};
use rhai::plugin::*;
use rhai::{Array, Dynamic, Engine, EvalAltResult, Map, Module};
use rhailib_macros::{
register_authorized_create_by_id_fn, register_authorized_delete_by_id_fn,
register_authorized_get_by_id_fn,
};
use std::collections::HashMap;
use std::sync::Arc;
use crate::models::circle::Circle;
type RhaiCircle = Circle;
use crate::db::hero::OurDB;
use crate::db::Collection;
use crate::db::hero::OurDB;
use crate::models::circle::ThemeData;
#[export_module]

View File

@@ -1,16 +1,6 @@
pub mod node;
pub use node::{
Node,
DeviceInfo,
StorageDevice,
MemoryDevice,
CPUDevice,
GPUDevice,
NetworkDevice,
NodeCapacity,
ComputeSlice,
StorageSlice,
PricingPolicy,
SLAPolicy,
};
CPUDevice, ComputeSlice, DeviceInfo, GPUDevice, MemoryDevice, NetworkDevice, Node,
NodeCapacity, PricingPolicy, SLAPolicy, StorageDevice, StorageSlice,
};

View File

@@ -1,6 +1,6 @@
use heromodels_core::BaseModelData;
use heromodels_derive::model;
use rhai::CustomType;
use rhai::{CustomType, TypeBuilder};
use serde::{Deserialize, Serialize};
/// Storage device information
@@ -160,19 +160,58 @@ impl ComputeSlice {
}
}
pub fn nodeid(mut self, nodeid: u32) -> Self { self.nodeid = nodeid; self }
pub fn slice_id(mut self, id: i32) -> Self { self.id = id; self }
pub fn mem_gb(mut self, v: f64) -> Self { self.mem_gb = v; self }
pub fn storage_gb(mut self, v: f64) -> Self { self.storage_gb = v; self }
pub fn passmark(mut self, v: i32) -> Self { self.passmark = v; self }
pub fn vcores(mut self, v: i32) -> Self { self.vcores = v; self }
pub fn cpu_oversubscription(mut self, v: i32) -> Self { self.cpu_oversubscription = v; self }
pub fn storage_oversubscription(mut self, v: i32) -> Self { self.storage_oversubscription = v; self }
pub fn price_range(mut self, min_max: Vec<f64>) -> Self { self.price_range = min_max; self }
pub fn gpus(mut self, v: u8) -> Self { self.gpus = v; self }
pub fn price_cc(mut self, v: f64) -> Self { self.price_cc = v; self }
pub fn pricing_policy(mut self, p: PricingPolicy) -> Self { self.pricing_policy = p; self }
pub fn sla_policy(mut self, p: SLAPolicy) -> Self { self.sla_policy = p; self }
pub fn nodeid(mut self, nodeid: u32) -> Self {
self.nodeid = nodeid;
self
}
pub fn slice_id(mut self, id: i32) -> Self {
self.id = id;
self
}
pub fn mem_gb(mut self, v: f64) -> Self {
self.mem_gb = v;
self
}
pub fn storage_gb(mut self, v: f64) -> Self {
self.storage_gb = v;
self
}
pub fn passmark(mut self, v: i32) -> Self {
self.passmark = v;
self
}
pub fn vcores(mut self, v: i32) -> Self {
self.vcores = v;
self
}
pub fn cpu_oversubscription(mut self, v: i32) -> Self {
self.cpu_oversubscription = v;
self
}
pub fn storage_oversubscription(mut self, v: i32) -> Self {
self.storage_oversubscription = v;
self
}
pub fn price_range(mut self, min_max: Vec<f64>) -> Self {
self.price_range = min_max;
self
}
pub fn gpus(mut self, v: u8) -> Self {
self.gpus = v;
self
}
pub fn price_cc(mut self, v: f64) -> Self {
self.price_cc = v;
self
}
pub fn pricing_policy(mut self, p: PricingPolicy) -> Self {
self.pricing_policy = p;
self
}
pub fn sla_policy(mut self, p: SLAPolicy) -> Self {
self.sla_policy = p;
self
}
}
/// Storage slice (typically 1GB of storage)
@@ -204,11 +243,26 @@ impl StorageSlice {
}
}
pub fn nodeid(mut self, nodeid: u32) -> Self { self.nodeid = nodeid; self }
pub fn slice_id(mut self, id: i32) -> Self { self.id = id; self }
pub fn price_cc(mut self, v: f64) -> Self { self.price_cc = v; self }
pub fn pricing_policy(mut self, p: PricingPolicy) -> Self { self.pricing_policy = p; self }
pub fn sla_policy(mut self, p: SLAPolicy) -> Self { self.sla_policy = p; self }
pub fn nodeid(mut self, nodeid: u32) -> Self {
self.nodeid = nodeid;
self
}
pub fn slice_id(mut self, id: i32) -> Self {
self.id = id;
self
}
pub fn price_cc(mut self, v: f64) -> Self {
self.price_cc = v;
self
}
pub fn pricing_policy(mut self, p: PricingPolicy) -> Self {
self.pricing_policy = p;
self
}
pub fn sla_policy(mut self, p: SLAPolicy) -> Self {
self.sla_policy = p;
self
}
}
/// Grid4 Node model
@@ -248,17 +302,41 @@ impl Node {
}
}
pub fn nodegroupid(mut self, v: i32) -> Self { self.nodegroupid = v; self }
pub fn uptime(mut self, v: i32) -> Self { self.uptime = v; self }
pub fn add_compute_slice(mut self, s: ComputeSlice) -> Self { self.computeslices.push(s); self }
pub fn add_storage_slice(mut self, s: StorageSlice) -> Self { self.storageslices.push(s); self }
pub fn devices(mut self, d: DeviceInfo) -> Self { self.devices = d; self }
pub fn country(mut self, c: impl ToString) -> Self { self.country = c.to_string(); self }
pub fn capacity(mut self, c: NodeCapacity) -> Self { self.capacity = c; self }
pub fn provisiontime(mut self, t: u32) -> Self { self.provisiontime = t; self }
pub fn nodegroupid(mut self, v: i32) -> Self {
self.nodegroupid = v;
self
}
pub fn uptime(mut self, v: i32) -> Self {
self.uptime = v;
self
}
pub fn add_compute_slice(mut self, s: ComputeSlice) -> Self {
self.computeslices.push(s);
self
}
pub fn add_storage_slice(mut self, s: StorageSlice) -> Self {
self.storageslices.push(s);
self
}
pub fn devices(mut self, d: DeviceInfo) -> Self {
self.devices = d;
self
}
pub fn country(mut self, c: impl ToString) -> Self {
self.country = c.to_string();
self
}
pub fn capacity(mut self, c: NodeCapacity) -> Self {
self.capacity = c;
self
}
pub fn provisiontime(mut self, t: u32) -> Self {
self.provisiontime = t;
self
}
/// Placeholder for capacity recalculation out of the devices on the Node
pub fn recalc_capacity(mut self) -> Self {
pub fn recalc_capacity(self) -> Self {
// TODO: calculate NodeCapacity out of the devices on the Node
self
}

View File

@@ -0,0 +1,194 @@
# Grid4 Data Model
This module defines data models for nodes, groups, and slices in a cloud/grid infrastructure. Each root object is marked with `@[heap]` and can be indexed for efficient querying.
## Root Objects Overview
| Object | Description | Index Fields |
| ----------- | --------------------------------------------- | ------------------------------ |
| `Node` | Represents a single node in the grid | `id`, `nodegroupid`, `country` |
| `NodeGroup` | Represents a group of nodes owned by a farmer | `id`, `farmerid` |
---
## Node
Represents a single node in the grid with slices, devices, and capacity.
| Field | Type | Description | Indexed |
| --------------- | ---------------- | -------------------------------------------- | ------- |
| `id` | `int` | Unique node ID | ✅ |
| `nodegroupid` | `int` | ID of the owning node group | ✅ |
| `uptime` | `int` | Uptime percentage (0-100) | ✅ |
| `computeslices` | `[]ComputeSlice` | List of compute slices | ❌ |
| `storageslices` | `[]StorageSlice` | List of storage slices | ❌ |
| `devices` | `DeviceInfo` | Hardware device info (storage, memory, etc.) | ❌ |
| `country` | `string` | 2-letter country code | ✅ |
| `capacity` | `NodeCapacity` | Aggregated hardware capacity | ❌ |
| `provisiontime` | `u32` | Provisioning time (simple/compatible format) | ✅ |
---
## NodeGroup
Represents a group of nodes owned by a farmer, with policies.
| Field | Type | Description | Indexed |
| ------------------------------------- | --------------- | ---------------------------------------------- | ------- |
| `id` | `u32` | Unique group ID | ✅ |
| `farmerid` | `u32` | Farmer/user ID | ✅ |
| `secret` | `string` | Encrypted secret for booting nodes | ❌ |
| `description` | `string` | Group description | ❌ |
| `slapolicy` | `SLAPolicy` | SLA policy details | ❌ |
| `pricingpolicy` | `PricingPolicy` | Pricing policy details | ❌ |
| `compute_slice_normalized_pricing_cc` | `f64` | Pricing per 2GB compute slice in cloud credits | ❌ |
| `storage_slice_normalized_pricing_cc` | `f64` | Pricing per 1GB storage slice in cloud credits | ❌ |
| `reputation` | `int` | Reputation (0-100) | ✅ |
| `uptime` | `int` | Uptime (0-100) | ✅ |
---
## ComputeSlice
Represents a compute slice (e.g., 1GB memory unit).
| Field | Type | Description |
| -------------------------- | --------------- | -------------------------------- |
| `nodeid` | `u32` | Owning node ID |
| `id` | `int` | Slice ID in node |
| `mem_gb` | `f64` | Memory in GB |
| `storage_gb` | `f64` | Storage in GB |
| `passmark` | `int` | Passmark score |
| `vcores` | `int` | Virtual cores |
| `cpu_oversubscription` | `int` | CPU oversubscription ratio |
| `storage_oversubscription` | `int` | Storage oversubscription ratio |
| `price_range` | `[]f64` | Price range [min, max] |
| `gpus` | `u8` | Number of GPUs |
| `price_cc` | `f64` | Price per slice in cloud credits |
| `pricing_policy` | `PricingPolicy` | Pricing policy |
| `sla_policy` | `SLAPolicy` | SLA policy |
---
## StorageSlice
Represents a 1GB storage slice.
| Field | Type | Description |
| ---------------- | --------------- | -------------------------------- |
| `nodeid` | `u32` | Owning node ID |
| `id` | `int` | Slice ID in node |
| `price_cc` | `f64` | Price per slice in cloud credits |
| `pricing_policy` | `PricingPolicy` | Pricing policy |
| `sla_policy` | `SLAPolicy` | SLA policy |
---
## DeviceInfo
Hardware device information for a node.
| Field | Type | Description |
| --------- | ----------------- | ----------------------- |
| `vendor` | `string` | Vendor of the node |
| `storage` | `[]StorageDevice` | List of storage devices |
| `memory` | `[]MemoryDevice` | List of memory devices |
| `cpu` | `[]CPUDevice` | List of CPU devices |
| `gpu` | `[]GPUDevice` | List of GPU devices |
| `network` | `[]NetworkDevice` | List of network devices |
---
## StorageDevice
| Field | Type | Description |
| ------------- | -------- | --------------------- |
| `id` | `string` | Unique ID for device |
| `size_gb` | `f64` | Size in GB |
| `description` | `string` | Description of device |
---
## MemoryDevice
| Field | Type | Description |
| ------------- | -------- | --------------------- |
| `id` | `string` | Unique ID for device |
| `size_gb` | `f64` | Size in GB |
| `description` | `string` | Description of device |
---
## CPUDevice
| Field | Type | Description |
| ------------- | -------- | ------------------------ |
| `id` | `string` | Unique ID for device |
| `cores` | `int` | Number of CPU cores |
| `passmark` | `int` | Passmark benchmark score |
| `description` | `string` | Description of device |
| `cpu_brand` | `string` | Brand of the CPU |
| `cpu_version` | `string` | Version of the CPU |
---
## GPUDevice
| Field | Type | Description |
| ------------- | -------- | --------------------- |
| `id` | `string` | Unique ID for device |
| `cores` | `int` | Number of GPU cores |
| `memory_gb` | `f64` | GPU memory in GB |
| `description` | `string` | Description of device |
| `gpu_brand` | `string` | Brand of the GPU |
| `gpu_version` | `string` | Version of the GPU |
---
## NetworkDevice
| Field | Type | Description |
| ------------- | -------- | --------------------- |
| `id` | `string` | Unique ID for device |
| `speed_mbps` | `int` | Network speed in Mbps |
| `description` | `string` | Description of device |
---
## NodeCapacity
Aggregated hardware capacity for a node.
| Field | Type | Description |
| ------------ | ----- | ---------------------- |
| `storage_gb` | `f64` | Total storage in GB |
| `mem_gb` | `f64` | Total memory in GB |
| `mem_gb_gpu` | `f64` | Total GPU memory in GB |
| `passmark` | `int` | Total passmark score |
| `vcores` | `int` | Total virtual cores |
---
## SLAPolicy
Service Level Agreement policy for slices or node groups.
| Field | Type | Description |
| -------------------- | ----- | --------------------------------------- |
| `sla_uptime` | `int` | Required uptime % (e.g., 90) |
| `sla_bandwidth_mbit` | `int` | Guaranteed bandwidth in Mbps (0 = none) |
| `sla_penalty` | `int` | Penalty % if SLA is breached (0-100) |
---
## PricingPolicy
Pricing policy for slices or node groups.
| Field | Type | Description |
| ---------------------------- | ------- | --------------------------------------------------------- |
| `marketplace_year_discounts` | `[]int` | Discounts for 1Y, 2Y, 3Y prepaid usage (e.g. [30,40,50]) |
| `volume_discounts` | `[]int` | Volume discounts based on purchase size (e.g. [10,20,30]) |

View File

@@ -0,0 +1,37 @@
module datamodel
// I can bid for infra, and optionally get accepted
@[heap]
pub struct Bid {
pub mut:
id u32
customer_id u32 // links back to customer for this capacity (user on ledger)
compute_slices_nr int // nr of slices I need in 1 machine
compute_slice_price f64 // price per 1 GB slice I want to accept
storage_slices_nr int
storage_slice_price f64 // price per 1 GB storage slice I want to accept
storage_slices_nr int
status BidStatus
obligation bool // if obligation then will be charged and money needs to be in escrow, otherwise its an intent
start_date u32 // epoch
end_date u32
signature_user string // signature as done by a user/consumer to validate their identity and intent
billing_period BillingPeriod
}
pub enum BidStatus {
pending
confirmed
assigned
cancelled
done
}
pub enum BillingPeriod {
hourly
monthly
yearly
biannually
triannually
}

View File

@@ -0,0 +1,52 @@
module datamodel
// I can bid for infra, and optionally get accepted
@[heap]
pub struct Contract {
pub mut:
id u32
customer_id u32 // links back to customer for this capacity (user on ledger)
compute_slices []ComputeSliceProvisioned
storage_slices []StorageSliceProvisioned
compute_slice_price f64 // price per 1 GB agreed upon
storage_slice_price f64 // price per 1 GB agreed upon
network_slice_price f64 // price per 1 GB agreed upon (transfer)
status ContractStatus
start_date u32 // epoch
end_date u32
signature_user string // signature as done by a user/consumer to validate their identity and intent
signature_hoster string // signature as done by the hoster
billing_period BillingPeriod
}
pub enum ConctractStatus {
active
cancelled
error
paused
}
// typically 1GB of memory, but can be adjusted based based on size of machine
pub struct ComputeSliceProvisioned {
pub mut:
node_id u32
id u16 // the id of the slice in the node
mem_gb f64
storage_gb f64
passmark int
vcores int
cpu_oversubscription int
tags string
}
// 1GB of storage
pub struct StorageSliceProvisioned {
pub mut:
node_id u32
id u16 // the id of the slice in the node, are tracked in the node itself
storage_size_gb int
tags string
}

View File

@@ -0,0 +1,104 @@
module datamodel
//ACCESS ONLY TF
@[heap]
pub struct Node {
pub mut:
id int
nodegroupid int
uptime int // 0..100
computeslices []ComputeSlice
storageslices []StorageSlice
devices DeviceInfo
country string // 2 letter code as specified in lib/data/countries/data/countryInfo.txt, use that library for validation
capacity NodeCapacity // Hardware capacity details
birthtime u32 // first time node was active
pubkey string
signature_node string // signature done on node to validate pubkey with privkey
signature_farmer string // signature as done by farmers to validate their identity
}
pub struct DeviceInfo {
pub mut:
vendor string
storage []StorageDevice
memory []MemoryDevice
cpu []CPUDevice
gpu []GPUDevice
network []NetworkDevice
}
pub struct StorageDevice {
pub mut:
id string // can be used in node
size_gb f64 // Size of the storage device in gigabytes
description string // Description of the storage device
}
pub struct MemoryDevice {
pub mut:
id string // can be used in node
size_gb f64 // Size of the memory device in gigabytes
description string // Description of the memory device
}
pub struct CPUDevice {
pub mut:
id string // can be used in node
cores int // Number of CPU cores
passmark int
description string // Description of the CPU
cpu_brand string // Brand of the CPU
cpu_version string // Version of the CPU
}
pub struct GPUDevice {
pub mut:
id string // can be used in node
cores int // Number of GPU cores
memory_gb f64 // Size of the GPU memory in gigabytes
description string // Description of the GPU
gpu_brand string
gpu_version string
}
pub struct NetworkDevice {
pub mut:
id string // can be used in node
speed_mbps int // Network speed in Mbps
description string // Description of the network device
}
// NodeCapacity represents the hardware capacity details of a node.
pub struct NodeCapacity {
pub mut:
storage_gb f64 // Total storage in gigabytes
mem_gb f64 // Total memory in gigabytes
mem_gb_gpu f64 // Total GPU memory in gigabytes
passmark int // Passmark score for the node
vcores int // Total virtual cores
}
// typically 1GB of memory, but can be adjusted based based on size of machine
pub struct ComputeSlice {
pub mut:
u16 int // the id of the slice in the node
mem_gb f64
storage_gb f64
passmark int
vcores int
cpu_oversubscription int
storage_oversubscription int
gpus u8 // nr of GPU's see node to know what GPU's are
}
// 1GB of storage
pub struct StorageSlice {
pub mut:
u16 int // the id of the slice in the node, are tracked in the node itself
}
fn (mut n Node) check() ! {
// todo calculate NodeCapacity out of the devices on the Node
}

View File

@@ -0,0 +1,33 @@
module datamodel
// is a root object, is the only obj farmer needs to configure in the UI, this defines how slices will be created
@[heap]
pub struct NodeGroup {
pub mut:
id u32
farmerid u32 // link back to farmer who owns the nodegroup, is a user?
secret string // only visible by farmer, in future encrypted, used to boot a node
description string
slapolicy SLAPolicy
pricingpolicy PricingPolicy
compute_slice_normalized_pricing_cc f64 // pricing in CC - cloud credit, per 2GB node slice
storage_slice_normalized_pricing_cc f64 // pricing in CC - cloud credit, per 1GB storage slice
signature_farmer string // signature as done by farmers to validate that they created this group
}
pub struct SLAPolicy {
pub mut:
sla_uptime int // should +90
sla_bandwidth_mbit int // minimal mbits we can expect avg over 1h per node, 0 means we don't guarantee
sla_penalty int // 0-100, percent of money given back in relation to month if sla breached, e.g. 200 means we return 2 months worth of rev if sla missed
}
pub struct PricingPolicy {
pub mut:
marketplace_year_discounts []int = [30, 40, 50] // e.g. 30,40,50 means if user has more CC in wallet than 1 year utilization on all his purchaes then this provider gives 30%, 2Y 40%, ...
// volume_discounts []int = [10, 20, 30] // e.g. 10,20,30
}

View File

@@ -0,0 +1,19 @@
@[heap]
pub struct NodeGroupReputation {
pub mut:
nodegroup_id u32
reputation int = 50 // between 0 and 100, earned over time
uptime int // between 0 and 100, set by system, farmer has no ability to set this
nodes []NodeReputation
}
pub struct NodeReputation {
pub mut:
node_id u32
reputation int = 50 // between 0 and 100, earned over time
uptime int // between 0 and 100, set by system, farmer has no ability to set this
}

View File

@@ -1,4 +1,4 @@
use heromodels_core::{Model, BaseModelData, IndexKey};
use heromodels_core::{BaseModelData, IndexKey, Model};
use heromodels_derive::model;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
@@ -297,5 +297,3 @@ impl DNSZone {
self
}
}

View File

@@ -1,4 +1,4 @@
use heromodels_core::{Model, BaseModelData, IndexKey};
use heromodels_core::{BaseModelData, IndexKey, Model};
use heromodels_derive::model;
use serde::{Deserialize, Serialize};
@@ -184,8 +184,6 @@ impl Group {
}
}
/// Represents the membership relationship between users and groups
#[model]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
@@ -232,5 +230,3 @@ impl UserGroupMembership {
self
}
}

View File

@@ -1,4 +1,4 @@
use heromodels_core::{Model, BaseModelData, IndexKey};
use heromodels_core::{BaseModelData, IndexKey, Model};
use heromodels_derive::model;
use serde::{Deserialize, Serialize};
@@ -111,5 +111,3 @@ impl Member {
self
}
}

View File

@@ -1,20 +1,10 @@
// Export all heroledger model modules
pub mod user;
pub mod group;
pub mod money;
pub mod membership;
pub mod dnsrecord;
pub mod group;
pub mod membership;
pub mod money;
pub mod rhai;
pub mod secretbox;
pub mod signature;
pub mod user;
pub mod user_kvs;
pub mod rhai;
// Re-export key types for convenience
pub use user::{User, UserStatus, UserProfile, KYCInfo, KYCStatus, SecretBox};
pub use group::{Group, UserGroupMembership, GroupStatus, Visibility, GroupConfig};
pub use money::{Account, Asset, AccountPolicy, AccountPolicyItem, Transaction, AccountStatus, TransactionType, Signature as TransactionSignature};
pub use membership::{Member, MemberRole, MemberStatus};
pub use dnsrecord::{DNSZone, DNSRecord, SOARecord, NameType, NameCat, DNSZoneStatus};
pub use secretbox::{Notary, NotaryStatus, SecretBoxCategory};
pub use signature::{Signature, SignatureStatus, ObjectType};
pub use user_kvs::{UserKVS, UserKVSItem};

View File

@@ -1,4 +1,4 @@
use heromodels_core::{Model, BaseModelData, IndexKey};
use heromodels_core::{BaseModelData, IndexKey, Model};
use heromodels_derive::model;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
@@ -223,8 +223,6 @@ impl Account {
}
}
/// Represents an asset in the financial system
#[model]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
@@ -342,8 +340,6 @@ impl Asset {
}
}
/// Represents account policies for various operations
#[model]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
@@ -400,8 +396,6 @@ impl AccountPolicy {
}
}
/// Represents a financial transaction
#[model]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
@@ -511,5 +505,3 @@ impl Transaction {
self
}
}

View File

@@ -1,8 +1,13 @@
use ::rhai::plugin::*;
use ::rhai::{Array, Dynamic, Engine, EvalAltResult, Map, Module};
use ::rhai::{Dynamic, Engine, EvalAltResult, Module};
use std::mem;
use crate::models::heroledger::*;
use crate::models::heroledger::{
dnsrecord::DNSZone,
group::{Group, Visibility},
money::Account,
user::{User, UserStatus},
};
// ============================================================================
// User Module
@@ -12,6 +17,8 @@ type RhaiUser = User;
#[export_module]
mod rhai_user_module {
use crate::models::heroledger::user::User;
use super::RhaiUser;
#[rhai_fn(name = "new_user", return_raw)]
@@ -30,30 +37,21 @@ mod rhai_user_module {
}
#[rhai_fn(name = "add_email", return_raw)]
pub fn add_email(
user: &mut RhaiUser,
email: String,
) -> Result<RhaiUser, Box<EvalAltResult>> {
pub fn add_email(user: &mut RhaiUser, email: String) -> Result<RhaiUser, Box<EvalAltResult>> {
let owned = std::mem::take(user);
*user = owned.add_email(email);
Ok(user.clone())
}
#[rhai_fn(name = "pubkey", return_raw)]
pub fn set_pubkey(
user: &mut RhaiUser,
pubkey: String,
) -> Result<RhaiUser, Box<EvalAltResult>> {
pub fn set_pubkey(user: &mut RhaiUser, pubkey: String) -> Result<RhaiUser, Box<EvalAltResult>> {
let owned = std::mem::take(user);
*user = owned.pubkey(pubkey);
Ok(user.clone())
}
#[rhai_fn(name = "status", return_raw)]
pub fn set_status(
user: &mut RhaiUser,
status: String,
) -> Result<RhaiUser, Box<EvalAltResult>> {
pub fn set_status(user: &mut RhaiUser, status: String) -> Result<RhaiUser, Box<EvalAltResult>> {
let status_enum = match status.as_str() {
"Active" => UserStatus::Active,
"Inactive" => UserStatus::Inactive,
@@ -115,10 +113,7 @@ mod rhai_group_module {
}
#[rhai_fn(name = "name", return_raw)]
pub fn set_name(
group: &mut RhaiGroup,
name: String,
) -> Result<RhaiGroup, Box<EvalAltResult>> {
pub fn set_name(group: &mut RhaiGroup, name: String) -> Result<RhaiGroup, Box<EvalAltResult>> {
let owned = std::mem::take(group);
*group = owned.name(name);
Ok(group.clone())
@@ -263,15 +258,11 @@ mod rhai_dns_zone_module {
Ok(zone.clone())
}
#[rhai_fn(name = "save_dns_zone", return_raw)]
pub fn save_dns_zone(zone: &mut RhaiDNSZone) -> Result<RhaiDNSZone, Box<EvalAltResult>> {
Ok(zone.clone())
}
// Getters
#[rhai_fn(name = "get_id")]
pub fn get_id(zone: &mut RhaiDNSZone) -> i64 {

View File

@@ -1,4 +1,4 @@
use heromodels_core::{Model, BaseModelData, IndexKey};
use heromodels_core::{BaseModelData, IndexKey, Model};
use heromodels_derive::model;
use serde::{Deserialize, Serialize};
@@ -138,5 +138,3 @@ impl Notary {
self
}
}

View File

@@ -1,4 +1,4 @@
use heromodels_core::{Model, BaseModelData, IndexKey};
use heromodels_core::{BaseModelData, IndexKey, Model};
use heromodels_derive::model;
use serde::{Deserialize, Serialize};
@@ -116,5 +116,3 @@ impl Signature {
self
}
}

View File

@@ -1,4 +1,4 @@
use heromodels_core::{Model, BaseModelData, IndexKey};
use heromodels_core::{BaseModelData, IndexKey, Model};
use heromodels_derive::model;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
@@ -366,5 +366,3 @@ impl User {
self
}
}

View File

@@ -1,7 +1,7 @@
use heromodels_core::{Model, BaseModelData, IndexKey};
use super::secretbox::SecretBox;
use heromodels_core::{BaseModelData, IndexKey, Model};
use heromodels_derive::model;
use serde::{Deserialize, Serialize};
use super::secretbox::SecretBox;
/// Represents a per-user key-value store
#[model]
@@ -44,8 +44,6 @@ impl UserKVS {
}
}
/// Represents an item in a user's key-value store
#[model]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
@@ -116,5 +114,3 @@ impl UserKVSItem {
self
}
}

View File

@@ -46,4 +46,4 @@ pub struct IdenfyVerificationData {
pub doc_issuing_country: Option<String>,
#[serde(rename = "manuallyDataChanged")]
pub manually_data_changed: Option<bool>,
}
}

View File

@@ -2,4 +2,4 @@
pub mod kyc;
pub use kyc::*;
pub use kyc::*;

View File

@@ -8,4 +8,4 @@ pub struct Address {
pub postal_code: String,
pub country: String,
pub company: Option<String>,
}
}

View File

@@ -10,16 +10,16 @@ pub mod contact;
pub mod finance;
pub mod flow;
pub mod governance;
pub mod grid4;
pub mod heroledger;
pub mod identity;
pub mod legal;
pub mod library;
pub mod location;
pub mod object;
pub mod projects;
pub mod payment;
pub mod identity;
pub mod tfmarketplace;
pub mod grid4;
pub mod projects;
// pub mod tfmarketplace;
// Re-export key types for convenience
pub use core::Comment;
@@ -39,3 +39,4 @@ pub use legal::{Contract, ContractRevision, ContractSigner, ContractStatus, Sign
pub use library::collection::Collection;
pub use library::items::{Image, Markdown, Pdf};
pub use projects::{Project, Status};
pub use heroledger::*;

View File

@@ -1,6 +1,6 @@
use super::Object;
use rhai::plugin::*;
use rhai::{CustomType, Dynamic, Engine, EvalAltResult, Module};
use super::Object;
type RhaiObject = Object;
@@ -16,10 +16,7 @@ pub mod generated_rhai_module {
/// Set the title of an Object
#[rhai_fn(name = "object_title")]
pub fn object_title(
object: &mut RhaiObject,
title: String,
) -> RhaiObject {
pub fn object_title(object: &mut RhaiObject, title: String) -> RhaiObject {
let mut result = object.clone();
result.title = title;
result
@@ -27,10 +24,7 @@ pub mod generated_rhai_module {
/// Set the description of an Object
#[rhai_fn(name = "object_description")]
pub fn object_description(
object: &mut RhaiObject,
description: String,
) -> RhaiObject {
pub fn object_description(object: &mut RhaiObject, description: String) -> RhaiObject {
let mut result = object.clone();
result.description = description;
result

View File

@@ -2,4 +2,4 @@
pub mod stripe;
pub use stripe::*;
pub use stripe::*;

View File

@@ -27,4 +27,4 @@ pub struct StripeEventData {
pub struct StripeEventRequest {
pub id: Option<String>,
pub idempotency_key: Option<String>,
}
}

View File

@@ -0,0 +1,345 @@
### 2.1 Accounts
* **id**: `BIGINT` identity (non-negative), unique account id
* **pubkey**: `BYTEA` unique public key for signing/encryption
* **display\_name**: `TEXT` (optional)
* **created\_at**: `TIMESTAMPTZ`
### 2.2 Currencies
* **asset\_code**: `TEXT` PK (e.g., `USDC-ETH`, `EUR`, `LND`)
* **name**: `TEXT`
* **symbol**: `TEXT`
* **decimals**: `INT` (default 2)
---
## 3) Services & Groups
### 3.1 Services
* **id**: `BIGINT` identity
* **name**: `TEXT` unique
* **description**: `TEXT`
* **default\_billing\_mode**: `ENUM('per_second','per_request')`
* **default\_price**: `NUMERIC(38,18)` (≥0)
* **default\_currency**: FK → `currencies(asset_code)`
* **max\_request\_seconds**: `INT` (>0 or `NULL`)
* **schema\_heroscript**: `TEXT`
* **schema\_json**: `JSONB`
* **created\_at**: `TIMESTAMPTZ`
#### Accepted Currencies (per service)
* **service\_id**: FK → `services(id)`
* **asset\_code**: FK → `currencies(asset_code)`
* **price\_override**: `NUMERIC(38,18)` (optional)
* **billing\_mode\_override**: `ENUM` (optional)
Primary key: `(service_id, asset_code)`
### 3.2 Service Groups
* **id**: `BIGINT` identity
* **name**: `TEXT` unique
* **description**: `TEXT`
* **created\_at**: `TIMESTAMPTZ`
#### Group Memberships
* **group\_id**: FK → `service_groups(id)`
* **service\_id**: FK → `services(id)`
Primary key: `(group_id, service_id)`
---
## 4) Providers & Runners
### 4.1 Service Providers
* **id**: `BIGINT` identity
* **account\_id**: FK → `accounts(id)` (the owning account)
* **name**: `TEXT` unique
* **description**: `TEXT`
* **created\_at**: `TIMESTAMPTZ`
#### Providers Offer Groups
* **provider\_id**: FK → `service_providers(id)`
* **group\_id**: FK → `service_groups(id)`
Primary key: `(provider_id, group_id)`
#### Provider Pricing Overrides (optional)
* **provider\_id**: FK → `service_providers(id)`
* **service\_id**: FK → `services(id)`
* **asset\_code**: FK → `currencies(asset_code)` (nullable for currency-agnostic override)
* **price\_override**: `NUMERIC(38,18)` (optional)
* **billing\_mode\_override**: `ENUM` (optional)
* **max\_request\_seconds\_override**: `INT` (optional)
Primary key: `(provider_id, service_id, asset_code)`
### 4.2 Runners
* **id**: `BIGINT` identity
* **address**: `INET` (must be IPv6)
* **name**: `TEXT`
* **description**: `TEXT`
* **pubkey**: `BYTEA` (optional)
* **created\_at**: `TIMESTAMPTZ`
#### Runner Ownership (many-to-many)
* **runner\_id**: FK → `runners(id)`
* **provider\_id**: FK → `service_providers(id)`
Primary key: `(runner_id, provider_id)`
#### Routing (provider → service/service\_group → runners)
* **provider\_service\_runners**: `(provider_id, service_id, runner_id)` PK
* **provider\_service\_group\_runners**: `(provider_id, group_id, runner_id)` PK
---
## 5) Subscriptions & Spend Control
A subscription authorizes an **account** to use either a **service** **or** a **service group**, with optional spend limits and allowed providers.
* **id**: `BIGINT` identity
* **account\_id**: FK → `accounts(id)`
* **service\_id** *xor* **group\_id**: FK (exactly one must be set)
* **secret**: `BYTEA` (random, provided by subscriber; recommend storing a hash)
* **subscription\_data**: `JSONB` (free-form)
* **limit\_amount**: `NUMERIC(38,18)` (optional)
* **limit\_currency**: FK → `currencies(asset_code)` (optional)
* **limit\_period**: `ENUM('hour','day','month')` (optional)
* **active**: `BOOLEAN` default `TRUE`
* **created\_at**: `TIMESTAMPTZ`
#### Allowed Providers per Subscription
* **subscription\_id**: FK → `subscriptions(id)`
* **provider\_id**: FK → `service_providers(id)`
Primary key: `(subscription_id, provider_id)`
**Intended Use:**
* Subscribers bound spending by amount/currency/period.
* Merchant (provider) can claim charges for requests fulfilled under an active subscription, within limits, and only if listed in `subscription_providers`.
---
## 6) Requests & Billing
### 6.1 Request Lifecycle
* **id**: `BIGINT` identity
* **account\_id**: FK → `accounts(id)`
* **subscription\_id**: FK → `subscriptions(id)`
* **provider\_id**: FK → `service_providers(id)`
* **service\_id**: FK → `services(id)`
* **runner\_id**: FK → `runners(id)` (nullable)
* **request\_schema**: `JSONB` (payload matching `schema_json`/`schema_heroscript`)
* **started\_at**, **ended\_at**: `TIMESTAMPTZ`
* **status**: `ENUM('pending','running','succeeded','failed','canceled')`
* **created\_at**: `TIMESTAMPTZ`
### 6.2 Billing Ledger (append-only)
* **id**: `BIGINT` identity
* **account\_id**: FK → `accounts(id)`
* **provider\_id**: FK → `service_providers(id)` (nullable)
* **service\_id**: FK → `services(id)` (nullable)
* **request\_id**: FK → `requests(id)` (nullable)
* **amount**: `NUMERIC(38,18)` (debit = positive, credit/refund = negative)
* **asset\_code**: FK → `currencies(asset_code)`
* **entry\_type**: `ENUM('debit','credit','adjustment')`
* **description**: `TEXT`
* **created\_at**: `TIMESTAMPTZ`
**Balances View (example):**
* `account_balances(account_id, asset_code, balance)` as a view over `billing_ledger`.
---
## 7) Pricing Precedence
When computing the **effective** pricing, billing mode, and max duration for a `(provider, service, currency)`:
1. **Provider override for (service, asset\_code)** — if present, use it.
2. **Service accepted currency override** — if present, use it.
3. **Service defaults** — fallback.
If `billing_mode` or `max_request_seconds` are not overridden at steps (1) or (2), inherit from the next step down.
---
## 8) Key Constraints & Validations
* All identity ids are non-negative (`CHECK (id >= 0)`).
* Runner IPv6 enforcement: `CHECK (family(address) = 6)`.
* Subscriptions must point to **exactly one** of `service_id` or `group_id`.
* Prices and limits must be non-negative if set.
* Unique natural keys where appropriate: service names, provider names, currency asset codes, account pubkeys.
---
## 9) Mermaid Diagrams
### 9.1 EntityRelationship Overview
```mermaid
erDiagram
ACCOUNTS ||--o{ SERVICE_PROVIDERS : "owns via account_id"
ACCOUNTS ||--o{ SUBSCRIPTIONS : has
CURRENCIES ||--o{ SERVICES : "default_currency"
CURRENCIES ||--o{ SERVICE_ACCEPTED_CURRENCIES : "asset_code"
CURRENCIES ||--o{ PROVIDER_SERVICE_OVERRIDES : "asset_code"
CURRENCIES ||--o{ BILLING_LEDGER : "asset_code"
SERVICES ||--o{ SERVICE_ACCEPTED_CURRENCIES : has
SERVICES ||--o{ SERVICE_GROUP_MEMBERS : member_of
SERVICE_GROUPS ||--o{ SERVICE_GROUP_MEMBERS : contains
SERVICE_PROVIDERS ||--o{ PROVIDER_SERVICE_GROUPS : offers
SERVICE_PROVIDERS ||--o{ PROVIDER_SERVICE_OVERRIDES : sets
SERVICE_PROVIDERS ||--o{ RUNNER_OWNERS : owns
SERVICE_PROVIDERS ||--o{ PROVIDER_SERVICE_RUNNERS : routes
SERVICE_PROVIDERS ||--o{ PROVIDER_SERVICE_GROUP_RUNNERS : routes
RUNNERS ||--o{ RUNNER_OWNERS : owned_by
RUNNERS ||--o{ PROVIDER_SERVICE_RUNNERS : executes
RUNNERS ||--o{ PROVIDER_SERVICE_GROUP_RUNNERS : executes
SUBSCRIPTIONS ||--o{ SUBSCRIPTION_PROVIDERS : allow
SERVICE_PROVIDERS ||--o{ SUBSCRIPTION_PROVIDERS : allowed
REQUESTS }o--|| ACCOUNTS : by
REQUESTS }o--|| SUBSCRIPTIONS : under
REQUESTS }o--|| SERVICE_PROVIDERS : via
REQUESTS }o--|| SERVICES : for
REQUESTS }o--o{ RUNNERS : executed_by
BILLING_LEDGER }o--|| ACCOUNTS : charges
BILLING_LEDGER }o--o{ SERVICES : reference
BILLING_LEDGER }o--o{ SERVICE_PROVIDERS : reference
BILLING_LEDGER }o--o{ REQUESTS : reference
```
### 9.2 Request Flow (Happy Path)
```mermaid
sequenceDiagram
autonumber
participant AC as Account
participant API as Broker/API
participant PR as Provider
participant RU as Runner
participant DB as PostgreSQL
AC->>API: Submit request (subscription_id, service_id, payload, secret)
API->>DB: Validate subscription (active, provider allowed, spend limits)
DB-->>API: OK + effective pricing (resolve precedence)
API->>PR: Dispatch request (service, payload)
PR->>DB: Select runner (provider_service_runners / group runners)
PR->>RU: Start job (payload)
RU-->>PR: Job started (started_at)
PR->>DB: Update REQUESTS (status=running, started_at)
RU-->>PR: Job finished (duration, result)
PR->>DB: Update REQUESTS (status=succeeded, ended_at)
API->>DB: Insert BILLING_LEDGER (debit per effective price)
DB-->>API: Ledger entry id
API-->>AC: Return result + charge info
```
### 9.3 Pricing Resolution
```mermaid
flowchart TD
A[Input: provider_id, service_id, asset_code] --> B{Provider override exists for (service, asset_code)?}
B -- Yes --> P1[Use provider price/mode/max]
B -- No --> C{Service accepted currency override exists?}
C -- Yes --> P2[Use service currency price/mode]
C -- No --> P3[Use service defaults]
P1 --> OUT[Effective pricing]
P2 --> OUT
P3 --> OUT
```
---
## 10) Operational Notes
* **Secrets:** store a hash (e.g., `digest(secret,'sha256')`) rather than raw `secret`. Keep the original only client-side.
* **Limits enforcement:** before insert of a debit ledger entry, compute period window (hour/day/month UTC or tenant TZ) and enforce `SUM(amount) + new_amount ≤ limit_amount`.
* **Durations:** enforce `max_request_seconds` (effective) at orchestration and/or via DB trigger on `REQUESTS` when transitioning to `running/succeeded`.
* **Routing:** prefer `provider_service_runners` when a request targets a service directly; otherwise use the union of runners from `provider_service_group_runners` for the group.
* **Balances:** serve balance queries via the `account_balances` view or a materialized cache updated by triggers/jobs.
---
## 11) Example Effective Pricing Query (sketch)
```sql
-- Inputs: :provider_id, :service_id, :asset_code
WITH p AS (
SELECT price_override, billing_mode_override, max_request_seconds_override
FROM provider_service_overrides
WHERE provider_id = :provider_id
AND service_id = :service_id
AND (asset_code = :asset_code)
),
sac AS (
SELECT price_override, billing_mode_override
FROM service_accepted_currencies
WHERE service_id = :service_id AND asset_code = :asset_code
),
svc AS (
SELECT default_price AS price, default_billing_mode AS mode, max_request_seconds
FROM services WHERE id = :service_id
)
SELECT
COALESCE(p.price_override, sac.price_override, svc.price) AS effective_price,
COALESCE(p.billing_mode_override, sac.billing_mode_override, svc.mode) AS effective_mode,
COALESCE(p.max_request_seconds_override, svc.max_request_seconds) AS effective_max_seconds;
```
---
## 12) Indices (non-exhaustive)
* `services(default_currency)`
* `service_accepted_currencies(service_id)`
* `provider_service_overrides(service_id, provider_id)`
* `requests(account_id)`, `requests(provider_id)`, `requests(service_id)`
* `billing_ledger(account_id, asset_code)`
* `subscriptions(account_id) WHERE active`
---
## 13) Migration & Compatibility
* Prefer additive migrations (new columns/tables) to avoid downtime.
* Use `ENUM` via `CREATE TYPE`; when extending, plan for `ALTER TYPE ... ADD VALUE`.
* For high-write ledgers, consider partitioning `billing_ledger` by `created_at` (monthly) and indexing partitions.
---
## 14) Non-Goals
* Wallet custody and on-chain settlement are out of scope.
* SLA tracking and detailed observability (metrics/log schema) are not part of this spec.
---
## 15) Acceptance Criteria
* Can represent services, groups, and providers with currency-specific pricing.
* Can route requests to runners by service or group.
* Can authorize usage via subscriptions, enforce spend limits, and record charges.
* Can reconstruct balances and audit via append-only ledger.
---
**End of Spec**

View File

@@ -0,0 +1,225 @@
# Concept Note: Generic Billing & Tracking Framework
## 1) Purpose
The model is designed to support a **flexible, generic, and auditable** billing environment that can be applied across diverse services and providers — from compute time billing to per-request API usage, across multiple currencies, with dynamic provider-specific overrides.
It is **not tied to a single business domain** — the same framework can be used for:
* Cloud compute time (per second)
* API transactions (per request)
* Data transfer charges
* Managed service subscriptions
* Brokered third-party service reselling
---
## 2) Key Concepts
### 2.1 Accounts
An **account** represents an economic actor in the system — typically a customer or a service provider.
* Identified by a **public key** (for authentication & cryptographic signing).
* Every billing action traces back to an account.
---
### 2.2 Currencies & Asset Codes
The system supports **multiple currencies** (crypto or fiat) via **asset codes**.
* Asset codes identify the unit of billing (e.g. `USDC-ETH`, `EUR`, `LND`).
* Currencies are **decoupled from services** so you can add or remove supported assets at any time.
---
### 2.3 Services & Groups
* **Service** = a billable offering (e.g., "Speech-to-Text", "VM Hosting").
* Has a **billing mode** (`per_second` or `per_request`).
* Has a **default price** and **default currency**.
* Supports **multiple accepted currencies** with optional per-currency pricing overrides.
* Has execution constraints (e.g. `max_request_seconds`).
* Includes structured schemas for request payloads.
* **Service Group** = a logical grouping of services.
* Groups make it easy to **bundle related services** and manage them together.
* Providers can offer entire groups rather than individual services.
---
### 2.4 Service Providers
A **service provider** is an **account** that offers services or service groups.
They can:
* Override **pricing** for their offered services (per currency).
* Route requests to their own **runners** (execution agents).
* Manage multiple **service groups** under one provider identity.
---
### 2.5 Runners
A **runner** is an execution agent — a node, VM, or service endpoint that can fulfill requests.
* Identified by an **IPv6 address** (supports Mycelium or other overlay networks).
* Can be owned by one or multiple providers.
* Providers map **services/groups → runners** to define routing.
---
### 2.6 Subscriptions
A **subscription** is **the authorization mechanism** for usage and spending control:
* Links an **account** to a **service** or **service group**.
* Defines **spending limits** (amount, currency, period: hour/day/month).
* Restricts which **providers** are allowed to serve the subscription.
* Uses a **secret** chosen by the subscriber — providers use this to claim charges.
---
### 2.7 Requests
A **request** represents a single execution under a subscription:
* Tied to **account**, **subscription**, **provider**, **service**, and optionally **runner**.
* Has **status** (`pending`, `running`, `succeeded`, `failed`, `canceled`).
* Records start/end times for duration-based billing.
---
### 2.8 Billing Ledger
The **ledger** is **append-only** — the source of truth for all charges and credits.
* Each entry records:
* `amount` (positive = debit, negative = credit/refund)
* `asset_code`
* Links to `account`, `provider`, `service`, and/or `request`
* From the ledger, **balances** can be reconstructed at any time.
---
## 3) How Billing Works — Step by Step
### 3.1 Setup
1. **Define services** with default pricing & schemas.
2. **Define currencies** and accepted currencies for services.
3. **Group services** into service groups.
4. **Onboard providers** (accounts) and associate them with service groups.
5. **Assign runners** to services or groups for execution routing.
---
### 3.2 Subscription Creation
1. Customer **creates a subscription**:
* Chooses service or service group.
* Sets **spending limit** (amount, currency, period).
* Chooses **secret**.
* Selects **allowed providers**.
2. Subscription is stored in DB.
---
### 3.3 Request Execution
1. Customer sends a request to broker/API with:
* `subscription_id`
* Target `service_id`
* Payload + signature using account pubkey.
2. Broker:
* Validates **subscription active**.
* Validates **provider allowed**.
* Checks **spend limit** hasnt been exceeded for current period.
* Resolves **effective price** via:
1. Provider override (currency-specific)
2. Service accepted currency override
3. Service default
3. Broker selects **runner** from providers routing tables.
4. Runner executes request and returns result.
---
### 3.4 Billing Entry
1. When the request completes:
* If `per_second` mode → calculate `duration × rate`.
* If `per_request` mode → apply flat rate.
2. Broker **inserts ledger entry**:
* Debit from customer account.
* Credit to provider account (can be separate entries or aggregated).
3. Ledger is append-only — historical billing cannot be altered.
---
### 3.5 Balance & Tracking
* **Current balances** are a sum of all ledger entries per account+currency.
* Spend limits are enforced by **querying the ledger** for the current period before each charge.
* Audit trails are guaranteed via immutable ledger entries.
---
## 4) Why This is Generic & Reusable
This design **decouples**:
* **Service definition** from **provider pricing** → multiple providers can sell the same service at different rates.
* **Execution agents** (runners) from **service definitions** → easy scaling or outsourcing of execution.
* **Billing rules** (per-second vs per-request) from **subscription limits** → same service can be sold in different billing modes.
* **Currencies** from the service → enabling multi-asset billing without changing the service definition.
Because of these separations, you can:
* Reuse the model for **compute**, **APIs**, **storage**, **SaaS features**, etc.
* Plug in different **payment backends** (on-chain, centralized payment processor, prepaid balance).
* Use the same model for **internal cost allocation** or **external customer billing**.
---
## 5) Potential Extensions
* **Prepaid model**: enforce that ledger debits cant exceed balance.
* **On-chain settlement**: periodically export ledger entries to blockchain transactions.
* **Discount models**: percentage or fixed-amount discounts per subscription.
* **Usage analytics**: aggregate requests/billing by time period, provider, or service.
* **SLAs**: link billing adjustments to performance metrics in requests.
---
## 6) Conceptual Diagram — Billing Flow
```mermaid
sequenceDiagram
participant C as Customer Account
participant B as Broker/API
participant P as Provider
participant R as Runner
participant DB as Ledger DB
C->>B: Request(service, subscription, payload, secret)
B->>DB: Validate subscription & spend limit
DB-->>B: OK + effective pricing
B->>P: Forward request
P->>R: Execute request
R-->>P: Result + execution time
P->>B: Return result
B->>DB: Insert debit (customer) + credit (provider)
DB-->>B: Ledger updated
B-->>C: Return result + charge info
```

View File

@@ -0,0 +1,234 @@
-- Enable useful extensions (optional)
CREATE EXTENSION IF NOT EXISTS pgcrypto; -- for digests/hashes if you want
CREATE EXTENSION IF NOT EXISTS btree_gist; -- for exclusion/partial indexes
-- =========================
-- Core: Accounts & Currency
-- =========================
CREATE TABLE accounts (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
pubkey BYTEA NOT NULL UNIQUE,
display_name TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
CHECK (id >= 0)
);
CREATE TABLE currencies (
asset_code TEXT PRIMARY KEY, -- e.g. "USDC-ETH", "EUR", "LND"
name TEXT NOT NULL,
symbol TEXT, -- e.g. "$", "€"
decimals INT NOT NULL DEFAULT 2, -- how many decimal places
UNIQUE (name)
);
-- =========================
-- Services & Groups
-- =========================
CREATE TYPE billing_mode AS ENUM ('per_second', 'per_request');
CREATE TABLE services (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
name TEXT NOT NULL UNIQUE,
description TEXT,
default_billing_mode billing_mode NOT NULL,
default_price NUMERIC(38, 18) NOT NULL, -- default price in "unit currency" (see accepted currencies)
default_currency TEXT NOT NULL REFERENCES currencies(asset_code) ON UPDATE CASCADE,
max_request_seconds INTEGER, -- nullable means no cap
schema_heroscript TEXT,
schema_json JSONB,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
CHECK (id >= 0),
CHECK (default_price >= 0),
CHECK (max_request_seconds IS NULL OR max_request_seconds > 0)
);
-- Accepted currencies for a service (subset + optional specific price per currency)
CREATE TABLE service_accepted_currencies (
service_id BIGINT NOT NULL REFERENCES services(id) ON DELETE CASCADE,
asset_code TEXT NOT NULL REFERENCES currencies(asset_code) ON UPDATE CASCADE,
price_override NUMERIC(38, 18), -- if set, overrides default_price for this currency
billing_mode_override billing_mode, -- if set, overrides default_billing_mode
PRIMARY KEY (service_id, asset_code),
CHECK (price_override IS NULL OR price_override >= 0)
);
CREATE TABLE service_groups (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
name TEXT NOT NULL UNIQUE,
description TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
CHECK (id >= 0)
);
CREATE TABLE service_group_members (
group_id BIGINT NOT NULL REFERENCES service_groups(id) ON DELETE CASCADE,
service_id BIGINT NOT NULL REFERENCES services(id) ON DELETE RESTRICT,
PRIMARY KEY (group_id, service_id)
);
-- =========================
-- Providers, Runners, Routing
-- =========================
CREATE TABLE service_providers (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
account_id BIGINT NOT NULL REFERENCES accounts(id) ON DELETE CASCADE, -- provider is an account
name TEXT NOT NULL,
description TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
UNIQUE (name),
CHECK (id >= 0)
);
-- Providers can offer groups (which imply their services)
CREATE TABLE provider_service_groups (
provider_id BIGINT NOT NULL REFERENCES service_providers(id) ON DELETE CASCADE,
group_id BIGINT NOT NULL REFERENCES service_groups(id) ON DELETE CASCADE,
PRIMARY KEY (provider_id, group_id)
);
-- Providers may set per-service overrides (price/mode/max seconds) (optionally per currency)
CREATE TABLE provider_service_overrides (
provider_id BIGINT NOT NULL REFERENCES service_providers(id) ON DELETE CASCADE,
service_id BIGINT NOT NULL REFERENCES services(id) ON DELETE CASCADE,
asset_code TEXT REFERENCES currencies(asset_code) ON UPDATE CASCADE,
price_override NUMERIC(38, 18),
billing_mode_override billing_mode,
max_request_seconds_override INTEGER,
PRIMARY KEY (provider_id, service_id, asset_code),
CHECK (price_override IS NULL OR price_override >= 0),
CHECK (max_request_seconds_override IS NULL OR max_request_seconds_override > 0)
);
-- Runners
CREATE TABLE runners (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
address INET NOT NULL, -- IPv6 (INET supports both IPv4/IPv6; require v6 via CHECK below if you like)
name TEXT NOT NULL,
description TEXT,
pubkey BYTEA, -- optional
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
UNIQUE (address),
CHECK (id >= 0),
CHECK (family(address) = 6) -- ensure IPv6
);
-- Runner ownership: a runner can be owned by multiple providers
CREATE TABLE runner_owners (
runner_id BIGINT NOT NULL REFERENCES runners(id) ON DELETE CASCADE,
provider_id BIGINT NOT NULL REFERENCES service_providers(id) ON DELETE CASCADE,
PRIMARY KEY (runner_id, provider_id)
);
-- Routing: link providers' services to specific runners
CREATE TABLE provider_service_runners (
provider_id BIGINT NOT NULL REFERENCES service_providers(id) ON DELETE CASCADE,
service_id BIGINT NOT NULL REFERENCES services(id) ON DELETE CASCADE,
runner_id BIGINT NOT NULL REFERENCES runners(id) ON DELETE CASCADE,
PRIMARY KEY (provider_id, service_id, runner_id)
);
-- Routing: link providers' service groups to runners
CREATE TABLE provider_service_group_runners (
provider_id BIGINT NOT NULL REFERENCES service_providers(id) ON DELETE CASCADE,
group_id BIGINT NOT NULL REFERENCES service_groups(id) ON DELETE CASCADE,
runner_id BIGINT NOT NULL REFERENCES runners(id) ON DELETE CASCADE,
PRIMARY KEY (provider_id, group_id, runner_id)
);
-- =========================
-- Subscriptions & Spend Control
-- =========================
CREATE TYPE spend_period AS ENUM ('hour', 'day', 'month');
-- A subscription ties an account to a specific service OR a service group, with spend limits and allowed providers
CREATE TABLE subscriptions (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
account_id BIGINT NOT NULL REFERENCES accounts(id) ON DELETE CASCADE,
service_id BIGINT REFERENCES services(id) ON DELETE CASCADE,
group_id BIGINT REFERENCES service_groups(id) ON DELETE CASCADE,
secret BYTEA NOT NULL, -- caller-chosen secret (consider storing a hash instead)
subscription_data JSONB, -- arbitrary client-supplied info
limit_amount NUMERIC(38, 18), -- allowed spend in the selected currency per period
limit_currency TEXT REFERENCES currencies(asset_code) ON UPDATE CASCADE,
limit_period spend_period, -- period for the limit
active BOOLEAN NOT NULL DEFAULT TRUE,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
-- Ensure exactly one of service_id or group_id
CHECK ( (service_id IS NOT NULL) <> (group_id IS NOT NULL) ),
CHECK (limit_amount IS NULL OR limit_amount >= 0),
CHECK (id >= 0)
);
-- Providers that are allowed to serve under a subscription
CREATE TABLE subscription_providers (
subscription_id BIGINT NOT NULL REFERENCES subscriptions(id) ON DELETE CASCADE,
provider_id BIGINT NOT NULL REFERENCES service_providers(id) ON DELETE CASCADE,
PRIMARY KEY (subscription_id, provider_id)
);
-- =========================
-- Usage, Requests & Billing
-- =========================
-- A request lifecycle record (optional but useful for auditing and max duration enforcement)
CREATE TYPE request_status AS ENUM ('pending', 'running', 'succeeded', 'failed', 'canceled');
CREATE TABLE requests (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
account_id BIGINT NOT NULL REFERENCES accounts(id) ON DELETE CASCADE,
subscription_id BIGINT NOT NULL REFERENCES subscriptions(id) ON DELETE RESTRICT,
provider_id BIGINT NOT NULL REFERENCES service_providers(id) ON DELETE RESTRICT,
service_id BIGINT NOT NULL REFERENCES services(id) ON DELETE RESTRICT,
runner_id BIGINT REFERENCES runners(id) ON DELETE SET NULL,
request_schema JSONB, -- concrete task payload (conforms to schema_json/heroscript)
started_at TIMESTAMPTZ,
ended_at TIMESTAMPTZ,
status request_status NOT NULL DEFAULT 'pending',
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
CHECK (id >= 0),
CHECK (ended_at IS NULL OR started_at IS NULL OR ended_at >= started_at)
);
-- Billing ledger (debits/credits). Positive amount = debit to account (charge). Negative = credit/refund.
CREATE TYPE ledger_entry_type AS ENUM ('debit', 'credit', 'adjustment');
CREATE TABLE billing_ledger (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
account_id BIGINT NOT NULL REFERENCES accounts(id) ON DELETE CASCADE,
provider_id BIGINT REFERENCES service_providers(id) ON DELETE SET NULL,
service_id BIGINT REFERENCES services(id) ON DELETE SET NULL,
request_id BIGINT REFERENCES requests(id) ON DELETE SET NULL,
amount NUMERIC(38, 18) NOT NULL, -- positive for debit, negative for credit
asset_code TEXT NOT NULL REFERENCES currencies(asset_code) ON UPDATE CASCADE,
entry_type ledger_entry_type NOT NULL,
description TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
CHECK (id >= 0)
);
-- Optional: running balances per account/currency (materialized view or real-time view)
-- This is a plain view; for performance, you might maintain a cached table.
CREATE VIEW account_balances AS
SELECT
account_id,
asset_code,
SUM(amount) AS balance
FROM billing_ledger
GROUP BY account_id, asset_code;
-- =========================
-- Helpful Indexes
-- =========================
CREATE INDEX idx_services_default_currency ON services(default_currency);
CREATE INDEX idx_service_accepted_currencies_service ON service_accepted_currencies(service_id);
CREATE INDEX idx_provider_overrides_service ON provider_service_overrides(service_id);
CREATE INDEX idx_requests_account ON requests(account_id);
CREATE INDEX idx_requests_provider ON requests(provider_id);
CREATE INDEX idx_requests_service ON requests(service_id);
CREATE INDEX idx_billing_account_currency ON billing_ledger(account_id, asset_code);
CREATE INDEX idx_subscriptions_account_active ON subscriptions(account_id) WHERE active;

View File

@@ -0,0 +1,266 @@
# Billing Logic — Whiteboard Version (for Devs)
## 1) Inputs You Always Need
* `account_id`, `subscription_id`
* `service_id` (or group → resolved to a service at dispatch)
* `provider_id`, `asset_code`
* `payload` (validated against service schema)
* (Optional) `runner_id`
* Idempotency key for the request (client-provided)
---
## 2) Gatekeeping (Hard Checks)
1. **Subscription**
* Must be `active`.
* Must target **exactly one** of {service, group}.
* If group: ensure `service_id` is a member.
2. **Provider Allowlist**
* If `subscription_providers` exists → `provider_id` must be listed.
3. **Spend Limit** (if set)
* Compute window by `limit_period` (`hour`/`day`/`month`, UTC unless tenant TZ).
* Current period spend = `SUM(ledger.amount WHERE account & currency & period)`.
* `current_spend + estimated_charge ≤ limit_amount`.
4. **Max Duration** (effective; see §3):
* If billing mode is `per_second`, reject if requested/max exceeds effective cap.
---
## 3) Effective Pricing (Single Resolution Function)
Inputs: `provider_id`, `service_id`, `asset_code`
Precedence:
1. `provider_service_overrides` for `(service_id, asset_code)`
2. `service_accepted_currencies` for `(service_id, asset_code)`
3. `services` defaults
Outputs:
* `effective_billing_mode ∈ {per_request, per_second}`
* `effective_price` (NUMERIC)
* `effective_max_request_seconds` (nullable)
---
## 4) Request Lifecycle (States)
* `pending``running` → (`succeeded` | `failed` | `canceled`)
* Timestamps: set `started_at` on `running`, `ended_at` on terminal states.
* Enforce `ended_at ≥ started_at` and `duration ≤ effective_max_request_seconds` (if set).
---
## 5) Charging Rules
### A) Per Request
```
charge = effective_price
```
### B) Per Second
```
duration_seconds = ceil(extract(epoch from (ended_at - started_at)))
charge = duration_seconds * effective_price
```
* Cap with `effective_max_request_seconds` if present.
* If ended early/failed before `started_at`: charge = 0.
---
## 6) Idempotency & Atomicity
* **Idempotency key** per `(account_id, subscription_id, provider_id, service_id, request_external_id)`; store on `requests` and enforce unique index.
* **Single transaction** to:
1. finalize `REQUESTS` status + timestamps,
2. insert **one** debit entry into `billing_ledger`.
* Never mutate ledger entries; use compensating **credit** entries for adjustments/refunds.
---
## 7) Spend-Limit Enforcement (Before Charging)
Pseudocode (SQL-ish):
```sql
WITH window AS (
SELECT tsrange(period_start(:limit_period), period_end(:limit_period)) AS w
),
spent AS (
SELECT COALESCE(SUM(amount), 0) AS total
FROM billing_ledger, window
WHERE account_id = :account_id
AND asset_code = :asset_code
AND created_at <@ (SELECT w FROM window)
),
check AS (
SELECT (spent.total + :estimated_charge) <= :limit_amount AS ok FROM spent
)
SELECT ok FROM check;
```
* If not ok → reject before dispatch, or allow but **set hard cap** on max seconds and auto-stop at limit.
---
## 8) Suggested DB Operations (Happy Path)
1. **Create request**
```sql
INSERT INTO requests (...)
VALUES (...)
ON CONFLICT (idempotency_key) DO NOTHING
RETURNING id;
```
2. **Start execution**
```sql
UPDATE requests
SET status='running', started_at=now()
WHERE id=:id AND status='pending';
```
3. **Finish & bill** (single transaction)
```sql
BEGIN;
-- lock for update to avoid double-billing
UPDATE requests
SET status=:final_status, ended_at=now()
WHERE id=:id AND status='running'
RETURNING started_at, ended_at;
-- compute charge in app (see §5), re-check spend window here
INSERT INTO billing_ledger (
account_id, provider_id, service_id, request_id,
amount, asset_code, entry_type, description
) VALUES (
:account_id, :provider_id, :service_id, :id,
:charge, :asset_code, 'debit', :desc
);
COMMIT;
```
---
## 9) Balances & Reporting
* **Current balance** = `SUM(billing_ledger.amount) GROUP BY account_id, asset_code`.
* Keep a **view** or **materialized view**; refresh asynchronously if needed.
* Never rely on cached balance for hard checks — re-check within the billing transaction if **prepaid** semantics are required.
---
## 10) Error & Edge Rules
* If runner fails before `running` → no charge.
* If runner starts, then fails:
* **per\_second**: bill actual seconds (can be 0).
* **per\_request**: default is **no charge** unless policy says otherwise; if charging partials, document it.
* Partial refunds/adjustments → insert **negative** ledger entries (type `credit`/`adjustment`) tied to the original `request_id`.
---
## 11) Minimal Pricing Resolver (Sketch)
```sql
WITH p AS (
SELECT price_override AS price,
billing_mode_override AS mode,
max_request_seconds_override AS maxsec
FROM provider_service_overrides
WHERE provider_id = :pid AND service_id = :sid AND asset_code = :asset
LIMIT 1
),
sac AS (
SELECT price_override AS price,
billing_mode_override AS mode
FROM service_accepted_currencies
WHERE service_id = :sid AND asset_code = :asset
LIMIT 1
),
svc AS (
SELECT default_price AS price,
default_billing_mode AS mode,
max_request_seconds AS maxsec
FROM services WHERE id = :sid
)
SELECT
COALESCE(p.price, sac.price, svc.price) AS price,
COALESCE(p.mode, sac.mode, svc.mode) AS mode,
COALESCE(p.maxsec, svc.maxsec) AS max_seconds;
```
---
## 12) Mermaid — Decision Trees
### Pricing & Duration
```mermaid
flowchart TD
A[provider_id, service_id, asset_code] --> B{Provider override exists?}
B -- yes --> P[Use provider price/mode/max]
B -- no --> C{Service currency override?}
C -- yes --> S[Use service currency price/mode]
C -- no --> D[Use service defaults]
P --> OUT[effective price/mode/max]
S --> OUT
D --> OUT
```
### Spend Check & Charge
```mermaid
flowchart TD
S[Has subscription limit?] -->|No| D1[Dispatch]
S -->|Yes| C{current_spend + est_charge <= limit?}
C -->|No| REJ[Reject or cap duration]
C -->|Yes| D1[Dispatch]
D1 --> RUN[Run request]
RUN --> DONE[Finalize + insert ledger]
```
---
## 13) Security Posture
* Store **hash of subscription secret**; compare hash on use.
* Sign client requests with **account pubkey**; verify before dispatch.
* Limit **request schema** to validated fields; reject unknowns.
* Enforce **IPv6** for runners where required.
---
## 14) What To Implement First
1. Pricing resolver (single function).
2. Spend-window checker (single query).
3. Request lifecycle + idempotency.
4. Ledger write (append-only) + balances view.
Everything else layers on top.
---
If you want, I can turn this into a small **README.md** with code blocks you can paste into the repo (plus a couple of SQL functions and example tests).

View File

@@ -24,16 +24,6 @@ pub enum CurrencyType {
custom
}
pub struct Price {
pub mut:
base_amount f64 // Using f64 for Decimal
base_currency string
display_currency string
display_amount f64 // Using f64 for Decimal
formatted_display string
conversion_rate f64 // Using f64 for Decimal
conversion_timestamp u64 // Unix timestamp
}
pub struct MarketplaceCurrencyConfig {
pub mut:

Some files were not shown because too many files have changed in this diff Show More