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
use thiserror::Error;

use async_lock::RwLock;
use simple_request::{hyper, Request, Client};

use scale::{Decode, Encode};
use serde::{Serialize, Deserialize, de::DeserializeOwned};

pub use sp_core::{
  Pair as PairTrait,
  sr25519::{Public, Pair},
};

pub use serai_abi as abi;
pub use abi::{primitives, Transaction};
use abi::*;

pub use primitives::{SeraiAddress, Signature, Amount};
use primitives::{Header, NetworkId};

pub mod coins;
pub use coins::SeraiCoins;
pub mod dex;
pub use dex::SeraiDex;
pub mod in_instructions;
pub use in_instructions::SeraiInInstructions;
pub mod validator_sets;
pub use validator_sets::SeraiValidatorSets;
pub mod genesis_liquidity;
pub use genesis_liquidity::SeraiGenesisLiquidity;
pub mod liquidity_tokens;
pub use liquidity_tokens::SeraiLiquidityTokens;

#[derive(Clone, PartialEq, Eq, Debug, scale::Encode, scale::Decode)]
pub struct Block {
  pub header: Header,
  pub transactions: Vec<Transaction>,
}
impl Block {
  pub fn hash(&self) -> [u8; 32] {
    self.header.hash().into()
  }
  pub fn number(&self) -> u64 {
    self.header.number
  }

  /// Returns the time of this block, set by its producer, in milliseconds since the epoch.
  pub fn time(&self) -> Result<u64, SeraiError> {
    for transaction in &self.transactions {
      if let Call::Timestamp(timestamp::Call::set { now }) = transaction.call() {
        return Ok(*now);
      }
    }
    Err(SeraiError::InvalidNode("no time was present in block".to_string()))
  }
}

#[derive(Error, Debug)]
pub enum SeraiError {
  #[error("failed to communicate with serai")]
  ConnectionError,
  #[error("node is faulty: {0}")]
  InvalidNode(String),
  #[error("error in response: {0}")]
  ErrorInResponse(String),
  #[error("serai-client library was intended for a different runtime version: {0}")]
  InvalidRuntime(String),
}

#[derive(Clone)]
pub struct Serai {
  url: String,
  client: Client,
  genesis: [u8; 32],
}

type EventsInBlock = Vec<frame_system::EventRecord<Event, [u8; 32]>>;
pub struct TemporalSerai<'a> {
  serai: &'a Serai,
  block: [u8; 32],
  events: RwLock<Option<EventsInBlock>>,
}
impl<'a> Clone for TemporalSerai<'a> {
  fn clone(&self) -> Self {
    Self { serai: self.serai, block: self.block, events: RwLock::new(None) }
  }
}

impl Serai {
  pub async fn call<Req: Serialize, Res: DeserializeOwned>(
    &self,
    method: &str,
    params: Req,
  ) -> Result<Res, SeraiError> {
    let request = Request::from(
      hyper::Request::post(&self.url)
        .header("Content-Type", "application/json")
        .body(
          serde_json::to_vec(
            &serde_json::json!({ "jsonrpc": "2.0", "id": 1, "method": method, "params": params }),
          )
          .unwrap()
          .into(),
        )
        .unwrap(),
    );

    #[derive(Deserialize)]
    pub struct Error {
      message: String,
    }

    #[derive(Deserialize)]
    #[serde(untagged)]
    enum RpcResponse<T> {
      Ok { result: T },
      Err { error: Error },
    }

    let mut res = self
      .client
      .request(request)
      .await
      .map_err(|_| SeraiError::ConnectionError)?
      .body()
      .await
      .map_err(|_| SeraiError::ConnectionError)?;

    let res: RpcResponse<Res> = serde_json::from_reader(&mut res).map_err(|e| {
      SeraiError::InvalidRuntime(format!(
        "response was a different type than expected: {:?}",
        e.classify()
      ))
    })?;
    match res {
      RpcResponse::Ok { result } => Ok(result),
      RpcResponse::Err { error } => Err(SeraiError::ErrorInResponse(error.message)),
    }
  }

  fn hex_decode(str: String) -> Result<Vec<u8>, SeraiError> {
    (if let Some(stripped) = str.strip_prefix("0x") {
      hex::decode(stripped)
    } else {
      hex::decode(str)
    })
    .map_err(|_| SeraiError::InvalidNode("expected hex from node wasn't hex".to_string()))
  }

  pub async fn block_hash(&self, number: u64) -> Result<Option<[u8; 32]>, SeraiError> {
    let hash: Option<String> = self.call("chain_getBlockHash", [number]).await?;
    let Some(hash) = hash else { return Ok(None) };
    Self::hex_decode(hash)?
      .try_into()
      .map_err(|_| SeraiError::InvalidNode("didn't respond to getBlockHash with hash".to_string()))
      .map(Some)
  }

