Skip to main content

agentstatedeveloper_core/
cross_service.rs

1//! Cross-service / cross-process edges (Plan competitive-harvest t-002).
2//!
3//! Call edges link two symbols by qname *within one repo*. Cross-service edges
4//! are different: a client call (`requests.post("/charge")`) in one repo and a
5//! route handler (`@app.post("/charge")`) in another can't be linked by qname —
6//! they're linked by a shared **contract**: the HTTP method+path, the pub-sub
7//! topic name. We therefore keep `symbol_id` repo-scoped and unchanged, and add
8//! a separate contract-keyed layer:
9//!
10//!   - Each repo detects the endpoints it *exposes* (inbound: routes/listeners)
11//!     and *consumes* (outbound: client calls/emits), normalizes each to a
12//!     [`ServiceEndpoint`] with a stable `contract` key and a `repo_id`.
13//!   - A repo exports its endpoints as a [`ServiceManifest`]; other repos import
14//!     manifests to match contracts cross-repo (federated — no shared DB, no
15//!     global symbol identity).
16//!   - [`match_edges`] pairs outbound→inbound endpoints sharing a contract key
17//!     into [`CrossServiceEdge`]s. Matching is by string equality on the
18//!     normalized contract, so the normalizers below are load-bearing: a client
19//!     and a server must phrase the same endpoint identically.
20//!
21//! HTTP and pub-sub share this machinery. Data-flow edges are a distinct
22//! intra-process mechanism (arg→param / field chains) tracked separately.
23
24use serde::{Deserialize, Serialize};
25
26/// Transport a service endpoint communicates over.
27#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
28#[serde(rename_all = "snake_case")]
29pub enum Transport {
30    Http,
31    PubSub,
32    /// Intra-process data-flow (distinct mechanism; see module docs).
33    DataFlow,
34}
35
36/// Whether an endpoint exposes/handles a contract (inbound) or consumes/calls
37/// it (outbound).
38#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
39#[serde(rename_all = "snake_case")]
40pub enum Direction {
41    /// Handles/serves/listens — the producer side (e.g. a route handler).
42    Inbound,
43    /// Calls/emits — the consumer side (e.g. an HTTP client call).
44    Outbound,
45}
46
47/// A detected service-boundary point owned by a local symbol.
48#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
49pub struct ServiceEndpoint {
50    pub transport: Transport,
51    pub direction: Direction,
52    /// Normalized contract key, e.g. `"http:POST /users/{}"` or
53    /// `"topic:payments.charged"`. Built via [`http_contract`] / [`pubsub_contract`].
54    pub contract: String,
55    pub repo_id: String,
56    pub symbol_id: String,
57    pub qname: String,
58    pub file: String,
59    pub line: u32,
60    /// Detection confidence in `[0, 1]`: a literal route/url is high; a
61    /// templated or dynamically-built target is lower.
62    pub confidence: f64,
63    #[serde(default, skip_serializing_if = "Option::is_none")]
64    pub note: Option<String>,
65}
66
67/// A lightweight reference to one end of a cross-service edge.
68#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
69pub struct EndpointRef {
70    pub repo_id: String,
71    pub symbol_id: String,
72    pub qname: String,
73    pub file: String,
74    pub line: u32,
75}
76
77impl From<&ServiceEndpoint> for EndpointRef {
78    fn from(e: &ServiceEndpoint) -> Self {
79        EndpointRef {
80            repo_id: e.repo_id.clone(),
81            symbol_id: e.symbol_id.clone(),
82            qname: e.qname.clone(),
83            file: e.file.clone(),
84            line: e.line,
85        }
86    }
87}
88
89/// A matched cross-service edge: an outbound endpoint linked to an inbound
90/// endpoint sharing a contract key. Computed on demand, not materialized.
91#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
92pub struct CrossServiceEdge {
93    pub transport: Transport,
94    pub contract: String,
95    /// The consumer (outbound) side.
96    pub from: EndpointRef,
97    /// The producer (inbound) side.
98    pub to: EndpointRef,
99    /// Edge confidence — the weaker of the two endpoint detections.
100    pub confidence: f64,
101    /// True when the two ends live in different repos.
102    pub cross_repo: bool,
103}
104
105/// A repo's exported endpoints — the unit shared for cross-repo matching.
106#[derive(Debug, Clone, Serialize, Deserialize)]
107pub struct ServiceManifest {
108    pub repo_id: String,
109    pub endpoints: Vec<ServiceEndpoint>,
110}
111
112/// An endpoint detected by a language adapter, before the index pipeline
113/// enriches it with this repo's `repo_id` and the owning symbol's `symbol_id`.
114/// Adapters work in qnames, so a [`DetectedEndpoint`] names its owner by qname.
115#[derive(Debug, Clone, PartialEq)]
116pub struct DetectedEndpoint {
117    pub transport: Transport,
118    pub direction: Direction,
119    pub contract: String,
120    /// qname of the symbol that owns this endpoint — the route handler, or the
121    /// function containing the client call.
122    pub owner_qname: String,
123    pub file: String,
124    pub line: u32,
125    pub confidence: f64,
126    pub note: Option<String>,
127}
128
129impl DetectedEndpoint {
130    /// Promote to a full [`ServiceEndpoint`] once repo + symbol identity resolve.
131    pub fn into_endpoint(self, repo_id: &str, symbol_id: &str) -> ServiceEndpoint {
132        ServiceEndpoint {
133            transport: self.transport,
134            direction: self.direction,
135            contract: self.contract,
136            repo_id: repo_id.to_string(),
137            symbol_id: symbol_id.to_string(),
138            qname: self.owner_qname,
139            file: self.file,
140            line: self.line,
141            confidence: self.confidence,
142            note: self.note,
143        }
144    }
145}
146
147// ---------------------------------------------------------------------------
148// Contract normalization
149// ---------------------------------------------------------------------------
150
151/// Build a normalized HTTP contract key from a method and a path (or full URL).
152/// A client call and a server route for the same endpoint must produce the same
153/// string: the method is upper-cased, any scheme+host is stripped, path
154/// parameters (`{id}`, `:id`, `<id>`, `<int:id>`) collapse to `{}`, query and
155/// fragment are dropped, and trailing slashes are removed.
156///
157/// ```
158/// use agentstatedeveloper_core::cross_service::http_contract;
159/// assert_eq!(http_contract("post", "/Users/{id}/"), "http:POST /users/{}");
160/// assert_eq!(http_contract("GET", "https://api.svc/users/:id?x=1"), "http:GET /users/{}");
161/// ```
162pub fn http_contract(method: &str, path_or_url: &str) -> String {
163    format!(
164        "http:{} {}",
165        method.trim().to_uppercase(),
166        normalize_http_path(path_or_url)
167    )
168}
169
170fn normalize_http_path(raw: &str) -> String {
171    // Strip scheme+host if a full URL was given.
172    let after_host = match raw.find("://") {
173        Some(i) => {
174            let rest = &raw[i + 3..];
175            match rest.find('/') {
176                Some(p) => &rest[p..],
177                None => "/",
178            }
179        }
180        None => raw,
181    };
182    // Drop query/fragment.
183    let path = after_host
184        .split(['?', '#'])
185        .next()
186        .unwrap_or(after_host)
187        .trim()
188        .trim_end_matches('/');
189
190    let segs: Vec<String> = path
191        .split('/')
192        .filter(|s| !s.is_empty())
193        .map(|seg| {
194            if is_path_param(seg) {
195                "{}".to_string()
196            } else {
197                seg.to_lowercase()
198            }
199        })
200        .collect();
201
202    if segs.is_empty() {
203        "/".to_string()
204    } else {
205        format!("/{}", segs.join("/"))
206    }
207}
208
209/// Does a path segment denote a parameter? Covers the common framework spellings:
210/// `{id}` (OpenAPI/Spring), `:id` (Express/Rails), `<id>` / `<int:id>` (Flask),
211/// `${id}` (template literals).
212fn is_path_param(seg: &str) -> bool {
213    (seg.starts_with('{') && seg.ends_with('}'))
214        || (seg.starts_with("${") && seg.ends_with('}'))
215        || seg.starts_with(':')
216        || (seg.starts_with('<') && seg.ends_with('>'))
217}
218
219/// Build a normalized pub-sub contract key from a topic / queue / event name.
220pub fn pubsub_contract(topic: &str) -> String {
221    format!("topic:{}", topic.trim().to_lowercase())
222}
223
224/// Stable 16-hex-char hash of a contract key, for use as a filesystem-/path-safe
225/// segment (raw contract keys contain spaces, `/`, and `:`).
226pub fn contract_hash(contract: &str) -> String {
227    blake3::hash(contract.as_bytes()).to_hex()[..16].to_string()
228}
229
230// ---------------------------------------------------------------------------
231// repo_id resolution
232// ---------------------------------------------------------------------------
233
234/// Normalize a git remote URL into a stable `repo_id`. SCP-form and HTTPS-form
235/// URLs for the same repo collapse to one id:
236///
237/// ```
238/// use agentstatedeveloper_core::cross_service::normalize_repo_id;
239/// assert_eq!(normalize_repo_id("git@github.com:Org/Repo.git"), "github.com/org/repo");
240/// assert_eq!(normalize_repo_id("https://github.com/Org/Repo.git"), "github.com/org/repo");
241/// ```
242pub fn normalize_repo_id(remote_url: &str) -> String {
243    let mut s = remote_url.trim().to_string();
244
245    // Strip scheme://
246    if let Some(i) = s.find("://") {
247        s = s[i + 3..].to_string();
248    }
249    // Strip userinfo (`git@`, `user@`) that precedes the host.
250    if let Some(at) = s.find('@') {
251        let host_boundary = s.find('/').unwrap_or(s.len());
252        if at < host_boundary {
253            s = s[at + 1..].to_string();
254        }
255    }
256    // SCP form `host:path` → `host/path`, but leave a `:port` alone.
257    if let Some(colon) = s.find(':') {
258        let next_is_digit = s[colon + 1..]
259            .chars()
260            .next()
261            .map_or(false, |c| c.is_ascii_digit());
262        if !next_is_digit {
263            s.replace_range(colon..colon + 1, "/");
264        }
265    }
266    // Trim trailing slash + `.git`.
267    let s = s.trim_end_matches('/');
268    let s = s.strip_suffix(".git").unwrap_or(s);
269    s.trim_end_matches('/').to_lowercase()
270}
271
272/// Resolve this repo's id: an explicit override wins; otherwise the normalized
273/// `origin` git remote; otherwise the working-directory name as a last resort.
274pub fn resolve_repo_id(override_id: Option<&str>, cwd: &std::path::Path) -> String {
275    if let Some(id) = override_id {
276        let id = id.trim();
277        if !id.is_empty() {
278            return id.to_string();
279        }
280    }
281    if let Some(url) = git_origin_url(cwd) {
282        let norm = normalize_repo_id(&url);
283        if !norm.is_empty() {
284            return norm;
285        }
286    }
287    cwd.file_name()
288        .and_then(|n| n.to_str())
289        .map(|n| n.to_lowercase())
290        .unwrap_or_else(|| "unknown-repo".to_string())
291}
292
293fn git_origin_url(cwd: &std::path::Path) -> Option<String> {
294    let out = std::process::Command::new("git")
295        .arg("-C")
296        .arg(cwd)
297        .args(["remote", "get-url", "origin"])
298        .output()
299        .ok()?;
300    if !out.status.success() {
301        return None;
302    }
303    let url = String::from_utf8_lossy(&out.stdout).trim().to_string();
304    if url.is_empty() { None } else { Some(url) }
305}
306
307/// Flatten the on-disk endpoint registry tree
308/// (`contract_hash → repo_id → symbol_id → ServiceEndpoint`) into a flat list.
309/// Malformed entries are skipped.
310pub fn endpoints_from_tree(tree: &serde_json::Value) -> Vec<ServiceEndpoint> {
311    let mut out = Vec::new();
312    let Some(by_contract) = tree.as_object() else {
313        return out;
314    };
315    for by_repo in by_contract.values() {
316        let Some(by_repo) = by_repo.as_object() else {
317            continue;
318        };
319        for by_sym in by_repo.values() {
320            let Some(by_sym) = by_sym.as_object() else {
321                continue;
322            };
323            for ep in by_sym.values() {
324                if let Ok(e) = serde_json::from_value::<ServiceEndpoint>(ep.clone()) {
325                    out.push(e);
326                }
327            }
328        }
329    }
330    out
331}
332
333// ---------------------------------------------------------------------------
334// Matching
335// ---------------------------------------------------------------------------
336
337/// Pair outbound endpoints with inbound endpoints sharing a contract key,
338/// producing cross-service edges. Endpoints may come from any repo (local +
339/// imported manifests). Edge confidence is the weaker of the two detections;
340/// `cross_repo` is set when the ends live in different repos.
341pub fn match_edges(endpoints: &[ServiceEndpoint]) -> Vec<CrossServiceEdge> {
342    use std::collections::HashMap;
343
344    let mut inbound_by_contract: HashMap<&str, Vec<&ServiceEndpoint>> = HashMap::new();
345    for e in endpoints
346        .iter()
347        .filter(|e| e.direction == Direction::Inbound)
348    {
349        inbound_by_contract
350            .entry(e.contract.as_str())
351            .or_default()
352            .push(e);
353    }
354
355    let mut edges = Vec::new();
356    for out in endpoints
357        .iter()
358        .filter(|e| e.direction == Direction::Outbound)
359    {
360        let Some(ins) = inbound_by_contract.get(out.contract.as_str()) else {
361            continue;
362        };
363        for inb in ins {
364            // The contract prefix already encodes the transport, but guard anyway.
365            if inb.transport != out.transport {
366                continue;
367            }
368            edges.push(CrossServiceEdge {
369                transport: out.transport,
370                contract: out.contract.clone(),
371                from: EndpointRef::from(out),
372                to: EndpointRef::from(*inb),
373                confidence: out.confidence.min(inb.confidence),
374                cross_repo: out.repo_id != inb.repo_id,
375            });
376        }
377    }
378    edges
379}
380
381/// Federated edges over a set of per-repo manifests: [`match_edges`] on the
382/// union of every manifest's endpoints. Each edge carries `cross_repo` (consumer
383/// and producer in different repos). With `cross_repo_only`, in-repo edges are
384/// dropped — the federated view a Team-tier tool wants: "a client in repo A
385/// calls a route served by repo B" (Plan Q t-005).
386pub fn federated_edges(
387    manifests: &[ServiceManifest],
388    cross_repo_only: bool,
389) -> Vec<CrossServiceEdge> {
390    let all: Vec<ServiceEndpoint> = manifests
391        .iter()
392        .flat_map(|m| m.endpoints.iter().cloned())
393        .collect();
394    let mut edges = match_edges(&all);
395    if cross_repo_only {
396        edges.retain(|e| e.cross_repo);
397    }
398    edges
399}
400
401#[cfg(test)]
402mod tests {
403    use super::*;
404
405    fn endpoint(repo: &str, dir: Direction, contract: &str) -> ServiceEndpoint {
406        ServiceEndpoint {
407            transport: Transport::Http,
408            direction: dir,
409            contract: contract.to_string(),
410            repo_id: repo.to_string(),
411            symbol_id: format!("{repo}:sym"),
412            qname: format!("{repo}.fn"),
413            file: format!("{repo}/f.py"),
414            line: 1,
415            confidence: 0.9,
416            note: None,
417        }
418    }
419
420    #[test]
421    fn federated_edges_match_client_in_a_to_route_in_b() {
422        let manifests = vec![
423            ServiceManifest {
424                repo_id: "frontend".into(),
425                endpoints: vec![
426                    endpoint("frontend", Direction::Outbound, "http:GET /api/orders/{}"),
427                    // a call to something nobody serves — must NOT produce an edge
428                    endpoint("frontend", Direction::Outbound, "http:GET /api/ghost"),
429                ],
430            },
431            ServiceManifest {
432                repo_id: "backend".into(),
433                endpoints: vec![endpoint(
434                    "backend",
435                    Direction::Inbound,
436                    "http:GET /api/orders/{}",
437                )],
438            },
439        ];
440        let edges = federated_edges(&manifests, true);
441        assert_eq!(edges.len(), 1, "{edges:?}");
442        assert_eq!(edges[0].from.repo_id, "frontend");
443        assert_eq!(edges[0].to.repo_id, "backend");
444        assert_eq!(edges[0].contract, "http:GET /api/orders/{}");
445        assert!(edges[0].cross_repo);
446
447        // An in-repo-only match is excluded when cross_repo_only is set.
448        let same = vec![ServiceManifest {
449            repo_id: "solo".into(),
450            endpoints: vec![
451                endpoint("solo", Direction::Outbound, "http:GET /api/x"),
452                endpoint("solo", Direction::Inbound, "http:GET /api/x"),
453            ],
454        }];
455        assert_eq!(federated_edges(&same, true).len(), 0);
456        assert_eq!(federated_edges(&same, false).len(), 1);
457    }
458
459    // --- HTTP contract normalization ------------------------------------
460
461    #[test]
462    fn http_method_uppercased_and_path_lowercased() {
463        assert_eq!(http_contract("post", "/Charge"), "http:POST /charge");
464    }
465
466    #[test]
467    fn http_path_params_collapse_across_framework_spellings() {
468        let want = "http:GET /users/{}/posts/{}";
469        assert_eq!(http_contract("get", "/users/{id}/posts/{postId}"), want);
470        assert_eq!(http_contract("get", "/users/:id/posts/:postId"), want);
471        assert_eq!(http_contract("get", "/users/<id>/posts/<int:postId>"), want);
472    }
473
474    #[test]
475    fn http_client_url_and_server_route_match() {
476        // A client calling a full URL must phrase the same contract as the
477        // server's path-only route.
478        let client = http_contract("GET", "https://payments.svc/users/:id?expand=1#frag");
479        let server = http_contract("get", "/users/{id}");
480        assert_eq!(client, server);
481        assert_eq!(server, "http:GET /users/{}");
482    }
483
484    #[test]
485    fn http_trailing_slash_and_root_normalized() {
486        assert_eq!(http_contract("GET", "/charge/"), "http:GET /charge");
487        assert_eq!(http_contract("GET", "/"), "http:GET /");
488        assert_eq!(http_contract("GET", ""), "http:GET /");
489    }
490
491    #[test]
492    fn pubsub_contract_normalizes_case() {
493        assert_eq!(
494            pubsub_contract("  Payments.Charged "),
495            "topic:payments.charged"
496        );
497    }
498
499    #[test]
500    fn contract_hash_is_stable_and_path_safe() {
501        let c = http_contract("POST", "/charge");
502        let h = contract_hash(&c);
503        assert_eq!(h, contract_hash(&c), "hash must be deterministic");
504        assert_eq!(h.len(), 16);
505        assert!(h.chars().all(|ch| ch.is_ascii_hexdigit()));
506    }
507
508    // --- repo_id ---------------------------------------------------------
509
510    #[test]
511    fn repo_id_scp_and_https_collapse() {
512        assert_eq!(
513            normalize_repo_id("git@github.com:Org/Repo.git"),
514            "github.com/org/repo"
515        );
516        assert_eq!(
517            normalize_repo_id("https://github.com/Org/Repo.git"),
518            "github.com/org/repo"
519        );
520        assert_eq!(
521            normalize_repo_id("https://github.com/Org/Repo"),
522            "github.com/org/repo"
523        );
524    }
525
526    #[test]
527    fn repo_id_strips_userinfo_keeps_port() {
528        // A `:port` is preserved (only an SCP `host:path` colon converts to `/`),
529        // so the two URL forms for the same host stay distinct from a portless one.
530        assert_eq!(
531            normalize_repo_id("ssh://git@host.example:22/team/svc.git"),
532            "host.example:22/team/svc"
533        );
534        assert_eq!(
535            normalize_repo_id("https://user@gitlab.com/team/svc.git"),
536            "gitlab.com/team/svc"
537        );
538    }
539
540    #[test]
541    fn repo_id_override_wins_and_blank_ignored() {
542        let cwd = std::path::Path::new("/tmp/whatever");
543        assert_eq!(resolve_repo_id(Some("payments-svc"), cwd), "payments-svc");
544        // Blank override falls through (here to the dir name, since /tmp/whatever has no git origin).
545        assert_eq!(resolve_repo_id(Some("   "), cwd), "whatever");
546    }
547
548    // --- matching --------------------------------------------------------
549
550    fn ep(dir: Direction, contract: &str, repo: &str, conf: f64) -> ServiceEndpoint {
551        ServiceEndpoint {
552            transport: Transport::Http,
553            direction: dir,
554            contract: contract.to_string(),
555            repo_id: repo.to_string(),
556            symbol_id: format!("sym_{repo}_{dir:?}"),
557            qname: "q".into(),
558            file: "f".into(),
559            line: 1,
560            confidence: conf,
561            note: None,
562        }
563    }
564
565    #[test]
566    fn match_pairs_outbound_to_inbound_on_contract() {
567        let c = http_contract("POST", "/charge");
568        let eps = vec![
569            ep(Direction::Outbound, &c, "billing", 0.9),
570            ep(Direction::Inbound, &c, "payments", 0.8),
571        ];
572        let edges = match_edges(&eps);
573        assert_eq!(edges.len(), 1);
574        let e = &edges[0];
575        assert_eq!(e.from.repo_id, "billing");
576        assert_eq!(e.to.repo_id, "payments");
577        assert!(e.cross_repo);
578        // Confidence is the weaker of the two detections.
579        assert_eq!(e.confidence, 0.8);
580    }
581
582    #[test]
583    fn match_marks_same_repo_edges_not_cross_repo() {
584        let c = http_contract("GET", "/health");
585        let eps = vec![
586            ep(Direction::Outbound, &c, "svc", 1.0),
587            ep(Direction::Inbound, &c, "svc", 1.0),
588        ];
589        let edges = match_edges(&eps);
590        assert_eq!(edges.len(), 1);
591        assert!(!edges[0].cross_repo);
592    }
593
594    #[test]
595    fn no_edge_without_a_matching_inbound() {
596        let c = http_contract("POST", "/charge");
597        let edges = match_edges(&[ep(Direction::Outbound, &c, "billing", 0.9)]);
598        assert!(edges.is_empty());
599    }
600
601    #[test]
602    fn different_contracts_do_not_match() {
603        let eps = vec![
604            ep(
605                Direction::Outbound,
606                &http_contract("POST", "/charge"),
607                "a",
608                1.0,
609            ),
610            ep(
611                Direction::Inbound,
612                &http_contract("POST", "/refund"),
613                "b",
614                1.0,
615            ),
616        ];
617        assert!(match_edges(&eps).is_empty());
618    }
619
620    #[test]
621    fn one_inbound_matches_many_outbound_consumers() {
622        let c = http_contract("GET", "/users/{id}");
623        let eps = vec![
624            ep(Direction::Inbound, &c, "users", 0.9),
625            ep(Direction::Outbound, &c, "web", 0.7),
626            ep(Direction::Outbound, &c, "mobile", 0.6),
627        ];
628        let edges = match_edges(&eps);
629        assert_eq!(edges.len(), 2, "both consumers link to the one handler");
630    }
631
632    #[test]
633    fn endpoints_from_tree_flattens_registry() {
634        // Mirror the on-disk shape: contract_hash → repo_id → symbol_id → endpoint.
635        let e = ep(
636            Direction::Inbound,
637            &http_contract("POST", "/charge"),
638            "pay",
639            0.9,
640        );
641        let tree = serde_json::json!({
642            contract_hash(&e.contract): { "pay": { "sym_1": serde_json::to_value(&e).unwrap() } }
643        });
644        let got = endpoints_from_tree(&tree);
645        assert_eq!(got.len(), 1);
646        assert_eq!(got[0].contract, "http:POST /charge");
647        // Malformed/empty trees yield nothing rather than panicking.
648        assert!(endpoints_from_tree(&serde_json::Value::Null).is_empty());
649        assert!(endpoints_from_tree(&serde_json::json!({"h": {"r": {"s": 42}}})).is_empty());
650    }
651
652    #[test]
653    fn manifest_round_trips() {
654        let m = ServiceManifest {
655            repo_id: "payments".into(),
656            endpoints: vec![ep(
657                Direction::Inbound,
658                &http_contract("POST", "/charge"),
659                "payments",
660                0.9,
661            )],
662        };
663        let back: ServiceManifest =
664            serde_json::from_str(&serde_json::to_string(&m).unwrap()).unwrap();
665        assert_eq!(back.repo_id, "payments");
666        assert_eq!(back.endpoints.len(), 1);
667        assert_eq!(back.endpoints[0].contract, "http:POST /charge");
668    }
669}