harmoniis-wallet 0.1.72

Smart-contract wallet for the Harmoniis marketplace for agents and robots (RGB contracts, Witness-backed bearer state, Webcash fees)
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
//! Vast.ai REST API client.
//!
//! Reference: <https://docs.vast.ai/api-reference/introduction>
//! Auth: `Authorization: Bearer {api_key}` on all requests.

use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use serde_json::{json, Value};

const API_BASE: &str = "https://console.vast.ai/api/v0";

pub struct VastClient {
    api_key: String,
    http: reqwest::Client,
}

/// A GPU offer from Vast.ai search.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Offer {
    pub id: u64,
    pub gpu_name: String,
    pub num_gpus: u32,
    pub total_flops: f64,
    pub gpu_mem_bw: f64,
    pub dph_total: f64,
    pub reliability: f64,
    pub cuda_max_good: f64,
    pub inet_down: f64,
    pub flops_per_dphtotal: f64,
}

impl Offer {
    pub fn tflops(&self) -> f64 {
        self.total_flops
    }

    /// SHA256 mining is compute-limited on older GPUs (Pascal) and
    /// bandwidth-limited on newer GPUs (Ampere/Ada). Effective hashrate
    /// is min(compute_ceiling, bandwidth_ceiling).
    ///
    /// Calibrated from measured data (2026-04-14, latest optimized build):
    ///   RTX 4090 (Ada):       81 TF, 898 GB/s → 14.5 GH/s (BW-limited)
    ///   RTX 4070 (Ada):       29 TF, 440 GB/s →  5.9 GH/s (BW-limited)
    ///   Titan X Pascal:       12 TF, 338 GB/s →  3.2 GH/s (compute-limited)
    ///   2x Titan Xp (Pascal): 24 TF, 411 GB/s →  6.0 GH/s (compute-limited)
    const GHS_PER_TFLOP: f64 = 0.267; // compute ceiling (3.2 / 12.0)
    const GHS_PER_MEM_BW: f64 = 0.016; // bandwidth ceiling (14.5 / 898)

    /// Estimated SHA256 hash rate — min(compute, bandwidth) per GPU × num_gpus.
    pub fn estimated_hashrate_ghs(&self) -> f64 {
        let per_gpu_compute = self.total_flops / self.num_gpus.max(1) as f64 * Self::GHS_PER_TFLOP;
        let per_gpu_bw = self.gpu_mem_bw * Self::GHS_PER_MEM_BW;
        let per_gpu = per_gpu_compute.min(per_gpu_bw);
        per_gpu * self.num_gpus as f64
    }

    /// Webcash.org processes mining reports sequentially at ~6s each
    /// (single-threaded Tornado). Multiple connections cascade-queue.
    const REPORT_SECONDS: f64 = 6.0;

    pub fn max_solutions_per_sec() -> f64 {
        1.0 / Self::REPORT_SECONDS
    }

    pub fn max_useful_hashrate_ghs(difficulty: u32) -> f64 {
        Self::max_solutions_per_sec() * 2.0_f64.powi(difficulty as i32) / 1e9
    }

    /// Max useful GPU memory bandwidth (GB/s, total across all GPUs) at difficulty.
    pub fn max_useful_mem_bw(difficulty: u32) -> f64 {
        Self::max_useful_hashrate_ghs(difficulty) / Self::GHS_PER_MEM_BW
    }

    pub fn estimated_solutions_per_sec(&self, difficulty: u32) -> f64 {
        self.estimated_hashrate_ghs() * 1e9 / 2.0_f64.powi(difficulty as i32)
    }

    pub fn exceeds_capacity(&self, difficulty: u32) -> bool {
        self.estimated_hashrate_ghs() > Self::max_useful_hashrate_ghs(difficulty) * 1.2
    }

    /// Best value: hashrate per dollar, capped at reporting capacity.
    /// Score = 0 for overcapacity (excluded from selection).
    pub fn capacity_score(&self, difficulty: u32) -> f64 {
        if self.dph_total <= 0.0 {
            return 0.0;
        }
        if self.exceeds_capacity(difficulty) {
            return 0.0;
        }
        // GH/s per dollar — how much useful mining per hour per dollar
        self.estimated_hashrate_ghs() / self.dph_total
    }
}

/// Instance details from Vast.ai.
#[derive(Debug, Clone, Deserialize)]
pub struct Instance {
    #[serde(default)]
    pub id: u64,
    /// Vast.ai API returns status in different fields depending on context.
    /// `actual_status` is the documented field, `cur_state` is the actual one.
    pub actual_status: Option<String>,
    /// The real status field in the Vast.ai API response.
    pub cur_state: Option<String>,
    pub status_msg: Option<String>,
    pub ssh_host: Option<String>,
    pub ssh_port: Option<u16>,
    pub public_ipaddr: Option<String>,
    pub gpu_name: Option<String>,
    pub num_gpus: Option<u32>,
    pub dph_total: Option<f64>,
    pub ports: Option<Value>,
}

