openzoo 0.51.2 → 0.51.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -31,6 +31,29 @@ pub fn pda_for_hash(program_id: &Address, hash: &[u8; 32]) -> (Address, u8) {
31
31
  Address::find_program_address(&[b"asset", hash], program_id)
32
32
  }
33
33
 
34
+ /// Asset PDA under the shared runtime: `["asset", site_id, hash]`.
35
+ pub fn pda_for_hash_ns(program_id: &Address, ns: Option<&[u8; 32]>, hash: &[u8; 32]) -> (Address, u8) {
36
+ match ns {
37
+ Some(n) => Address::find_program_address(&[b"asset", n, hash], program_id),
38
+ None => pda_for_hash(program_id, hash),
39
+ }
40
+ }
41
+
42
+ /// Who may write assets: a compiled site's upgrade authority, or under the
43
+ /// shared runtime the authority recorded in the site account (`accounts[1]`).
44
+ #[derive(Clone, Copy)]
45
+ pub enum Auth {
46
+ Upgrade,
47
+ Site([u8; 32]),
48
+ }
49
+
50
+ pub fn check_auth(program_id: &Address, accounts: &[AccountView], auth: Auth) -> ProgramResult {
51
+ match auth {
52
+ Auth::Upgrade => check_authority(program_id, accounts),
53
+ Auth::Site(site) => crate::site::check_site_authority(program_id, accounts, &site),
54
+ }
55
+ }
56
+
34
57
  /// Verify `accounts[0]` signs as the upgrade authority of `program_id`
35
58
  /// (read off `accounts[1]`, the ProgramData account).
