use alloy_primitives::{Address, ChainId, U256};
use alloy_sol_types::SolStruct;
use blueprint_client_tangle::contracts::ITangleTypes;
use blueprint_crypto::k256::{K256Signature, K256SigningKey, K256VerifyingKey};
use rust_decimal::Decimal;
use crate::config::OperatorConfig;
use crate::error::{PricingError, Result};
use crate::pricing_engine::{self, asset::AssetType};
use crate::types::ResourceUnit;
use crate::utils::{decimal_to_scaled_amount, percent_to_bps};
pub type BlueprintId = u64;
pub type OperatorId = Address;
#[derive(Debug, Clone)]
pub struct SignedQuote {
pub quote_details: pricing_engine::QuoteDetails,
pub abi_details: ITangleTypes::QuoteDetails,
pub signature: K256Signature,
pub recovery_id: u8,
pub operator_id: OperatorId,
pub proof_of_work: Vec<u8>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct QuoteSigningDomain {
pub chain_id: ChainId,
pub verifying_contract: Address,
}
#[derive(Debug)]
pub struct SignableQuote {
proto_details: pricing_engine::QuoteDetails,
abi_details: ITangleTypes::QuoteDetails,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum Confidentiality {
Any = 0,
Required = 1,
Preferred = 2,
}
impl SignableQuote {
pub fn new(details: pricing_engine::QuoteDetails, total_cost: Decimal) -> Result<Self> {
Self::with_confidentiality(details, total_cost, Confidentiality::Any)
}
pub fn with_confidentiality(
details: pricing_engine::QuoteDetails,
total_cost: Decimal,
confidentiality: Confidentiality,
) -> Result<Self> {
let abi_details = build_abi_quote_details(&details, total_cost, confidentiality as u8)?;
Ok(Self {
proto_details: details,
abi_details,
})
}
#[must_use]
pub fn abi_details(&self) -> &ITangleTypes::QuoteDetails {
&self.abi_details
}
}
pub struct OperatorSigner {
keypair: K256SigningKey,
operator_id: OperatorId,
domain: QuoteSigningDomain,
}
impl OperatorSigner {
pub fn new(
config: &OperatorConfig,
keypair: K256SigningKey,
domain: QuoteSigningDomain,
) -> Result<Self> {
let operator_id = keypair.alloy_address().map_err(|e| {
PricingError::Signing(format!("Failed to derive operator address: {e}"))
})?;
let _ = config;
Ok(OperatorSigner {
keypair,
operator_id,
domain,
})
}
pub fn sign_quote(
&mut self,
quote: SignableQuote,
proof_of_work: Vec<u8>,
) -> Result<SignedQuote> {
let hash = quote_digest_eip712("e.abi_details, self.domain)?;
let (signature, recovery_id) = self
.keypair
.0
.sign_prehash_recoverable(&hash)
.map_err(|e| PricingError::Signing(format!("Error signing quote hash: {e}")))?;
Ok(SignedQuote {
quote_details: quote.proto_details,
abi_details: quote.abi_details,
signature: K256Signature(signature),
recovery_id: recovery_id.to_byte(),
operator_id: self.operator_id,
proof_of_work,
})
}
pub fn operator_id(&self) -> OperatorId {
self.operator_id
}
#[must_use]
pub fn domain(&self) -> QuoteSigningDomain {
self.domain
}
pub fn verifying_key(&self) -> K256VerifyingKey {
self.keypair.verifying_key()
}
}
pub fn quote_digest_eip712(
quote_details: &ITangleTypes::QuoteDetails,
domain: QuoteSigningDomain,
) -> Result<[u8; 32]> {
let eip712_domain = alloy_sol_types::eip712_domain! {
name: "TangleQuote",
version: "1",
chain_id: domain.chain_id,
verifying_contract: domain.verifying_contract,
};
Ok(quote_details.eip712_signing_hash(&eip712_domain).into())
}
pub fn verify_quote(
quote: &SignedQuote,
public_key: &K256VerifyingKey,
domain: QuoteSigningDomain,
) -> Result<bool> {
use k256::ecdsa::signature::hazmat::PrehashVerifier;
let hash = quote_digest_eip712("e.abi_details, domain)?;
Ok(public_key
.0
.verify_prehash(&hash, "e.signature.0)
.is_ok())
}
use blueprint_tangle_extra::job_quote as jq;
#[derive(Debug, Clone)]
pub struct SignedJobQuote {
pub quote_details: crate::pricing_engine::JobQuoteDetails,
pub signature: K256Signature,
pub recovery_id: u8,
pub operator_id: OperatorId,
pub proof_of_work: Vec<u8>,
}
impl OperatorSigner {
pub fn sign_job_quote(
&mut self,
details: &crate::pricing_engine::JobQuoteDetails,
proof_of_work: Vec<u8>,
) -> Result<SignedJobQuote> {
let digest = job_quote_digest_eip712(details, self.domain)?;
let (signature, recovery_id) = self
.keypair
.0
.sign_prehash_recoverable(&digest)
.map_err(|e| PricingError::Signing(format!("Error signing job quote: {e}")))?;
Ok(SignedJobQuote {
quote_details: details.clone(),
signature: K256Signature(signature),
recovery_id: recovery_id.to_byte(),
operator_id: self.operator_id,
proof_of_work,
})
}
}
fn proto_to_native_job_quote(
details: &crate::pricing_engine::JobQuoteDetails,
) -> Result<jq::JobQuoteDetails> {
let price = if details.price.is_empty() {
U256::ZERO
} else {
U256::from_be_slice(&details.price)
};
let job_index = u8::try_from(details.job_index).map_err(|_| {
PricingError::Signing(format!(
"job_index {} exceeds u8 range (max 255)",
details.job_index
))
})?;
Ok(jq::JobQuoteDetails {
service_id: details.service_id,
job_index,
price,
timestamp: details.timestamp,
expiry: details.expiry,
confidentiality: u8::try_from(details.confidentiality).map_err(|_| {
PricingError::Signing(format!(
"confidentiality {} exceeds u8 range (max 255)",
details.confidentiality
))
})?,
})
}
fn to_jq_domain(domain: QuoteSigningDomain) -> jq::QuoteSigningDomain {
jq::QuoteSigningDomain {
chain_id: domain.chain_id,
verifying_contract: domain.verifying_contract,
}
}
pub fn job_quote_digest_eip712(
details: &crate::pricing_engine::JobQuoteDetails,
domain: QuoteSigningDomain,
) -> Result<[u8; 32]> {
let native = proto_to_native_job_quote(details)?;
Ok(jq::job_quote_digest_eip712(&native, to_jq_domain(domain)))
}
fn build_abi_quote_details(
details: &pricing_engine::QuoteDetails,
total_cost: Decimal,
confidentiality: u8,
) -> Result<ITangleTypes::QuoteDetails> {
let security_commitments = details
.security_commitments
.iter()
.map(proto_commitment_to_abi)
.collect::<Result<Vec<_>>>()?;
let resource_commitments = details
.resources
.iter()
.map(proto_resource_commitment_to_abi)
.collect::<Result<Vec<_>>>()?;
Ok(ITangleTypes::QuoteDetails {
blueprintId: details.blueprint_id,
ttlBlocks: details.ttl_blocks,
totalCost: decimal_to_scaled_amount(total_cost)?,
timestamp: details.timestamp,
expiry: details.expiry,
confidentiality,
securityCommitments: security_commitments,
resourceCommitments: resource_commitments,
})
}
fn proto_commitment_to_abi(
commitment: &pricing_engine::AssetSecurityCommitment,
) -> Result<ITangleTypes::AssetSecurityCommitment> {
let asset = commitment
.asset
.as_ref()
.ok_or_else(|| PricingError::Signing("Missing commitment asset".to_string()))?;
Ok(ITangleTypes::AssetSecurityCommitment {
asset: proto_asset_to_abi(asset)?,
exposureBps: percent_to_bps(commitment.exposure_percent)?,
})
}
fn proto_asset_to_abi(asset: &pricing_engine::Asset) -> Result<ITangleTypes::Asset> {
let asset_type = asset
.asset_type
.as_ref()
.ok_or_else(|| PricingError::Signing("missing asset type".to_string()))?;
match asset_type {
AssetType::Erc20(bytes) => {
const ERC20_ADDRESS_LEN: usize = 20;
const ERC20_KIND: u8 = ITangleTypes::AssetKind::from_underlying(1).into_underlying();
if bytes.len() != ERC20_ADDRESS_LEN {
return Err(PricingError::Signing(format!(
"ERC20 address must be 20 bytes, got {}",
bytes.len()
)));
}
let mut addr = [0u8; ERC20_ADDRESS_LEN];
addr.copy_from_slice(bytes);
Ok(ITangleTypes::Asset {
kind: ERC20_KIND,
token: Address::from(addr),
})
}
AssetType::Custom(_) => Err(PricingError::Signing(
"Custom assets are not supported on Tangle EVM".to_string(),
)),
}
}
fn proto_resource_commitment_to_abi(
resource: &pricing_engine::ResourcePricing,
) -> Result<ITangleTypes::ResourceCommitment> {
let kind =
match resource.kind.parse::<ResourceUnit>().map_err(|_| {
PricingError::Signing(format!("Invalid resource kind: {}", resource.kind))
})? {
ResourceUnit::CPU => 0,
ResourceUnit::MemoryMB => 1,
ResourceUnit::StorageMB => 2,
ResourceUnit::NetworkEgressMB => 3,
ResourceUnit::NetworkIngressMB => 4,
ResourceUnit::GPU => 5,
_ => {
return Err(PricingError::Signing(format!(
"Unsupported resource kind for tnt-core quote commitments: {}",
resource.kind
)));
}
};
Ok(ITangleTypes::ResourceCommitment {
kind,
count: resource.count,
})
}