impl Instance {
    /// Extract the SSH host and port from instance data.
    pub fn ssh_connection(&self) -> Option<(String, u16)> {
        // Try ports map first (direct port mapping)
        if let Some(ports) = &self.ports {
            if let Some(tcp22) = ports.get("22/tcp") {
                if let Some(arr) = tcp22.as_array() {
                    if let Some(entry) = arr.first() {
                        if let Some(host_port) = entry.get("HostPort").and_then(|v| v.as_str()) {
                            if let Ok(port) = host_port.parse::<u16>() {
                                let host = self
                                    .public_ipaddr
                                    .clone()
                                    .unwrap_or_else(|| "localhost".to_string());
                                return Some((host, port));
                            }
                        }
                    }
                }
            }
        }
        // Fallback to ssh_host/ssh_port
        if let (Some(host), Some(port)) = (&self.ssh_host, self.ssh_port) {
            return Some((host.clone(), port));
        }
        None
    }

    pub fn is_running(&self) -> bool {
        self.status().as_deref() == Some("running")
    }

    /// Effective status: prefers `cur_state` (actual API field), falls back to `actual_status`.
    pub fn status(&self) -> Option<String> {
        self.cur_state
            .clone()
            .or_else(|| self.actual_status.clone())
    }
}

impl VastClient {
    pub fn new(api_key: &str) -> Self {
        Self {
            api_key: api_key.to_string(),
            http: reqwest::Client::new(),
        }
    }

    fn auth_header(&self) -> String {
        format!("Bearer {}", self.api_key)
    }

    /// Search for GPU offers — any GPU count, sorted by TFlops/$/hr, reliability >= 99.5%.
    pub async fn search_offers(&self, limit: u32) -> Result<Vec<Offer>> {
        let body = json!({
            "num_gpus": {"gte": 1},
            "cuda_max_good": {"gte": 12.0},
            "verified": {"eq": true},
            "rentable": {"eq": true},
            "rented": {"eq": false},
            "reliability": {"gte": 0.995},
            "inet_down": {"gte": 200},
            "duration": {"gte": 86400.0},
            "order": [["flops_per_dphtotal", "desc"]],
            "limit": limit,
            "type": "on-demand"
        });

        let resp = self
            .http
            .post(format!("{API_BASE}/bundles/"))
            .header("Authorization", self.auth_header())
            .json(&body)
            .send()
            .await
            .context("Vast.ai search request failed")?;

        let status = resp.status();
        let text = resp.text().await?;
        if !status.is_success() {
            anyhow::bail!("Vast.ai search failed (HTTP {status}): {text}");
        }

        let data: Value = serde_json::from_str(&text)?;
        let offers_raw = data
            .get("offers")
            .and_then(|v| v.as_array())
            .cloned()
            .unwrap_or_default();

        let mut offers = Vec::new();
        for raw in offers_raw {
            let id = raw.get("id").and_then(|v| v.as_u64()).unwrap_or(0);
            let gpu_name = raw
                .get("gpu_name")
                .and_then(|v| v.as_str())
                .unwrap_or("Unknown")
                .to_string();
            let num_gpus = raw.get("num_gpus").and_then(|v| v.as_u64()).unwrap_or(0) as u32;
            let total_flops = raw
                .get("total_flops")
                .and_then(|v| v.as_f64())
                .unwrap_or(0.0);
            let dph_total = raw.get("dph_total").and_then(|v| v.as_f64()).unwrap_or(0.0);
            let reliability = raw
                .get("reliability2")
                .or_else(|| raw.get("reliability"))
                .and_then(|v| v.as_f64())
                .unwrap_or(0.0);
            let cuda_max_good = raw
                .get("cuda_max_good")
                .and_then(|v| v.as_f64())
                .unwrap_or(0.0);
            let inet_down = raw.get("inet_down").and_then(|v| v.as_f64()).unwrap_or(0.0);
            let flops_per_dphtotal = raw
                .get("flops_per_dphtotal")
                .and_then(|v| v.as_f64())
                .unwrap_or(0.0);
            let gpu_mem_bw = raw
                .get("gpu_mem_bw")
                .and_then(|v| v.as_f64())
                .unwrap_or(0.0);

            offers.push(Offer {
                id,
                gpu_name,
                num_gpus,
                total_flops,
                gpu_mem_bw,
                dph_total,
                reliability,
                cuda_max_good,
                inet_down,
                flops_per_dphtotal,
            });
        }
        Ok(offers)
    }

