Skip to content

Latest commit

ย 

History

History
436 lines (365 loc) ยท 12 KB

File metadata and controls

436 lines (365 loc) ยท 12 KB

SCAPI Architecture

Detailed architecture documentation for SCAPI project.

๐Ÿ“ Project Structure

SCAPI/
โ”œโ”€โ”€ src/
โ”‚   โ”œโ”€โ”€ lib.rs          # Library entry point, exports modules
โ”‚   โ”œโ”€โ”€ main.rs         # CLI application entry point
โ”‚   โ”œโ”€โ”€ sc_api.rs       # Core API implementation
โ”‚   โ””โ”€โ”€ wasm.rs         # WASM bindings and JavaScript interop
โ”œโ”€โ”€ pkg/                # WASM build output (after wasm-pack build)
โ”‚   โ”œโ”€โ”€ scapi.js        # JavaScript/TypeScript wrapper
โ”‚   โ”œโ”€โ”€ scapi_bg.wasm   # Compiled WebAssembly binary
โ”‚   โ”œโ”€โ”€ scapi.d.ts      # TypeScript type definitions
โ”‚   โ””โ”€โ”€ package.json    # NPM package metadata
โ”œโ”€โ”€ Cargo.toml          # Rust project configuration
โ”œโ”€โ”€ README.md           # Main documentation
โ”œโ”€โ”€ EXAMPLES.md         # Usage examples
โ”œโ”€โ”€ QUICKSTART.md       # Quick start guide
โ”œโ”€โ”€ CONTRIBUTING.md     # Contribution guidelines
โ”œโ”€โ”€ CHANGELOG.md        # Version history
โ””โ”€โ”€ LICENSE             # MIT License

๐Ÿ—๏ธ Core Components

1. RequestDataBuilder (sc_api.rs)

Purpose: Fluent API for building smart contract queries

Responsibilities:

  • Managing binary buffer for input data
  • Encoding various data types (u8, u16, u32, u64, i8, i16, i32, i64, f32, f64)
  • Supporting arrays and special types (m256i for IDs)
  • Converting to base64 for HTTP requests
  • Sending requests to RPC endpoint

Key Methods:

pub struct RequestDataBuilder {
    contract_index: u32,
    input_type: u32,
    buffer: Vec<u8>,
    little_endian: bool,
}

impl RequestDataBuilder {
    pub fn new() -> Self
    pub fn set_contract_index(mut self, index: u32) -> Self
    pub fn set_input_type(mut self, input_type: u32) -> Self
    pub fn add_uint64(mut self, value: u64) -> Self
    pub fn to_bytes(&self) -> Vec<u8>
    pub async fn send(&self) -> Result<Vec<u8>>
}

Data Flow:

Input โ†’ add_* methods โ†’ buffer โ†’ to_bytes() โ†’ send() โ†’ RPC โ†’ Response bytes

2. ResponseDecoder (sc_api.rs)

Purpose: Declarative decoding of binary contract responses

Responsibilities:

  • Reading binary data with correct endianness
  • Parsing various data types
  • Validating data availability
  • Creating structured JSON representation
  • Supporting arrays and nested structures

Key Methods:

pub struct ResponseDecoder<'a> {
    reader: BinaryReader<'a>,
    obj: serde_json::Map<String, serde_json::Value>,
}

impl<'a> ResponseDecoder<'a> {
    pub fn new(data: &'a [u8]) -> Self
    pub fn u8(mut self, field: &str) -> Result<Self>
    pub fn u64(mut self, field: &str) -> Result<Self>
    pub fn array_m256i(mut self, field: &str, count: usize) -> Result<Self>
    pub fn array_struct_bytes(mut self, field: &str, count: usize, struct_size: usize) -> Result<Self>
    pub fn to_value(self) -> serde_json::Value
}

Data Flow:

Response bytes โ†’ BinaryReader โ†’ read_* methods โ†’ JSON Map โ†’ to_value() โ†’ JavaScript object

3. BinaryReader (sc_api.rs)

Purpose: Low-level binary data reading

Responsibilities:

  • Managing read position in buffer
  • Handling Little Endian / Big Endian
  • Validating data bounds
  • Primitive read operations

Key Methods:

pub struct BinaryReader<'a> {
    data: &'a [u8],
    position: usize,
    pub endianness: Endianness,
}

impl<'a> BinaryReader<'a> {
    pub fn new(data: &'a [u8]) -> Self
    pub fn read_u8(&mut self) -> Result<u8>
    pub fn read_u64(&mut self) -> Result<u64>
    pub fn read_bytes(&mut self, len: usize) -> Result<&'a [u8]>
    fn ensure_available(&self, len: usize) -> Result<()>
}

4. WASM Bindings (wasm.rs)

Purpose: JavaScript/TypeScript interface for WASM module

Responsibilities:

  • Exporting Rust functions to JavaScript
  • Converting between Rust and JS types
  • Handling asynchronous operations (Promises)
  • Managing memory between WASM and JS

Key Components:

#[wasm_bindgen]
pub struct RequestDataBuilderHandle {
    inner: RequestDataBuilder,
}

#[wasm_bindgen]
pub struct ResponseDecoderHandle {
    data: Vec<u8>,
    decoder: ResponseDecoder<'static>,
}

#[wasm_bindgen]
impl RequestDataBuilderHandle {
    #[wasm_bindgen(constructor)]
    pub fn new() -> Self
    
    pub fn set_contract_index(mut self, index: u32) -> Self
    
