Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ocean: API #2858

Draft
wants to merge 1 commit into
base: ocean-setup
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 72 additions & 18 deletions lib/ain-macros/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -127,29 +127,55 @@ pub fn repository_derive(input: TokenStream) -> TokenStream {
TokenStream::from(expanded)
}

#[proc_macro_derive(ConsensusEncoding)]
pub fn consensus_encoding_derive(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let name = input.ident;
#[proc_macro_attribute]
pub fn ocean_endpoint(_attr: TokenStream, item: TokenStream) -> TokenStream {
let input = parse_macro_input!(item as ItemFn);
let inputs = &input.sig.inputs;

let fields = if let Data::Struct(data) = input.data {
if let Fields::Named(fields) = data.fields {
fields
.named
.into_iter()
.map(|f| f.ident)
.collect::<Vec<_>>()
} else {
Vec::new()
}
} else {
Vec::new()
let name = &input.sig.ident;

let output = &input.sig.output;
let inner_type = match output {
ReturnType::Type(_, type_box) => match &**type_box {
Type::Path(type_path) => type_path.path.segments.last().and_then(|pair| {
if let syn::PathArguments::AngleBracketed(angle_bracketed_args) = &pair.arguments {
angle_bracketed_args.args.first()
} else {
None
}
}),
_ => None,
},
_ => None,
};

let field_names = fields.iter().filter_map(|f| f.as_ref()).collect::<Vec<_>>();
let param_names: Vec<_> = inputs
.iter()
.filter_map(|arg| {
if let syn::FnArg::Typed(pat_type) = arg {
Some(&pat_type.pat)
} else {
None
}
})
.collect();

let expanded = quote! {
bitcoin::impl_consensus_encoding!(#name, #(#field_names),*);
pub async fn #name(axum::extract::OriginalUri(uri): axum::extract::OriginalUri, #inputs) -> std::result::Result<axum::Json<#inner_type>, ApiError> {
#input

match #name(#(#param_names),*).await {
Err(e) => {
let (status, message) = e.into_code_and_message();
Err(ApiError::new(
status,
message,
uri.to_string()
))
},
Ok(v) => Ok(axum::Json(v))
}
}
};

TokenStream::from(expanded)
Expand Down Expand Up @@ -194,3 +220,31 @@ pub fn test_dftx_serialization(_attr: TokenStream, item: TokenStream) -> TokenSt

TokenStream::from(output)
}

#[proc_macro_derive(ConsensusEncoding)]
pub fn consensus_encoding_derive(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let name = input.ident;

let fields = if let Data::Struct(data) = input.data {
if let Fields::Named(fields) = data.fields {
fields
.named
.into_iter()
.map(|f| f.ident)
.collect::<Vec<_>>()
} else {
Vec::new()
}
} else {
Vec::new()
};

let field_names = fields.iter().filter_map(|f| f.as_ref()).collect::<Vec<_>>();

let expanded = quote! {
bitcoin::impl_consensus_encoding!(#name, #(#field_names),*);
};

TokenStream::from(expanded)
}
74 changes: 74 additions & 0 deletions lib/ain-ocean/src/api/address.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
use std::sync::Arc;

use axum::{routing::get, Router};
use defichain_rpc::{Client, RpcApi};
use serde::Deserialize;
use super::path::Path;

#[derive(Deserialize)]
struct Address {
address: String,
}

#[derive(Deserialize)]
struct History {
address: String,
height: i64,
txno: i64,
}

async fn get_account_history(
Path(History {
address,
height,
txno,
}): Path<History>,
) -> String {
format!(
"Account history for address {}, height {}, txno {}",
address, height, txno
)
}

async fn list_account_history(Path(Address { address }): Path<Address>) -> String {
format!("List account history for address {}", address)
}

async fn get_balance(Path(Address { address }): Path<Address>) -> String {
format!("balance for address {address}")
}

async fn get_aggregation(Path(Address { address }): Path<Address>) -> String {
format!("Aggregation for address {}", address)
}

async fn list_token(Path(Address { address }): Path<Address>) -> String {
format!("List tokens for address {}", address)
}

async fn list_vault(Path(Address { address }): Path<Address>) -> String {
format!("List vaults for address {}", address)
}

async fn list_transaction(Path(Address { address }): Path<Address>) -> String {
format!("List transactions for address {}", address)
}

async fn list_transaction_unspent(Path(Address { address }): Path<Address>) -> String {
format!("List unspent transactions for address {}", address)
}

pub fn router(ctx: Arc<AppContext>) -> Router {
Router::new().nest(
"/:address",
Router::new()
.route("/history/:height/:txno", get(get_account_history))
.route("/history", get(list_account_history))
.route("/balance", get(get_balance))
.route("/aggregation", get(get_aggregation))
.route("/tokens", get(list_token))
.route("/vaults", get(list_vault))
.route("/transactions", get(list_transaction))
.route("/transactions/unspent", get(list_transaction_unspent)),
)
}
183 changes: 183 additions & 0 deletions lib/ain-ocean/src/api/block.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
use std::sync::Arc;

use ain_macros::ocean_endpoint;
use anyhow::format_err;
use axum::{routing::get, Extension, Router};
use bitcoin::{BlockHash, Txid};
use rust_decimal::Decimal;
use serde::{Deserialize, Deserializer, Serialize};
use serde_with::serde_as;

use super::{
path::Path,
query::{PaginationQuery, Query},
response::{ApiPagedResponse, Response},
AppContext,
};
use crate::{
api::common::Paginate,
error::{ApiError, Error},
model::{Block, BlockContext, Transaction},
repository::{
InitialKeyProvider, RepositoryOps, SecondaryIndex, TransactionByBlockHashRepository,
},
storage::SortOrder,
Result,
};

pub enum HashOrHeight {
Height(u32),
Id(BlockHash),
}

impl<'de> Deserialize<'de> for HashOrHeight {
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
if let Ok(height) = s.parse::<u32>() {
Ok(HashOrHeight::Height(height))
} else if let Ok(id) = s.parse::<BlockHash>() {
Ok(HashOrHeight::Id(id))
} else {
Err(serde::de::Error::custom("Error parsing HashOrHeight"))
}
}
}

#[serde_as]
#[derive(Serialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct TransactionResponse {
pub id: Txid,
pub txid: Txid,
pub order: usize, // tx order
pub block: BlockContext,
pub hash: String,
pub version: u32,
pub size: u64,
pub v_size: u64,
pub weight: u64,
#[serde(with = "rust_decimal::serde::str")]
pub total_vout_value: Decimal,
pub lock_time: u64,
pub vin_count: usize,
pub vout_count: usize,
}