  pub async fn new(url: String) -> Result<Self, SeraiError> {
    let client = Client::with_connection_pool();
    let mut res = Serai { url, client, genesis: [0xfe; 32] };
    res.genesis = res.block_hash(0).await?.ok_or_else(|| {
      SeraiError::InvalidNode("node didn't have the first block's hash".to_string())
    })?;
    Ok(res)
  }

  fn unsigned(call: Call) -> Transaction {
    Transaction::new(call, None)
  }

  pub fn sign(&self, signer: &Pair, call: Call, nonce: u32, tip: u64) -> Transaction {
    const SPEC_VERSION: u32 = 1;
    const TX_VERSION: u32 = 1;

    let extra = Extra { era: sp_runtime::generic::Era::Immortal, nonce, tip };
    let signature_payload = (
      &call,
      &extra,
      SignedPayloadExtra {
        spec_version: SPEC_VERSION,
        tx_version: TX_VERSION,
        genesis: self.genesis,
        mortality_checkpoint: self.genesis,
      },
    )
      .encode();
    let signature = signer.sign(&signature_payload);

    Transaction::new(call, Some((signer.public().into(), signature, extra)))
  }

  pub async fn publish(&self, tx: &Transaction) -> Result<(), SeraiError> {
    // Drop the returned hash, which is the hash of the raw extrinsic, as extrinsics are allowed
    // to share hashes and this hash is accordingly useless/unsafe
    // If we are to return something, it should be block included in and position within block
    let _: String = self.call("author_submitExtrinsic", [hex::encode(tx.encode())]).await?;
    Ok(())
  }

  pub async fn latest_finalized_block_hash(&self) -> Result<[u8; 32], SeraiError> {
    let hash: String = self.call("chain_getFinalizedHead", ()).await?;
    Self::hex_decode(hash)?.try_into().map_err(|_| {
      SeraiError::InvalidNode("didn't respond to getFinalizedHead with hash".to_string())
    })
  }

  pub async fn header(&self, hash: [u8; 32]) -> Result<Option<Header>, SeraiError> {
    self.call("chain_getHeader", [hex::encode(hash)]).await
  }

  pub async fn block(&self, hash: [u8; 32]) -> Result<Option<Block>, SeraiError> {
    let block: Option<String> = self.call("chain_getBlockBin", [hex::encode(hash)]).await?;
    let Some(block) = block else { return Ok(None) };
    let Ok(bytes) = Self::hex_decode(block) else {
      Err(SeraiError::InvalidNode("didn't return a hex-encoded block".to_string()))?
    };
    let Ok(block) = Block::decode(&mut bytes.as_slice()) else {
      Err(SeraiError::InvalidNode("didn't return a block".to_string()))?
    };
    Ok(Some(block))
  }

  pub async fn latest_finalized_block(&self) -> Result<Block, SeraiError> {
    let latest = self.latest_finalized_block_hash().await?;
    let Some(block) = self.block(latest).await? else {
      Err(SeraiError::InvalidNode("node didn't have a latest block".to_string()))?
    };
    Ok(block)
  }

  // There is no provided method for this
  // TODO: Add one to Serai
  pub async fn is_finalized(&self, header: &Header) -> Result<bool, SeraiError> {
    // Get the latest finalized block
    let finalized = self.latest_finalized_block_hash().await?;
    // If the latest finalized block is this block, return true
    if finalized == header.hash().as_ref() {
      return Ok(true);
    }

    let Some(finalized) = self.header(finalized).await? else {
      Err(SeraiError::InvalidNode("couldn't get finalized header".to_string()))?
    };

    // If the finalized block has a lower number, this block can't be finalized
    if finalized.number < header.number {
      return Ok(false);
    }

    // This block, if finalized, comes before the finalized block
    // If we request the hash of this block's number, Substrate will return the hash on the main
    // chain
    // If that hash is this hash, this block is finalized
    let Some(hash) = self.block_hash(header.number).await? else {
      // This is an error since there is a finalized block at this index
      Err(SeraiError::InvalidNode(
        "couldn't get block hash for a block number below the finalized block".to_string(),
      ))?
    };

    Ok(header.hash().as_ref() == hash)
  }

  pub async fn finalized_block_by_number(&self, number: u64) -> Result<Option<Block>, SeraiError> {
    let hash = self.block_hash(number).await?;
    let Some(hash) = hash else { return Ok(None) };
    let Some(block) = self.block(hash).await? else { return Ok(None) };
    if !self.is_finalized(&block.header).await? {
      return Ok(None);
    }
    Ok(Some(block))
  }