    pub async fn send(&self) -> Result<Vec<u8>, JsValue>
}

JavaScript Integration:

// JavaScript side
const builder = new RequestDataBuilder()
    .set_contract_index(16)
    .send(); // Returns Promise<Uint8Array>

๐Ÿ”„ Request/Response Flow

Complete Flow Diagram

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  JavaScript     โ”‚
โ”‚  Application    โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
         โ”‚
         โ”‚ 1. new RequestDataBuilder()
         โ”‚    .set_contract_index(16)
         โ”‚    .set_input_type(1)
         โ”‚    .send()
         โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  WASM Binding Layer     โ”‚
โ”‚  (wasm.rs)              โ”‚
โ”‚                         โ”‚
โ”‚  - RequestDataBuilder   โ”‚
โ”‚    Handle               โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
         โ”‚
         โ”‚ 2. Convert JS params to Rust
         โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  Core API Layer         โ”‚
โ”‚  (sc_api.rs)            โ”‚
โ”‚                         โ”‚
โ”‚  - RequestDataBuilder   โ”‚
โ”‚  - BinaryWriter         โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
         โ”‚
         โ”‚ 3. Build binary payload
         โ”‚    - Contract index
         โ”‚    - Input type
         โ”‚    - Encoded parameters
         โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  HTTP Client            โ”‚
โ”‚  (reqwest)              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
         โ”‚
         โ”‚ 4. POST to https://rpc.qubic.org
         โ”‚    Body: base64 encoded payload
         โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  Qubic RPC Endpoint     โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
         โ”‚
         โ”‚ 5. Execute view function
         โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  Smart Contract         โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
         โ”‚
         โ”‚ 6. Return response bytes
         โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  HTTP Client            โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
         โ”‚
         โ”‚ 7. Decode base64 response
         โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  Core API Layer         โ”‚
โ”‚                         โ”‚
โ”‚  - ResponseDecoder      โ”‚
โ”‚  - BinaryReader         โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
         โ”‚
         โ”‚ 8. Parse binary response
         โ”‚    - Read types sequentially
         โ”‚    - Build JSON object
         โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  WASM Binding Layer     โ”‚
โ”‚                         โ”‚
โ”‚  - Convert to JsValue   โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
         โ”‚
         โ”‚ 9. Return JavaScript object
         โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  JavaScript     โ”‚
โ”‚  Application    โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

๐Ÿ”’ Memory Management

Rust Side

  • Stack-allocated for simple types
  • Heap-allocated Vec<u8> for buffers
  • Borrowing (&[u8]) for zero-copy operations
  • RAII ensures cleanup

WASM Side

  • Linear memory shared between JS and WASM
  • wasm-bindgen handles marshalling
  • JavaScript owns returned data
  • Rust side cleaned up when handles drop

Best Practices

// โœ… Good: zero-copy with borrowing
pub fn new(data: &'a [u8]) -> Self

// โœ… Good: move ownership to JavaScript
pub fn to_value(self) -> serde_json::Value

// โŒ Bad: unnecessary copy
pub fn to_value(&self) -> serde_json::Value

๐ŸŽฏ Design Patterns

1. Builder Pattern (RequestDataBuilder)

let request = RequestDataBuilder::new()
    .set_contract_index(16)
    .set_input_type(1)
    .add_uint64(1000);

Benefits:

  • Fluent, readable API
  • Compile-time validation
  • Flexible parameter order

2. Method Chaining (ResponseDecoder)

let result = ResponseDecoder::new(&bytes)
    .u8("field1")
    .u64("field2")
    .to_value();

Benefits:

  • Declarative data structure
  • Order matches binary layout
  • Easy to read and maintain

3. Handle Pattern (WASM)

pub struct ResponseDecoderHandle {
    data: Vec<u8>,
    decoder: ResponseDecoder<'static>,
}

Benefits:

  • Safely manages lifetimes across WASM boundary
  • Ensures data outlives decoder
  • JavaScript-friendly interface

๐Ÿงช Error Handling

Rust (anyhow)

pub fn read_u64(&mut self) -> Result<u64> {
    self.ensure_available(8)?;
    // ...
}

WASM (JsValue)

pub fn u64(self, field: &str) -> Result<Self, JsValue> {
    ResponseDecoderHandle::map_result(data, decoder.u64(field))
}

JavaScript (Promises)

try {
    const result = await builder.send();
} catch (error) {
    console.error('Failed:', error);
}

๐Ÿ“Š Data Encoding

Request Format

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ Contract Index (u32)     โ”‚ 4 bytes
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚ Input Type (u32)         โ”‚ 4 bytes
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚ Parameter 1              โ”‚ variable
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚ Parameter 2              โ”‚ variable
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚ ...                      โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Response Format (Contract-specific)

Example: GetFees

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ teamFeePercent (u8)      โ”‚ 1 byte
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚ distributionFeePercent   โ”‚ 1 byte
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚ winnerFeePercent (u8)    โ”‚ 1 byte
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚ burnPercent (u8)         โ”‚ 1 byte
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Endianness

  • Default: Little Endian (Qubic standard)
  • Configurable: via with_endianness()
// Little Endian: 0x1234 โ†’ [0x34, 0x12]
// Big Endian:    0x1234 โ†’ [0x12, 0x34]

๐Ÿš€ Performance Considerations

Zero-Copy Operations

  • ResponseDecoder borrows input data
  • No unnecessary allocations
  • Efficient for large responses

Minimal WASM Size

[profile.release]
lto = "thin"  # Link-time optimization

Async I/O

  • Non-blocking HTTP requests
  • JavaScript Promise integration
  • Tokio runtime for Rust native

๐Ÿ”ฎ Future Enhancements

Planned Features

  • Custom RPC endpoint configuration
  • Transaction execution support
  • Event subscription
  • Contract ABI parsing
  • Automatic type generation

Optimization Opportunities

  • Response caching
  • Request batching
  • Compression support
  • WebSocket support for subscriptions

For more details, see:


Made with โค๏ธ for Qubic ecosystem