36
59
  pub fn check_authority(program_id: &Address, accounts: &[AccountView]) -> ProgramResult {
@@ -67,7 +90,11 @@ fn split_hash(data: &[u8]) -> Result<(&[u8; 32], &[u8]), ProgramError> {
67
90
  /// `[32 hash][u32 total_len][u8 ct_len][ct]`; accounts: authority, program
68
91
  /// data, asset PDA, system program.
69
92
  pub fn init(program_id: &Address, accounts: &mut [AccountView], data: &[u8]) -> ProgramResult {
70
- check_authority(program_id, accounts)?;
93
+ init_ns(program_id, accounts, data, None, Auth::Upgrade)
94
+ }
95
+
96
+ pub fn init_ns(program_id: &Address, accounts: &mut [AccountView], data: &[u8], ns: Option<[u8; 32]>, auth: Auth) -> ProgramResult {
97
+ check_auth(program_id, accounts, auth)?;
71
98
  let (hash, rest) = split_hash(data)?;
72
99
  if rest.len() < 5 {
73
100
  return Err(ProgramError::InvalidInstructionData);
@@ -76,7 +103,7 @@ pub fn init(program_id: &Address, accounts: &mut [AccountView], data: &[u8]) ->
76
103
  let ct_len = rest[4] as usize;
77
104
  let ct = rest.get(5..5 + ct_len).ok_or(ProgramError::InvalidInstructionData)?;
78
105
  let header = FIXED_HEADER + ct_len;
79
- let (addr, bump) = pda_for_hash(program_id, hash);
106
+ let (addr, bump) = pda_for_hash_ns(program_id, ns.as_ref(), hash);
80
107
  let (head, tail) = accounts.split_at_mut(2);
81
108
  let authority = &head[0];
82
109
  let asset = tail.first_mut().ok_or(ProgramError::NotEnoughAccountKeys)?;
@@ -94,10 +121,18 @@ pub fn init(program_id: &Address, accounts: &mut [AccountView], data: &[u8]) ->
94
121
  asset.resize(space)?;
95
122
  } else {
96
123
  let bump_bytes = [bump];
97
- let seeds = [Seed::from(b"asset".as_ref()), Seed::from(hash.as_ref()), Seed::from(bump_bytes.as_ref())];
98
- let signer = Signer::from(&seeds);
99
- CreateAccount::with_minimum_balance(authority, asset, space as u64, program_id, None)?
100
- .invoke_signed(&[signer])?;
124
+ match ns.as_ref() {
125
+ Some(n) => {
126
+ let seeds = [Seed::from(b"asset".as_ref()), Seed::from(n.as_ref()), Seed::from(hash.as_ref()), Seed::from(bump_bytes.as_ref())];
127
+ let signer = Signer::from(&seeds);
128
+ CreateAccount::with_minimum_balance(authority, asset, space as u64, program_id, None)?.invoke_signed(&[signer])?;
129
+ }
130
+ None => {
131
+ let seeds = [Seed::from(b"asset".as_ref()), Seed::from(hash.as_ref()), Seed::from(bump_bytes.as_ref())];
132
+ let signer = Signer::from(&seeds);
133
+ CreateAccount::with_minimum_balance(authority, asset, space as u64, program_id, None)?.invoke_signed(&[signer])?;
134
+ }
135
+ }
101
136
  }
102
137
  let mut d = asset.try_borrow_mut()?;
103
138
  d[0] = 1;
@@ -110,14 +145,18 @@ pub fn init(program_id: &Address, accounts: &mut [AccountView], data: &[u8]) ->
110
145
 
111
146
  /// `[32 hash][u32 offset][bytes]`; same accounts as `init`.
112
147
  pub fn write(program_id: &Address, accounts: &mut [AccountView], data: &[u8]) -> ProgramResult {
113
- check_authority(program_id, accounts)?;
148
+ write_ns(program_id, accounts, data, None, Auth::Upgrade)
149
+ }
150
+
151
+ pub fn write_ns(program_id: &Address, accounts: &mut [AccountView], data: &[u8], ns: Option<[u8; 32]>, auth: Auth) -> ProgramResult {
152
+ check_auth(program_id, accounts, auth)?;
114
153
  let (hash, rest) = split_hash(data)?;
115
154
  if rest.len() < 4 {
116
155
  return Err(ProgramError::InvalidInstructionData);
117
156
  }
118
157
  let offset = u32::from_le_bytes([rest[0], rest[1], rest[2], rest[3]]) as usize;
119
158
  let bytes = &rest[4..];
120
- let (addr, _) = pda_for_hash(program_id, hash);
159
+ let (addr, _) = pda_for_hash_ns(program_id, ns.as_ref(), hash);
121
160
  let (head, tail) = accounts.split_at_mut(2);
122
161
  let authority = &head[0];
123
162
  let asset = tail.first_mut().ok_or(ProgramError::NotEnoughAccountKeys)?;
@@ -153,9 +192,13 @@ pub fn write(program_id: &Address, accounts: &mut [AccountView], data: &[u8]) ->
153
192
 
154
193
  /// `[32 hash]`: reclaim the rent to the authority.
155
194
  pub fn close(program_id: &Address, accounts: &mut [AccountView], data: &[u8]) -> ProgramResult {
156
- check_authority(program_id, accounts)?;
195
+ close_ns(program_id, accounts, data, None, Auth::Upgrade)
196
+ }
197
+
198
+ pub fn close_ns(program_id: &Address, accounts: &mut [AccountView], data: &[u8], ns: Option<[u8; 32]>, auth: Auth) -> ProgramResult {
199
+ check_auth(program_id, accounts, auth)?;
157
200
  let (hash, _) = split_hash(data)?;
158
- let (addr, _) = pda_for_hash(program_id, hash);
201
+ let (addr, _) = pda_for_hash_ns(program_id, ns.as_ref(), hash);
159
202
  let (head, tail) = accounts.split_at_mut(2);
160
203
  let authority = &mut head[0];
161
204
  let asset = tail.first_mut().ok_or(ProgramError::NotEnoughAccountKeys)?;
@@ -17,6 +17,11 @@ pub struct Ctx<'a> {
17
17
  pub res: Resp,
18
18
  pub sent: bool,
19
19
  pub env: &'a [(&'static str, &'static str)],
20
+ /// Environment from a bytecode module (shared runtime); consulted first.
21
+ pub env_dyn: Vec<(String, String)>,
22
+ /// Site namespace for KV/asset seeds: `None` for a compiled site program
23
+ /// (the program id is the namespace), `Some(site_id)` under the shared runtime.
24
+ pub ns: Option<[u8; 32]>,
20
25
  pub kv: KvState,
21
26
  query_cache: Option<Val>,
22
27
  body_cache: Option<Val>,
@@ -36,6 +41,8 @@ impl<'a> Ctx<'a> {
36
41
  res: Resp::default(),
37
42
  sent: false,
38
43
  env,
44
+ env_dyn: Vec::new(),
45
+ ns: None,
39
46
  kv: KvState::default(),
40
47
  query_cache: None,
41
48
  body_cache: None,
@@ -216,6 +223,11 @@ impl<'a> Ctx<'a> {
216
223
 
217
224
  pub fn env(&self, name: &Val) -> Val {
218
225
  let n = name.to_js_string();
226
+ for (k, v) in self.env_dyn.iter() {
227
+ if *k == n {
228
+ return Val::str(v);
229
+ }
230
+ }
219
231
  for (k, v) in self.env.iter() {
220
232
  if *k == n {
221
233
  return Val::str(v);
@@ -225,6 +237,9 @@ impl<'a> Ctx<'a> {
225
237
  }
226
238
  pub fn env_obj(&self) -> Val {
227
239
  let mut o = Val::obj();
240
+ for (k, v) in self.env_dyn.iter() {
241
+ o.set_str(k, Val::str(v));
242
+ }
228
243
  for (k, v) in self.env.iter() {
229
244
  o.set_str(k, Val::str(v));
230
245
  }
@@ -0,0 +1,245 @@
1
+ //! Bridge helpers shared by compiled sites (the generated prelude calls
2
+ //! these) and the bytecode interpreter (by id).
3
+ use crate::json;
4
+ use crate::val::{url_decode, Val};
5
+ use crate::Ctx;
6
+ use alloc::{string::String, vec::Vec};
7
+
8
+ /// Response value for app-router handlers: a tagged object `send` turns into
9
+ /// the bridge response.
10
+ pub fn resp(kind: &str, a: Val, b: Val) -> Val {
11
+ let mut o = Val::obj();
12
+ o.set_str("__zoo", Val::str(kind));
13
+ o.set_str("a", a);
14
+ o.set_str("b", b);
15
+ o
16
+ }
17
+
18
+ pub fn send(cx: &mut Ctx, v: &Val) -> Result<(), Val> {
19
+ let kind = v.get_str("__zoo");
20
+ match kind.as_str() {
21
+ Some("json") => { let a = v.get_str("a"); let b = v.get_str("b"); cx.respond_json(&a, &b); Ok(()) }
22
+ Some("raw") => { let a = v.get_str("a"); let b = v.get_str("b"); cx.respond(&a, &b); Ok(()) }
23
+ Some("redirect") => {
24
+ let url = v.get_str("a");
25
+ let mut st = v.get_str("b");
26
+ if let Val::Obj(_) = st { st = st.get_str("status"); }
27
+ cx.respond_redirect(&url, &st);
28
+ Ok(())
29
+ }
30
+ _ => if v.is_undef() { Ok(()) } else { Err(Val::str("TypeError: the handler returned a value that is not a Response")) },
31
+ }
32
+ }
33
+
34
+ /// Runtime throws are strings ("SyntaxError: ..."); in a catch they become
35
+ /// Error-like objects so `e.message` / `e.name` read as in Node.
36
+ pub fn caught(v: Val) -> Val {
37
+ if let Val::Str(s) = &v {
38
+ if let Some(i) = s.find(": ") {
39
+ let name = &s[..i];
40
+ if name.ends_with("Error") && !name.contains(' ') {
41
+ let mut o = Val::obj();
42
+ o.set_str("name", Val::str(name));
43
+ o.set_str("message", Val::str(&s[i + 2..]));
44
+ return o;
45
+ }
46
+ }
47
+ }
48
+ v
49
+ }
50
+
51
+ pub fn param_value(s: &str, catch_all: bool) -> Val {
52
+ if catch_all {
53
+ Val::Arr(s.split('/').filter(|p| !p.is_empty()).map(|p| Val::Str(url_decode(p))).collect())
54
+ } else {
55
+ Val::Str(url_decode(s))
56
+ }
57
+ }
58
+
59
+ /// A dynamic segment, passed by the gateway as the `x-zoo-param-<name>` header.
60
+ pub fn param(cx: &mut Ctx, name: &str, catch_all: bool) -> Val {
61
+ let mut h = String::from("x-zoo-param-");
62
+ h.push_str(name);
63
+ match cx.req_header(&Val::Str(h)) {
64
+ Val::Str(s) => param_value(&s, catch_all),
65
+ _ => Val::Undef,
66
+ }
67
+ }
68
+
69
+ pub fn params(cx: &mut Ctx, params: &[(&str, bool)]) -> Val {
70
+ let mut o = Val::obj();
71
+ for (name, catch_all) in params.iter() {
72
+ let v = param(cx, name, *catch_all);
73
+ if !v.is_undef() { o.set_str(name, v); }
74
+ }
75
+ o
76
+ }
77
+
78
+ /// `req.query`: the query string plus the route's dynamic segments.
79
+ pub fn query(cx: &mut Ctx, params: &[(&str, bool)]) -> Val {
80
+ let mut q = cx.req_query();
81
+ for (name, catch_all) in params.iter() {
82
+ let v = param(cx, name, *catch_all);
83
+ if !v.is_undef() { q.set_str(name, v); }
84
+ }
85
+ q
86
+ }
87
+
88
+ pub fn cookies(cx: &mut Ctx) -> Val {
89
+ let mut o = Val::obj();
90
+ if let Val::Str(h) = cx.req_header(&Val::str("cookie")) {
91
+ for part in h.split(';') {
92
+ let p = part.trim();
93
+ if let Some(i) = p.find('=') { o.set_str(&p[..i], Val::Str(url_decode(&p[i + 1..]))); }
94
+ }
95
+ }
96
+ o
97
+ }
98
+
99
+ pub fn log(a: &[Val]) -> Val {
100
+ let mut s = String::new();
101
+ for (i, v) in a.iter().enumerate() {
102
+ if i > 0 { s.push(' '); }
103
+ match v {
104
+ Val::Str(x) => s.push_str(x),
105
+ Val::Obj(_) | Val::Arr(_) => s.push_str(&json::stringify(v)),
106
+ other => s.push_str(&other.to_js_string()),
107
+ }
108
+ }
109
+ crate::wire::log_str(&s);
110
+ Val::Undef
111
+ }
112
+
113
+ /// Helper ids shared with the bytecode emitter (lib/compile/bytecode.js).
114
+ pub const H_COOKIES: u8 = 0;
115
+ pub const H_COOKIE_GET: u8 = 1;
116
+ pub const H_COOKIE_HAS: u8 = 2;
117
+ pub const H_COOKIE_ALL: u8 = 3;
118
+ pub const H_HEADER_HAS: u8 = 4;
119
+ pub const H_QUERY_HAS: u8 = 5;
120
+ pub const H_QUERY_GET_ALL: u8 = 6;
121
+ pub const H_SEARCH: u8 = 7;
122
+ pub const H_ASSIGN: u8 = 8;
123
+ pub const H_OMIT: u8 = 9;
124
+ pub const H_FROM_ENTRIES: u8 = 10;
125
+ pub const H_ARRAY_FROM: u8 = 11;
126
+ pub const H_SLICE_FROM: u8 = 12;
127
+ pub const H_URL: u8 = 13;
128
+ pub const H_NUM: u8 = 14;
129
+ pub const H_SET_HEADERS: u8 = 15;
130
+
131
+ pub fn helper_id(name: &str) -> Option<u8> {
132
+ Some(match name {
133
+ "cookies" => H_COOKIES, "cookie_get" => H_COOKIE_GET, "cookie_has" => H_COOKIE_HAS, "cookie_all" => H_COOKIE_ALL,
134
+ "header_has" => H_HEADER_HAS, "query_has" => H_QUERY_HAS, "query_get_all" => H_QUERY_GET_ALL, "search" => H_SEARCH,
135
+ "assign" => H_ASSIGN, "omit" => H_OMIT, "from_entries" => H_FROM_ENTRIES, "array_from" => H_ARRAY_FROM,
136
+ "slice_from" => H_SLICE_FROM, "url" => H_URL, "num" => H_NUM, "set_headers" => H_SET_HEADERS,
137
+ _ => return None,
138
+ })
139
+ }
140
+
141
+ pub fn helper(cx: &mut Ctx, id: u8, a: &[Val]) -> Val {
142
+ let arg = |i: usize| a.get(i).cloned().unwrap_or(Val::Undef);
143
+ match id {
144
+ H_COOKIES => cookies(cx),
145
+ H_COOKIE_GET => {
146
+ let name = arg(0);
147
+ let v = cookies(cx).get(&name);
148
+ if v.is_undef() { return Val::Undef; }
149
+ let mut o = Val::obj();
150
+ o.set_str("name", name);
151
+ o.set_str("value", v);
152
+ o
153
+ }
154
+ H_COOKIE_HAS => Val::Bool(cookies(cx).has(&arg(0))),
155
+ H_COOKIE_ALL => {
156
+ let mut out = Val::arr();
157
+ if let Val::Obj(kv) = cookies(cx) {
158
+ for (k, v) in kv {
159
+ let mut o = Val::obj();
160
+ o.set_str("name", Val::Str(k));
161
+ o.set_str("value", v);
162
+ out.push(o);
163
+ }
164
+ }
165
+ out
166
+ }
167
+ H_HEADER_HAS => Val::Bool(!cx.req_header(&arg(0)).is_nullish()),
168
+ H_QUERY_HAS => Val::Bool(cx.req_query().has(&arg(0))),
169
+ H_QUERY_GET_ALL => match cx.req_query_get(&arg(0)) {
170
+ Val::Null | Val::Undef => Val::arr(),
171
+ v => Val::Arr(alloc::vec![v]),
172
+ },
173
+ H_SEARCH => {
174
+ let with_q = arg(0).truthy();
175
+ let u = cx.req_url().to_js_string();
176
+ match u.find('?') {
177
+ Some(i) => Val::Str(String::from(if with_q { &u[i..] } else { &u[i + 1..] })),
178
+ None => Val::str(""),
179
+ }
180
+ }
181
+ H_ASSIGN => {
182
+ let mut t = if a.is_empty() { Val::obj() } else { arg(0) };
183
+ for src in a.iter().skip(1) {
184
+ if let Val::Obj(kv) = src { for (k, v) in kv.iter() { t.set_str(k, v.clone()); } }
185
+ }
186
+ t
187
+ }
188
+ H_OMIT => {
189
+ let mut out = Val::obj();
190
+ let skip: Vec<String> = arg(1).iter_values().iter().map(|k| k.to_js_string()).collect();
191
+ if let Val::Obj(kv) = arg(0) {
192
+ for (k, v) in kv.iter() { if !skip.iter().any(|s| s == k) { out.set_str(k, v.clone()); } }
193
+ }
194
+ out
195
+ }
196
+ H_FROM_ENTRIES => {
197
+ let mut o = Val::obj();
198
+ for pair in arg(0).iter_values() {
199
+ let k = pair.get(&Val::Num(0.0));
200
+ let v = pair.get(&Val::Num(1.0));
201
+ o.set(&k, v);
202
+ }
203
+ o
204
+ }
205
+ H_ARRAY_FROM => Val::Arr(arg(0).iter_values()),
206
+ H_SLICE_FROM => {
207
+ let items = arg(0).iter_values();
208
+ let from = arg(1).to_num().max(0.0) as usize;
209
+ Val::Arr(items.into_iter().skip(from).collect())
210
+ }
211
+ H_URL => {
212
+ let input = arg(0).to_js_string();
213
+ let base = if arg(1).is_undef() { String::new() } else { arg(1).to_js_string() };
214
+ if input.contains("://") || base.is_empty() { return Val::Str(input); }
215
+ let origin_end = match base.find("://") {
216
+ Some(i) => base[i + 3..].find('/').map(|j| i + 3 + j).unwrap_or(base.len()),
217
+ None => 0,
218
+ };
219
+ let mut out = String::new();
220
+ if input.starts_with('/') { out.push_str(&base[..origin_end]); out.push_str(&input); return Val::Str(out); }
221
+ let path_end = base.rfind('/').map(|i| i + 1).unwrap_or(base.len()).max(origin_end).min(base.len());
222
+ out.push_str(&base[..path_end]);
223
+ out.push_str(&input);
224
+ Val::Str(out)
225
+ }
226
+ H_NUM => {
227
+ let name = arg(0).to_js_string();
228
+ let n = match a.get(1) { Some(Val::Num(n)) => *n, _ => return Val::Bool(false) };
229
+ Val::Bool(match name.as_str() {
230
+ "isInteger" => n.is_finite() && n == libm::trunc(n),
231
+ "isNaN" => n.is_nan(),
232
+ "isFinite" => n.is_finite(),
233
+ "isSafeInteger" => n.is_finite() && n == libm::trunc(n) && libm::fabs(n) <= 9007199254740991.0,
234
+ _ => false,
235
+ })
236
+ }
237
+ H_SET_HEADERS => {
238
+ if let Val::Obj(kv) = arg(0) {
239
+ for (k, v) in kv.iter() { cx.res_header(&Val::str(k), v); }
240
+ }
241
+ Val::Undef
242
+ }
243
+ _ => Val::Undef,
244
+ }
245
+ }
@@ -36,8 +36,17 @@ pub fn key_hash(key: &str) -> [u8; 32] {
36
36
  }
37
37
 
38
38
  pub fn pda_for(program_id: &Address, key: &str) -> (Address, u8, [u8; 32]) {
39
+ pda_for_ns(program_id, None, key)
40
+ }
41
+
42
+ /// KV PDA: `["kv", hash]` for a compiled site, `["kv", site_id, hash]` under
43
+ /// the shared runtime.
44
+ pub fn pda_for_ns(program_id: &Address, ns: Option<&[u8; 32]>, key: &str) -> (Address, u8, [u8; 32]) {
39
45
  let h = key_hash(key);
40
- let (a, bump) = Address::find_program_address(&[b"kv", &h], program_id);
46
+ let (a, bump) = match ns {
47
+ Some(n) => Address::find_program_address(&[b"kv", n, &h], program_id),
48
+ None => Address::find_program_address(&[b"kv", &h], program_id),
49
+ };
41
50
  (a, bump, h)
42
51
  }
43
52
 
@@ -50,7 +59,7 @@ struct Slot {
50
59
  impl<'a> Ctx<'a> {
51
60
  fn kv_slot(&mut self, key: &Val) -> Result<Slot, Val> {
52
61
  let k = key.to_js_string();
53
- let (addr, bump, hash) = pda_for(self.program_id, &k);
62
+ let (addr, bump, hash) = pda_for_ns(self.program_id, self.ns.as_ref(), &k);
54
63
  match self.accounts.iter().position(|a| *a.address() == addr) {
55
64
  Some(idx) => Ok(Slot { idx, bump, hash }),
56
65
  None => {
@@ -147,12 +156,24 @@ impl<'a> Ctx<'a> {
147
156
  if !payer.is_signer() {
148
157
  return Err(Val::str("kv.set: payer must sign"));
149
158
  }
150
- let seeds = [Seed::from(b"kv".as_ref()), Seed::from(s.hash.as_ref()), Seed::from(bump.as_ref())];
151
- let signer = Signer::from(&seeds);
152
- CreateAccount::with_minimum_balance(payer, acc, need as u64, program_id, None)
153
- .map_err(|e| Val::Str(err_str("kv create", e)))?
154
- .invoke_signed(&[signer])
155
- .map_err(|e| Val::Str(err_str("kv create", e)))?;
159
+ let ns = self.ns;
160
+ let r = match ns.as_ref() {
161
+ Some(n) => {
162
+ let seeds = [Seed::from(b"kv".as_ref()), Seed::from(n.as_ref()), Seed::from(s.hash.as_ref()), Seed::from(bump.as_ref())];
163
+ let signer = Signer::from(&seeds);
164
+ CreateAccount::with_minimum_balance(payer, acc, need as u64, program_id, None)
165
+ .map_err(|e| Val::Str(err_str("kv create", e)))?
166
+ .invoke_signed(&[signer])
167
+ }
168
+ None => {
169
+ let seeds = [Seed::from(b"kv".as_ref()), Seed::from(s.hash.as_ref()), Seed::from(bump.as_ref())];
170
+ let signer = Signer::from(&seeds);
171
+ CreateAccount::with_minimum_balance(payer, acc, need as u64, program_id, None)
172
+ .map_err(|e| Val::Str(err_str("kv create", e)))?
173
+ .invoke_signed(&[signer])
174
+ }
175
+ };
176
+ r.map_err(|e| Val::Str(err_str("kv create", e)))?;
156
177
  } else if need > acc.data_len() {
157
178
  let rent = Rent::get().map_err(|e| Val::Str(err_str("rent", e)))?;
158
179
  let min = rent.try_minimum_balance(need).map_err(|e| Val::Str(err_str("rent", e)))?;
@@ -23,6 +23,9 @@ extern crate alloc;
23
23
  pub mod assets;
24
24
  pub mod ctx;
25
25
  pub mod fmt;
26
+ pub mod helpers;
27
+ pub mod site;
28
+ pub mod vm;
26
29
  pub mod json;
27
30
  pub mod kv;
28
31
  pub mod val;
@@ -43,6 +46,16 @@ pub const TAG_INVOKE: u8 = 0;
43
46
  pub const TAG_ASSET_INIT: u8 = 1;
44
47
  pub const TAG_ASSET_WRITE: u8 = 2;
45
48
  pub const TAG_ASSET_CLOSE: u8 = 3;
49
+ // Shared runtime (zoo-vm) tags. Instruction data after the tag starts with
50
+ // the 32-byte site id, then the same payloads as the compiled-site tags.
51
+ pub const TAG_SITE_INIT: u8 = 16;
52
+ pub const TAG_SITE_SET_AUTHORITY: u8 = 17;
53
+ pub const TAG_VM_INVOKE: u8 = 18;
54
+ pub const TAG_VM_ASSET_INIT: u8 = 19;
55
+ pub const TAG_VM_ASSET_WRITE: u8 = 20;
56
+ pub const TAG_VM_ASSET_CLOSE: u8 = 21;
57
+ /// Path of a site's bytecode asset under the shared runtime.
58
+ pub const CODE_PATH: &str = "/.zoo/code.bin";
46
59
 
47
60
  /// Custom program error: a KV account the handler needed was not passed.
48
61
  /// The gateway reads the `ZOOK` log lines, adds the accounts and retries.
@@ -0,0 +1,94 @@
1
+ //! The shared runtime's site registry.
2
+ //!
3
+ //! Under the shared runtime program a site is not a program: it is a site
4
+ //! account `["site", site_id]` naming its authority, a code asset
5
+ //! (`/.zoo/code.bin`, the bytecode module), its static assets and its
6
+ //! manifest, all PDAs of the runtime program namespaced by `site_id`, plus KV
7
+ //! accounts `["kv", site_id, hash]`. `site_id` is any 32-byte id the deployer
8
+ //! picks (the CLI uses a fresh keypair's public key so ids look like programs).
9
+ use crate::ERR_NOT_AUTHORITY;
10
+ use pinocchio::{
11
+ cpi::{Seed, Signer},
12
+ error::ProgramError,
13
+ AccountView, Address, ProgramResult,
14
+ };
15
+ use pinocchio_system::instructions::CreateAccount;
16
+
17
+ pub const SITE_LEN: usize = 2 + 32 + 32; // ver, bump, authority, reserved
18
+
19
+ pub fn site_pda(program_id: &Address, site_id: &[u8; 32]) -> (Address, u8) {
20
+ Address::find_program_address(&[b"site", site_id], program_id)
21
+ }
22
+
23
+ /// `accounts[0]` must sign as the authority recorded in the site account at `accounts[1]`.
24
+ pub fn check_site_authority(program_id: &Address, accounts: &[AccountView], site_id: &[u8; 32]) -> ProgramResult {
25
+ let authority = accounts.first().ok_or(ProgramError::NotEnoughAccountKeys)?;
26
+ let site = accounts.get(1).ok_or(ProgramError::NotEnoughAccountKeys)?;
27
+ if !authority.is_signer() {
28
+ return Err(ProgramError::MissingRequiredSignature);
29
+ }
30
+ let (expected, _) = site_pda(program_id, site_id);
31
+ if *site.address() != expected || !site.owned_by(program_id) {
32
+ return Err(ProgramError::Custom(ERR_NOT_AUTHORITY));
33
+ }
34
+ let d = site.try_borrow()?;
35
+ if d.len() < SITE_LEN || d[0] != 1 || d[2..34] != authority.address().as_ref()[..] {
36
+ return Err(ProgramError::Custom(ERR_NOT_AUTHORITY));
37
+ }
38
+ Ok(())
39
+ }
40
+
41
+ /// `[32 site_id]`; accounts: authority (signer, pays), site PDA (writable), system program.
42
+ /// Creates the site account owned by the runtime with `authority` in charge.
43
+ pub fn init(program_id: &Address, accounts: &mut [AccountView], data: &[u8]) -> ProgramResult {
44
+ if data.len() < 32 {
45
+ return Err(ProgramError::InvalidInstructionData);
46
+ }
47
+ let mut site_id = [0u8; 32];
48
+ site_id.copy_from_slice(&data[..32]);
49
+ let (head, tail) = accounts.split_at_mut(1);
50
+ let authority = &head[0];
51
+ let site = tail.first_mut().ok_or(ProgramError::NotEnoughAccountKeys)?;
52
+ if !authority.is_signer() {
53
+ return Err(ProgramError::MissingRequiredSignature);
54
+ }
55
+ let (addr, bump) = site_pda(program_id, &site_id);
56
+ if *site.address() != addr {
57
+ return Err(ProgramError::InvalidSeeds);
58
+ }
59
+ if site.owned_by(program_id) {
60
+ return Err(ProgramError::AccountAlreadyInitialized);
61
+ }
62
+ let bump_bytes = [bump];
63
+ let seeds = [Seed::from(b"site".as_ref()), Seed::from(site_id.as_ref()), Seed::from(bump_bytes.as_ref())];
64
+ let signer = Signer::from(&seeds);
65
+ CreateAccount::with_minimum_balance(authority, site, SITE_LEN as u64, program_id, None)?.invoke_signed(&[signer])?;
66
+ let mut d = site.try_borrow_mut()?;
67
+ d[0] = 1;
68
+ d[1] = bump;
69
+ d[2..34].copy_from_slice(authority.address().as_ref());
70
+ Ok(())
71
+ }
72
+
73
+ /// `[32 site_id][32 new_authority]`; accounts: authority (signer), site PDA (writable).
74
+ pub fn set_authority(program_id: &Address, accounts: &mut [AccountView], data: &[u8]) -> ProgramResult {
75
+ if data.len() < 64 {
76
+ return Err(ProgramError::InvalidInstructionData);
77
+ }
78
+ let mut site_id = [0u8; 32];
79
+ site_id.copy_from_slice(&data[..32]);
80
+ check_site_authority(program_id, accounts, &site_id)?;
81
+ let site = accounts.get_mut(1).ok_or(ProgramError::NotEnoughAccountKeys)?;
82
+ let mut d = site.try_borrow_mut()?;
83
+ d[2..34].copy_from_slice(&data[32..64]);
84
+ Ok(())
85
+ }
86
+
87
+ pub fn site_id_from(data: &[u8]) -> Result<([u8; 32], &[u8]), ProgramError> {
88
+ if data.len() < 32 {
89
+ return Err(ProgramError::InvalidInstructionData);
90
+ }
91
+ let mut id = [0u8; 32];
92
+ id.copy_from_slice(&data[..32]);
93
+ Ok((id, &data[32..]))
94
+ }