  /*
  /// A stream which yields whenever new block(s) have been finalized.
  pub async fn newly_finalized_block(
    &self,
  ) -> Result<impl Stream<Item = Result<(), SeraiError>>, SeraiError> {
    Ok(self.0.rpc().subscribe_finalized_block_headers().await
    .map_err(|_| SeraiError::ConnectionError)?.map(
      |next| {
        next.map_err(|_| SeraiError::ConnectionError)?;
        Ok(())
      },
    ))
  }

  pub async fn nonce(&self, address: &SeraiAddress) -> Result<u32, SeraiError> {
    self
      .0
      .rpc()
      .system_account_next_index(&sp_core::sr25519::Public(address.0).to_string())
      .await
      .map_err(|_| SeraiError::ConnectionError)
  }
  */

  /// Create a TemporalSerai bound to whatever is currently the latest finalized block.
  ///
  /// The binding occurs at time of call. This does not track the latest finalized block and update
  /// itself.
  pub async fn as_of_latest_finalized_block(&self) -> Result<TemporalSerai, SeraiError> {
    let latest = self.latest_finalized_block_hash().await?;
    Ok(TemporalSerai { serai: self, block: latest, events: RwLock::new(None) })
  }

  /// Returns a TemporalSerai able to retrieve state as of the specified block.
  pub fn as_of(&self, block: [u8; 32]) -> TemporalSerai {
    TemporalSerai { serai: self, block, events: RwLock::new(None) }
  }

  /// Return the P2P Multiaddrs for the validators of the specified network.
  pub async fn p2p_validators(
    &self,
    network: NetworkId,
  ) -> Result<Vec<multiaddr::Multiaddr>, SeraiError> {
    self.call("p2p_validators", network).await
  }
}

impl<'a> TemporalSerai<'a> {
  async fn events<E>(
    &self,
    filter_map: impl Fn(&Event) -> Option<E>,
  ) -> Result<Vec<E>, SeraiError> {
    let mut events = self.events.read().await;
    if events.is_none() {
      drop(events);
      let mut events_write = self.events.write().await;
      if events_write.is_none() {
        *events_write = Some(self.storage("System", "Events", ()).await?.unwrap_or(vec![]));
      }
      drop(events_write);
      events = self.events.read().await;
    }

    let mut res = vec![];
    for event in events.as_ref().unwrap() {
      if let Some(event) = filter_map(&event.event) {
        res.push(event);
      }
    }
    Ok(res)
  }

  async fn storage<K: Encode, R: Decode>(
    &self,
    pallet: &'static str,
    name: &'static str,
    key: K,
  ) -> Result<Option<R>, SeraiError> {
    // TODO: Make this const?
    let mut full_key = sp_core::hashing::twox_128(pallet.as_bytes()).to_vec();
    full_key.extend(sp_core::hashing::twox_128(name.as_bytes()));
    full_key.extend(key.encode());

    let res: Option<String> =
      self.serai.call("state_getStorage", [hex::encode(full_key), hex::encode(self.block)]).await?;
    let Some(res) = res else { return Ok(None) };
    let res = Serai::hex_decode(res)?;
    Ok(Some(R::decode(&mut res.as_slice()).map_err(|_| {
      SeraiError::InvalidRuntime(format!(
        "different type present at storage location, raw value: {}",
        hex::encode(res)
      ))
    })?))
  }

  async fn runtime_api<P: Encode, R: Decode>(
    &self,
    method: &'static str,
    params: P,
  ) -> Result<R, SeraiError> {
    let result: String = self
      .serai
      .call(
        "state_call",
        [method.to_string(), hex::encode(params.encode()), hex::encode(self.block)],
      )
      .await?;

    let bytes = Serai::hex_decode(result.clone())?;
    R::decode(&mut bytes.as_slice()).map_err(|_| {
      SeraiError::InvalidRuntime(format!(
        "different type than what is expected to be returned, raw value: {}",
        hex::encode(result)
      ))
    })
  }

  pub fn coins(&'a self) -> SeraiCoins<'a> {
    SeraiCoins(self)
  }

  pub fn dex(&'a self) -> SeraiDex<'a> {
    SeraiDex(self)
  }

  pub fn in_instructions(&'a self) -> SeraiInInstructions<'a> {
    SeraiInInstructions(self)
  }

  pub fn validator_sets(&'a self) -> SeraiValidatorSets<'a> {
    SeraiValidatorSets(self)
  }

  pub fn genesis_liquidity(&'a self) -> SeraiGenesisLiquidity {
    SeraiGenesisLiquidity(self)
  }

  pub fn liquidity_tokens(&'a self) -> SeraiLiquidityTokens {
    SeraiLiquidityTokens(self)
  }
}