    /// Find top offers — any GPU count, reliability >= 99.5%, sorted by TFlops/$/hr.
    pub async fn find_best_offers(&self) -> Result<Vec<Offer>> {
        let mut candidates = self.search_offers(40).await?;

        // Sort by GH/s per dollar (best mining value).
        candidates.sort_by(|a, b| {
            let score_a = if a.dph_total > 0.0 {
                a.estimated_hashrate_ghs() / a.dph_total
            } else {
                0.0
            };
            let score_b = if b.dph_total > 0.0 {
                b.estimated_hashrate_ghs() / b.dph_total
            } else {
                0.0
            };
            score_b
                .partial_cmp(&score_a)
                .unwrap_or(std::cmp::Ordering::Equal)
        });

        candidates.truncate(20);
        Ok(candidates)
    }

    /// Create an instance from an offer ID using the CUDA 12 Docker image.
    pub async fn create_instance(&self, offer_id: u64, onstart_script: &str) -> Result<u64> {
        let body = json!({
            "client_id": "me",
            "image": "nvidia/cuda:12.0.1-devel-ubuntu20.04",
            "template_hash_id": "fd2e982e4facaf7b2918006939d1e06e",
            "disk": 16,
            "label": "hrmw-cloud-mining",
            "onstart": onstart_script,
        });

        let resp = self
            .http
            .put(format!("{API_BASE}/asks/{offer_id}/"))
            .header("Authorization", self.auth_header())
            .json(&body)
            .send()
            .await
            .context("Vast.ai create instance failed")?;

        let status = resp.status();
        let text = resp.text().await?;
        if !status.is_success() {
            anyhow::bail!("Vast.ai create instance failed (HTTP {status}): {text}");
        }

        let data: Value = serde_json::from_str(&text)?;
        let instance_id = data
            .get("new_contract")
            .and_then(|v| v.as_u64())
            .ok_or_else(|| anyhow::anyhow!("No instance ID in response: {text}"))?;

        Ok(instance_id)
    }

    /// Get instance details.
    pub async fn get_instance(&self, instance_id: u64) -> Result<Instance> {
        let resp = self
            .http
            .get(format!("{API_BASE}/instances/{instance_id}/?owner=me"))
            .header("Authorization", self.auth_header())
            .send()
            .await
            .context("Vast.ai get instance failed")?;

        let status = resp.status();
        let text = resp.text().await?;
        if !status.is_success() {
            anyhow::bail!("Vast.ai get instance failed (HTTP {status}): {text}");
        }

        let data: Value = serde_json::from_str(&text)?;
        // Response is { "instances": { ...fields... } } — the object IS the instance
        let instance_val = if let Some(inner) = data.get("instances") {
            if inner.is_object() && inner.get("actual_status").is_some() {
                // "instances" is the instance object itself
                let mut obj = inner.clone();
                // Inject "id" from the URL if missing
                if obj.get("id").is_none() {
                    obj["id"] = serde_json::json!(instance_id);
                }
                obj
            } else if inner.is_array() {
                let mut first = inner
                    .as_array()
                    .and_then(|a| a.first().cloned())
                    .unwrap_or(data.clone());
                if first.get("id").is_none() {
                    first["id"] = serde_json::json!(instance_id);
                }
                first
            } else {
                data.clone()
            }
        } else {
            data
        };

        let instance: Instance = serde_json::from_value(instance_val)?;
        Ok(instance)
    }

    /// Destroy an instance.
    pub async fn destroy_instance(&self, instance_id: u64) -> Result<()> {
        let resp = self
            .http
            .delete(format!("{API_BASE}/instances/{instance_id}/"))
            .header("Authorization", self.auth_header())
            .json(&json!({}))
            .send()
            .await
            .context("Vast.ai destroy instance failed")?;

        let status = resp.status();
        if !status.is_success() {
            let text = resp.text().await?;
            anyhow::bail!("Vast.ai destroy failed (HTTP {status}): {text}");
        }
        Ok(())
    }

    /// Restart a stopped/exited instance.
    pub async fn restart_instance(&self, instance_id: u64) -> Result<()> {
        let resp = self
            .http
            .put(format!("{API_BASE}/instances/{instance_id}/"))
            .header("Authorization", self.auth_header())
            .json(&json!({"state": "running"}))
            .send()
            .await
            .context("Vast.ai restart instance failed")?;

        let status = resp.status();
        if !status.is_success() {
            let text = resp.text().await?;
            anyhow::bail!("Vast.ai restart failed (HTTP {status}): {text}");
        }
        Ok(())
    }

    /// Upload an SSH public key to the account.
    pub async fn upload_ssh_key(&self, pubkey: &str) -> Result<()> {
        let resp = self
            .http
            .post(format!("{API_BASE}/ssh/"))
            .header("Authorization", self.auth_header())
            .json(&json!({ "ssh_key": pubkey }))
            .send()
            .await
            .context("Vast.ai SSH key upload failed")?;

        let status = resp.status();
        if !status.is_success() {
            let text = resp.text().await?;
            // Ignore "already exists" errors
            if !text.contains("already") {
                anyhow::bail!("Vast.ai SSH key upload failed (HTTP {status}): {text}");
            }
        }
        Ok(())
    }
}