/// NeuraMesh Marketplace — Job creation, assignment, completion with Walrus blob refs module neuramesh::marketplace { use sui::object::{Self, UID}; use sui::tx_context::{Self, TxContext}; use sui::transfer; use sui::coin::{Self, Coin}; use sui::sui::SUI; use sui::balance::{Self, Balance}; use sui::event; use sui::clock::{Self, Clock}; use std::string::{Self, String}; use std::option::{Self, Option}; use neuramesh::core::{Self, ProtocolState}; use neuramesh::registry::{Self, ComputeNode}; const ENotJobOwner: u64 = 200; const ENotAssignedNode: u64 = 201; const EInvalidJobStatus: u64 = 202; const EJobExpired: u64 = 204; const ENodeNotEligible: u64 = 205; const EDeadlineNotReached: u64 = 206; const DEFAULT_DEADLINE_MS: u64 = 3_600_000; public struct ComputeJob has key, store { id: UID, creator: address, description: String, compute_type: u8, min_gpu_tier: u8, model_blob_id: String, input_blob_id: String, result_blob_id: String, payment: Balance, status: u8, assigned_node: Option
, created_at: u64, deadline: u64, completed_at: u64, max_duration_ms: u64, } public struct JobCreated has copy, drop { job_id: address, creator: address, compute_type: u8, min_gpu_tier: u8, payment_amount: u64, model_blob_id: String, input_blob_id: String, deadline: u64, } public struct JobAssigned has copy, drop { job_id: address, node_id: address, node_owner: address, timestamp: u64, } public struct JobCompleted has copy, drop { job_id: address, node_id: address, result_blob_id: String, timestamp: u64, payment_amount: u64, } public struct JobFailed has copy, drop { job_id: address, node_id: address, reason: String, timestamp: u64, } public struct JobCancelled has copy, drop { job_id: address, creator: address, refund_amount: u64, } /// Create a new compute job with escrowed payment public entry fun create_job( state: &mut ProtocolState, clock: &Clock, description: vector, compute_type: u8, min_gpu_tier: u8, model_blob_id: vector, input_blob_id: vector, max_duration_ms: u64, payment: Coin, ctx: &mut TxContext, ) { let now = clock::timestamp_ms(clock); let duration = if (max_duration_ms == 0) { DEFAULT_DEADLINE_MS } else { max_duration_ms }; let sender = tx_context::sender(ctx); let job = ComputeJob { id: object::new(ctx), creator: sender, description: string::utf8(description), compute_type, min_gpu_tier, model_blob_id: string::utf8(model_blob_id), input_blob_id: string::utf8(input_blob_id), result_blob_id: string::utf8(b""), payment: coin::into_balance(payment), status: core::job_status_open(), assigned_node: option::none(), created_at: now, deadline: now + duration, completed_at: 0, max_duration_ms: duration, }; let job_addr = object::uid_to_address(&job.id); core::increment_jobs(state); event::emit(JobCreated { job_id: job_addr, creator: sender, compute_type, min_gpu_tier, payment_amount: balance::value(&job.payment), model_blob_id: string::utf8(model_blob_id), input_blob_id: string::utf8(input_blob_id), deadline: now + duration, }); transfer::share_object(job); } /// Node accepts an open job public entry fun accept_job( job: &mut ComputeJob, node: &mut ComputeNode, clock: &Clock, ctx: &mut TxContext, ) { let sender = tx_context::sender(ctx); assert!(job.status == core::job_status_open(), EInvalidJobStatus); assert!(registry::node_owner(node) == sender, ENotAssignedNode); assert!(registry::node_status(node) == 1, ENodeNotEligible); assert!(registry::node_gpu_tier(node) >= job.min_gpu_tier, ENodeNotEligible); let now = clock::timestamp_ms(clock); assert!(now < job.deadline, EJobExpired); let node_addr = object::uid_to_address(object::borrow_id(node)); job.status = core::job_status_assigned(); job.assigned_node = option::some(node_addr); registry::set_node_busy(node); event::emit(JobAssigned { job_id: object::uid_to_address(&job.id), node_id: node_addr, node_owner: sender, timestamp: now, }); } /// Node submits completed job with result blob ID from Walrus public entry fun complete_job( state: &mut ProtocolState, job: &mut ComputeJob, node: &mut ComputeNode, clock: &Clock, result_blob_id: vector, ctx: &mut TxContext, ) { let sender = tx_context::sender(ctx); assert!(sender == registry::node_owner(node), ENotAssignedNode); assert!( job.status == core::job_status_computing() || job.status == core::job_status_assigned(), EInvalidJobStatus, ); let node_addr = object::uid_to_address(object::borrow_id(node)); assert!(option::contains(&job.assigned_node, &node_addr), ENotAssignedNode); let now = clock::timestamp_ms(clock); job.status = core::job_status_completed(); job.result_blob_id = string::utf8(result_blob_id); job.completed_at = now; // Protocol fee let total_payment = balance::value(&job.payment); let protocol_fee = (total_payment * core::fee_bps(state)) / 10000; let node_payment = total_payment - protocol_fee; core::add_treasury(state, protocol_fee); // Pay node (full balance — fee tracked in treasury counter) let payment_coin = coin::from_balance(balance::withdraw_all(&mut job.payment), ctx); transfer::public_transfer(payment_coin, sender); registry::set_node_free(node, clock); registry::record_completion(node); let current_rep = registry::node_reputation(node); registry::update_reputation(node, current_rep + 100); core::increment_completed(state); event::emit(JobCompleted { job_id: object::uid_to_address(&job.id), node_id: node_addr, result_blob_id: string::utf8(result_blob_id), timestamp: now, payment_amount: node_payment, }); } /// Node reports failure public entry fun report_failure( job: &mut ComputeJob, node: &mut ComputeNode, clock: &Clock, reason: vector, ctx: &mut TxContext, ) { let sender = tx_context::sender(ctx); assert!(sender == registry::node_owner(node), ENotAssignedNode); let node_addr = object::uid_to_address(object::borrow_id(node)); assert!(option::contains(&job.assigned_node, &node_addr), ENotAssignedNode); job.status = core::job_status_failed(); registry::set_node_free(node, clock); registry::record_failure(node); let current_rep = registry::node_reputation(node); let new_rep = if (current_rep >= 200) { current_rep - 200 } else { 0 }; registry::update_reputation(node, new_rep); event::emit(JobFailed { job_id: object::uid_to_address(&job.id), node_id: node_addr, reason: string::utf8(reason), timestamp: clock::timestamp_ms(clock), }); } /// Creator cancels an open job public entry fun cancel_job(job: &mut ComputeJob, ctx: &mut TxContext) { let sender = tx_context::sender(ctx); assert!(sender == job.creator, ENotJobOwner); assert!(job.status == core::job_status_open(), EInvalidJobStatus); let refund_amount = balance::value(&job.payment); let refund = coin::from_balance(balance::withdraw_all(&mut job.payment), ctx); job.status = core::job_status_failed(); event::emit(JobCancelled { job_id: object::uid_to_address(&job.id), creator: sender, refund_amount, }); transfer::public_transfer(refund, sender); } /// Claim refund for expired job public entry fun claim_expired_refund( job: &mut ComputeJob, node: &mut ComputeNode, clock: &Clock, ctx: &mut TxContext, ) { let sender = tx_context::sender(ctx); assert!(sender == job.creator, ENotJobOwner); assert!( job.status == core::job_status_assigned() || job.status == core::job_status_computing(), EInvalidJobStatus, ); let now = clock::timestamp_ms(clock); assert!(now >= job.deadline, EDeadlineNotReached); let refund_amount = balance::value(&job.payment); let refund = coin::from_balance(balance::withdraw_all(&mut job.payment), ctx); job.status = core::job_status_failed(); registry::set_node_free(node, clock); registry::record_failure(node); let current_rep = registry::node_reputation(node); let new_rep = if (current_rep >= 500) { current_rep - 500 } else { 0 }; registry::update_reputation(node, new_rep); transfer::public_transfer(refund, sender); } // Accessors public fun job_creator(job: &ComputeJob): address { job.creator } public fun job_status(job: &ComputeJob): u8 { job.status } public fun job_compute_type(job: &ComputeJob): u8 { job.compute_type } public fun job_payment_amount(job: &ComputeJob): u64 { balance::value(&job.payment) } public fun job_model_blob(job: &ComputeJob): &String { &job.model_blob_id } public fun job_input_blob(job: &ComputeJob): &String { &job.input_blob_id } public fun job_result_blob(job: &ComputeJob): &String { &job.result_blob_id } public fun job_deadline(job: &ComputeJob): u64 { job.deadline } }