impl From<Transaction> for TransactionResponse {
fn from(v: Transaction) -> Self {
TransactionResponse {
id: v.id,
txid: v.id,
order: v.order,
block: v.block,
hash: v.hash,
version: v.version,
size: v.size,
v_size: v.v_size,
weight: v.weight,
total_vout_value: v.total_vout_value,
lock_time: v.lock_time,
vin_count: v.vin_count,
vout_count: v.vout_count,
}
}
}

#[ocean_endpoint]
async fn list_blocks(
Query(query): Query<PaginationQuery>,
Extension(ctx): Extension<Arc<AppContext>>,
) -> Result<ApiPagedResponse<Block>> {
let next = query
.next
.as_ref()
.map(|q| {
let height = q
.parse::<u32>()
.map_err(|_| format_err!("Invalid height"))?;
Ok::<u32, Error>(height)
})
.transpose()?;

let repository = &ctx.services.block.by_height;
let blocks = repository
.list(next, SortOrder::Descending)?
.paginate(&query)
.map(|e| repository.retrieve_primary_value(e))
.collect::<Result<Vec<_>>>()?;

Ok(ApiPagedResponse::of(blocks, query.size, |block| {
block.clone().height
}))
}

#[ocean_endpoint]
async fn get_block(
Path(id): Path<HashOrHeight>,
Extension(ctx): Extension<Arc<AppContext>>,
) -> Result<Response<Option<Block>>> {
let block = if let Some(id) = match id {
HashOrHeight::Height(n) => ctx.services.block.by_height.get(&n)?,
HashOrHeight::Id(id) => Some(id),
} {
ctx.services.block.by_id.get(&id)?
} else {
None
};

Ok(Response::new(block))
}

#[ocean_endpoint]
async fn get_transactions(
Path(hash): Path<BlockHash>,
Query(query): Query<PaginationQuery>,
Extension(ctx): Extension<Arc<AppContext>>,
) -> Result<ApiPagedResponse<TransactionResponse>> {
let repository = &ctx.services.transaction.by_block_hash;

let next = query.next.as_ref().map_or(
Ok(TransactionByBlockHashRepository::initial_key(hash)),
|q| {
let height = q
.parse::<usize>()
.map_err(|_| format_err!("Invalid height"))?;
Ok::<(BlockHash, usize), Error>((hash, height))
},
)?;

let txs = repository
.list(Some(next), SortOrder::Ascending)?
.paginate(&query)
.take_while(|item| match item {
Ok(((h, _), _)) => h == &hash,
_ => true,
})
.map(|el| repository.retrieve_primary_value(el))
.map(|v| v.map(TransactionResponse::from))
.collect::<Result<Vec<_>>>()?;

Ok(ApiPagedResponse::of(txs, query.size, |tx| tx.order))
}

// Get highest indexed block
#[ocean_endpoint]
async fn get_highest(
Extension(ctx): Extension<Arc<AppContext>>,
) -> Result<Response<Option<Block>>> {
let block = ctx.services.block.by_height.get_highest()?;

Ok(Response::new(block))
}

pub fn router(ctx: Arc<AppContext>) -> Router {
Router::new()
.route("/", get(list_blocks))
.route("/highest", get(get_highest))
.route("/:id", get(get_block))
.route("/:hash/transactions", get(get_transactions))
.layer(Extension(ctx))
}
25 changes: 25 additions & 0 deletions lib/ain-ocean/src/api/cache.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
use std::sync::Arc;

use anyhow::format_err;
use cached::proc_macro::cached;
use defichain_rpc::{defichain_rpc_json::token::TokenInfo, TokenRPC};

use super::AppContext;
use crate::Result;

#[cached(
result = true,
key = "String",
convert = r#"{ format!("gettoken{symbol}") }"#
)]
pub async fn get_token_cached(ctx: &Arc<AppContext>, symbol: &str) -> Result<(String, TokenInfo)> {
let token = ctx
.client
.get_token(symbol)
.await?
.0
.into_iter()
.next()
.ok_or(format_err!("Error getting token info"))?;
Ok(token)
}
Loading