From 555399b95ceb8da55f2c383ed43c912292f1ebfe Mon Sep 17 00:00:00 2001 From: David-patrick-chuks Date: Thu, 26 Mar 2026 06:39:16 +0100 Subject: [PATCH] Add automated oracle milestone release flow --- backend/app/api/v1/api.py | 4 +- backend/app/api/v1/endpoints/payments.py | 85 +- backend/app/core/config.py | 8 +- backend/app/schemas/user.py | 2 +- backend/app/services/email.py | 48 + backend/app/services/payments.py | 98 + backend/app/services/soroban.py | 47 +- backend/app/tests/test_payments_oracle.py | 168 ++ contracts/escrow/src/lib.rs | 46 +- contracts/escrow/src/test.rs | 42 +- ...ath_oracle_release_funds_to_artisan.1.json | 1732 +++++++++++++++++ 11 files changed, 2250 insertions(+), 30 deletions(-) create mode 100644 backend/app/tests/test_payments_oracle.py create mode 100644 contracts/escrow/test_snapshots/test/happy_path_tests/test_happy_path_oracle_release_funds_to_artisan.1.json diff --git a/backend/app/api/v1/api.py b/backend/app/api/v1/api.py index 7f241a7..1f60df6 100644 --- a/backend/app/api/v1/api.py +++ b/backend/app/api/v1/api.py @@ -1,6 +1,6 @@ from fastapi import APIRouter -from app.api.v1.endpoints import admin, artisan, auth, booking, health, payments, user +from app.api.v1.endpoints import admin, artisan, auth, booking, health, payments, stats, user api_router = APIRouter() @@ -12,4 +12,4 @@ api_router.include_router(artisan.router, tags=["artisans"]) api_router.include_router(admin.router, tags=["admin"]) api_router.include_router(payments.router, prefix="/payments", tags=["payments"]) -api_router.include_router(stats.router, tags=["stats"]) \ No newline at end of file +api_router.include_router(stats.router, tags=["stats"]) diff --git a/backend/app/api/v1/endpoints/payments.py b/backend/app/api/v1/endpoints/payments.py index 16c2a43..4698c7d 100644 --- a/backend/app/api/v1/endpoints/payments.py +++ b/backend/app/api/v1/endpoints/payments.py @@ -1,8 +1,9 @@ # app/api/v1/endpoints/payments.py import uuid from decimal import Decimal +from typing import Any -from fastapi import APIRouter, Depends, HTTPException, status +from fastapi import APIRouter, BackgroundTasks, Depends, Header, HTTPException, status from pydantic import BaseModel, Field from sqlalchemy.orm import Session from stellar_sdk import TransactionEnvelope @@ -13,7 +14,9 @@ from app.models.booking import Booking from app.models.user import User from app.services import payments as payments_service +from app.services.email import send_auto_release_email from app.services.payments import ( + auto_release_milestone_payment, prepare_payment, refund_payment, release_payment, @@ -47,6 +50,36 @@ class RefundRequest(BaseModel): amount: Decimal = Field(..., gt=0) +class OracleAutoReleaseRequest(BaseModel): + booking_id: str + engagement_id: int = Field(..., gt=0) + token_address: str = Field(..., min_length=3) + confidence_score: float = Field(..., ge=0, le=1) + test_results: dict[str, Any] | list[str] | str + + +def _serialize_test_results(test_results: dict[str, Any] | list[str] | str) -> str: + if isinstance(test_results, str): + return test_results + if isinstance(test_results, list): + return "\n".join(f"- {item}" for item in test_results) + return "\n".join(f"- {key}: {value}" for key, value in test_results.items()) + + +def _require_oracle_token(x_oracle_token: str | None) -> None: + if not settings.BACKEND_ORACLE_TOKEN: + raise HTTPException( + status_code=status.HTTP_503_SERVICE_UNAVAILABLE, + detail="Backend oracle token is not configured", + ) + + if x_oracle_token != settings.BACKEND_ORACLE_TOKEN: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Invalid backend oracle token", + ) + + # The old /hold endpoint has been removed due to security concerns. Clients # should use the two-step prepare/submit flow instead. A request to this path # will now return 404 (FastAPI simply won't register it). @@ -170,3 +203,53 @@ def refund(req: RefundRequest, db: Session = Depends(get_db)): if res.get("status") == "error": raise HTTPException(status_code=400, detail=res.get("message")) return res + + +@router.post( + "/oracle/auto-release", + summary="Auto-release escrow through the backend oracle for high-confidence jobs", +) +def auto_release( + req: OracleAutoReleaseRequest, + background_tasks: BackgroundTasks, + db: Session = Depends(get_db), + x_oracle_token: str | None = Header(default=None, alias="X-Oracle-Token"), +): + _require_oracle_token(x_oracle_token) + rendered_test_results = _serialize_test_results(req.test_results) + result = auto_release_milestone_payment( + db, + booking_id=req.booking_id, + engagement_id=req.engagement_id, + token_address=req.token_address, + confidence_score=req.confidence_score, + test_results=rendered_test_results, + threshold=settings.AUTO_RELEASE_CONFIDENCE_THRESHOLD, + ) + + if result.get("status") == "error": + raise HTTPException(status_code=400, detail=result["message"]) + + if result.get("status") in {"success", "exists"}: + background_tasks.add_task( + send_auto_release_email, + to=result["client_email"], + full_name=result["client_name"], + booking_id=req.booking_id, + confidence_score=req.confidence_score, + transaction_hash=result["transaction_hash"], + test_results=rendered_test_results, + ) + result["client_notification"] = { + "channel": "email", + "recipient": result["client_email"], + "delivered_test_results": True, + } + else: + result["client_notification"] = { + "channel": "email", + "recipient": None, + "delivered_test_results": False, + } + + return result diff --git a/backend/app/core/config.py b/backend/app/core/config.py index a0c92e0..92a9384 100644 --- a/backend/app/core/config.py +++ b/backend/app/core/config.py @@ -2,6 +2,7 @@ from pydantic import AnyHttpUrl, field_validator from pydantic_settings import BaseSettings +from stellar_sdk import Network class Settings(BaseSettings): @@ -55,10 +56,15 @@ def assemble_cors_origins(cls, v: str | list[str]) -> list[str] | str: STRIPE_SECRET_KEY: str | None = None STRIPE_PUBLISHABLE_KEY: str | None = None - # Soroban Configuration + # Soroban Configuration SOROBAN_RPC_URL: str = "https://soroban-testnet.stellar.org" + SOROBAN_NETWORK_PASSPHRASE: str = Network.TESTNET_NETWORK_PASSPHRASE + SOROBAN_BASE_FEE: int = 300 ESCROW_CONTRACT_ID: str | None = None REPUTATION_CONTRACT_ID: str | None = None + BACKEND_SECRET_KEY: str | None = None + BACKEND_ORACLE_TOKEN: str | None = None + AUTO_RELEASE_CONFIDENCE_THRESHOLD: float = 0.90 model_config = {"env_file": ".env", "case_sensitive": True, "extra": "ignore"} diff --git a/backend/app/schemas/user.py b/backend/app/schemas/user.py index 4b443f4..eef67ba 100644 --- a/backend/app/schemas/user.py +++ b/backend/app/schemas/user.py @@ -1,7 +1,7 @@ from __future__ import annotations import re -from enum import StrEnum +from enum import Enum, StrEnum from pydantic import BaseModel, ConfigDict, EmailStr, Field, field_validator diff --git a/backend/app/services/email.py b/backend/app/services/email.py index 7187ecc..f00e09e 100644 --- a/backend/app/services/email.py +++ b/backend/app/services/email.py @@ -41,3 +41,51 @@ async def send_verification_email(to: str, full_name: str, verify_url: str) -> N fm = FastMail(conf) await fm.send_message(message) + + +async def send_auto_release_email( + to: str, + full_name: str, + booking_id: str, + confidence_score: float, + transaction_hash: str, + test_results: str, +) -> None: + """Notify the client that the backend oracle auto-released an escrow.""" + subject = f"{settings.PROJECT_NAME} - Automated milestone release completed" + body = ( + f"Hi {full_name},\n\n" + "Your milestone was automatically approved and released because the AI " + f"confidence score reached {confidence_score:.2f}, above the " + f"{settings.AUTO_RELEASE_CONFIDENCE_THRESHOLD:.2f} threshold.\n\n" + f"Booking ID: {booking_id}\n" + f"Transaction Hash: {transaction_hash}\n\n" + "Test results:\n" + f"{test_results}\n\n" + "No manual approval was required from you for this release." + ) + + message = MessageSchema( + subject=subject, + recipients=[to], + body=body, + subtype="plain", + ) + + conf = ConnectionConfig( + MAIL_USERNAME=settings.SMTP_USER or "", + MAIL_PASSWORD=settings.SMTP_PASSWORD or "", + MAIL_FROM=( + settings.EMAILS_FROM or settings.SMTP_USER or "no-reply@example.com" + ), + MAIL_PORT=settings.SMTP_PORT or 587, + MAIL_SERVER=settings.SMTP_HOST or "localhost", + MAIL_STARTTLS=settings.SMTP_TLS, + MAIL_SSL_TLS=not settings.SMTP_TLS, + USE_CREDENTIALS=True, + VALIDATE_CERTS=True, + SUPPRESS_SEND=(settings.SMTP_HOST is None or settings.SMTP_HOST == "localhost"), + ) + + fm = FastMail(conf) + await fm.send_message(message) diff --git a/backend/app/services/payments.py b/backend/app/services/payments.py index 9270017..eed340d 100644 --- a/backend/app/services/payments.py +++ b/backend/app/services/payments.py @@ -19,6 +19,7 @@ from app.models.booking import Booking from app.models.payment import Payment, PaymentStatus +from app.services.soroban import release_escrow_via_oracle # Stellar config HORIZON = os.getenv("STELLAR_HORIZON", "https://horizon-testnet.stellar.org") @@ -367,3 +368,100 @@ def submit_signed_payment(db: Session, signed_xdr: str) -> dict[str, Any]: return {"status": "error", "message": str(e)} except Exception as e: return {"status": "error", "message": f"Invalid or rejected transaction: {e}"} + + +def auto_release_milestone_payment( + db: Session, + *, + booking_id: str, + engagement_id: int, + token_address: str, + confidence_score: float, + test_results: str, + threshold: float, +) -> dict[str, Any]: + """Trigger a Soroban escrow release when the confidence score is high enough.""" + if confidence_score <= threshold: + return { + "status": "skipped", + "auto_released": False, + "reason": ( + f"Confidence score {confidence_score:.2f} did not exceed the " + f"{threshold:.2f} auto-release threshold." + ), + "confidence_score": confidence_score, + "threshold": threshold, + "test_results": test_results, + } + + try: + booking_uuid = uuid.UUID(booking_id) + except ValueError: + return {"status": "error", "message": "Booking not found"} + + booking = db.query(Booking).filter(Booking.id == booking_uuid).first() + if not booking: + return {"status": "error", "message": "Booking not found"} + + held_payment = ( + db.query(Payment) + .filter(Payment.booking_id == booking_uuid, Payment.status == PaymentStatus.HELD) + .order_by(Payment.created_at.desc()) + .first() + ) + if not held_payment: + return { + "status": "error", + "message": "No held escrow payment found for this booking", + } + + existing_release = ( + db.query(Payment) + .filter( + Payment.booking_id == booking_uuid, + Payment.status == PaymentStatus.RELEASED, + ) + .order_by(Payment.created_at.desc()) + .first() + ) + if existing_release: + return { + "status": "exists", + "auto_released": True, + "payment_id": str(existing_release.id), + "transaction_hash": existing_release.transaction_hash, + "confidence_score": confidence_score, + "threshold": threshold, + "test_results": test_results, + "client_email": booking.client.user.email, + "client_name": booking.client.user.full_name or "there", + } + + soroban_result = release_escrow_via_oracle(engagement_id, token_address) + if not soroban_result.get("success"): + return {"status": "error", "message": "Oracle release transaction failed"} + + memo = f"auto-release-{str(booking_uuid)[:15]}"[:MAX_MEMO_LENGTH] + record = _record_payment( + db, + booking_id, + soroban_result["hash"], + PaymentStatus.RELEASED, + Decimal(held_payment.amount), + os.getenv("ESCROW_CONTRACT_ID", "soroban-escrow"), + f"engagement:{engagement_id}", + memo, + ) + + return { + **record, + "auto_released": True, + "confidence_score": confidence_score, + "threshold": threshold, + "engagement_id": engagement_id, + "test_results": test_results, + "prepared_xdr": soroban_result.get("prepared_xdr"), + "signed_xdr": soroban_result.get("signed_xdr"), + "client_email": booking.client.user.email, + "client_name": booking.client.user.full_name or "there", + } diff --git a/backend/app/services/soroban.py b/backend/app/services/soroban.py index 4f3070f..3629323 100644 --- a/backend/app/services/soroban.py +++ b/backend/app/services/soroban.py @@ -4,17 +4,15 @@ """ import logging import time -from typing import Any, Dict, List, Optional +from typing import Any from stellar_sdk import ( Keypair, - Network, SorobanServer, TransactionBuilder, scval, xdr as stellar_xdr, ) -from stellar_sdk.exceptions import PrepareTransactionException, SorobanRpcError from stellar_sdk.soroban_rpc import SendTransactionStatus from app.core.config import settings @@ -59,8 +57,8 @@ def invoke_contract_function( tx = ( TransactionBuilder( source_account=source_account, - network_passphrase=Network.TESTNET_NETWORK_PASSPHRASE, - base_fee=300, + network_passphrase=SOROBAN_NETWORK_PASSPHRASE, + base_fee=settings.SOROBAN_BASE_FEE, ) .append_invoke_contract_function_op( contract_id=contract_id, @@ -81,7 +79,9 @@ def invoke_contract_function( # Prepare and sign tx = soroban_server.prepare_transaction(tx, sim_response) + prepared_xdr = tx.to_xdr() tx.sign(source_keypair) + signed_xdr = tx.to_xdr() # Submit logger.info(f"Submitting {function_name} transaction...") @@ -105,6 +105,8 @@ def invoke_contract_function( return { "success": True, "hash": tx_hash, + "prepared_xdr": prepared_xdr, + "signed_xdr": signed_xdr, "result": status_response.result_xdr, } elif status_response.status == "FAILED": @@ -124,7 +126,13 @@ def invoke_contract_function( # Contract IDs ESCROW_CONTRACT_ID = settings.ESCROW_CONTRACT_ID REPUTATION_CONTRACT_ID = settings.REPUTATION_CONTRACT_ID -BACKEND_SIGNER = Keypair.from_secret(settings.BACKEND_SECRET_KEY) + + +def get_backend_signer() -> Keypair: + if not settings.BACKEND_SECRET_KEY: + raise RuntimeError("BACKEND_SECRET_KEY must be configured for oracle signing") + + return Keypair.from_secret(settings.BACKEND_SECRET_KEY) def initialize_escrow_contract(source_keypair: Keypair) -> dict[str, Any]: @@ -157,4 +165,29 @@ def get_reputation_stats(artisan_address: str, source_keypair: Keypair) -> tuple args, source_keypair, ) - return (0, 0) \ No newline at end of file + return (0, 0) + + +def release_escrow_via_oracle( + engagement_id: int, + token_address: str, + timeout_seconds: int = 60, +) -> dict[str, Any]: + """Construct, sign, and submit a Soroban `release` invocation.""" + if not ESCROW_CONTRACT_ID: + raise RuntimeError("ESCROW_CONTRACT_ID must be configured") + + signer = get_backend_signer() + args = [ + scval.to_uint64(engagement_id), + scval.to_address(token_address), + scval.to_address(signer.public_key), + ] + + return invoke_contract_function( + ESCROW_CONTRACT_ID, + "release", + args, + signer, + timeout_seconds, + ) diff --git a/backend/app/tests/test_payments_oracle.py b/backend/app/tests/test_payments_oracle.py new file mode 100644 index 0000000..8d36e1a --- /dev/null +++ b/backend/app/tests/test_payments_oracle.py @@ -0,0 +1,168 @@ +from __future__ import annotations + +import uuid +from decimal import Decimal +from unittest.mock import AsyncMock, patch + +from app.core.security import get_password_hash +from app.models.artisan import Artisan +from app.models.booking import Booking +from app.models.client import Client +from app.models.payment import Payment, PaymentStatus +from app.models.user import User + + +def _seed_booking_with_held_payment(db_session): + hashed = get_password_hash("Password1!") + + client_user = User( + email="oracle-client@example.com", + hashed_password=hashed, + role="client", + is_verified=True, + full_name="Client Oracle", + ) + artisan_user = User( + email="oracle-artisan@example.com", + hashed_password=hashed, + role="artisan", + is_verified=True, + ) + db_session.add_all([client_user, artisan_user]) + db_session.commit() + db_session.refresh(client_user) + db_session.refresh(artisan_user) + + client_profile = Client(user_id=client_user.id) + artisan_profile = Artisan(user_id=artisan_user.id, business_name="Oracle Artisan") + db_session.add_all([client_profile, artisan_profile]) + db_session.commit() + db_session.refresh(client_profile) + db_session.refresh(artisan_profile) + + booking = Booking( + client_id=client_profile.id, + artisan_id=artisan_profile.id, + service="Inspection-backed release", + estimated_cost=Decimal("150.00"), + ) + db_session.add(booking) + db_session.commit() + db_session.refresh(booking) + + held_payment = Payment( + booking_id=booking.id, + amount=Decimal("150.0000000"), + from_account="GCLIENTHOLD", + to_account="GESCROWHOLD", + memo=f"hold-{booking.id}"[:28], + transaction_hash=f"held-{uuid.uuid4().hex[:10]}", + status=PaymentStatus.HELD, + ) + db_session.add(held_payment) + db_session.commit() + + return booking + + +def test_oracle_auto_release_triggers_and_notifies(client, db_session): + booking = _seed_booking_with_held_payment(db_session) + + with ( + patch("app.api.v1.endpoints.payments.settings.BACKEND_ORACLE_TOKEN", "oracle-secret"), + patch( + "app.services.payments.release_escrow_via_oracle", + return_value={ + "success": True, + "hash": "soroban-hash-123", + "prepared_xdr": "prepared-xdr", + "signed_xdr": "signed-xdr", + }, + ) as mock_release, + patch( + "app.api.v1.endpoints.payments.send_auto_release_email", + new_callable=AsyncMock, + ) as mock_email, + ): + response = client.post( + "/api/v1/payments/oracle/auto-release", + headers={"X-Oracle-Token": "oracle-secret"}, + json={ + "booking_id": str(booking.id), + "engagement_id": 17, + "token_address": "CDLZFC3SY3TOKEN", + "confidence_score": 0.96, + "test_results": { + "inspection_suite": "passed", + "leak_test": "passed", + "beam_alignment": "stable", + }, + }, + ) + + assert response.status_code == 200 + payload = response.json() + assert payload["status"] == "success" + assert payload["auto_released"] is True + assert payload["transaction_hash"] == "soroban-hash-123" + assert payload["prepared_xdr"] == "prepared-xdr" + assert payload["client_notification"]["recipient"] == "oracle-client@example.com" + mock_release.assert_called_once_with(17, "CDLZFC3SY3TOKEN") + mock_email.assert_awaited_once() + + released = ( + db_session.query(Payment) + .filter( + Payment.booking_id == booking.id, + Payment.status == PaymentStatus.RELEASED, + ) + .first() + ) + assert released is not None + assert released.transaction_hash == "soroban-hash-123" + + +def test_oracle_auto_release_skips_below_threshold(client, db_session): + booking = _seed_booking_with_held_payment(db_session) + + with ( + patch("app.api.v1.endpoints.payments.settings.BACKEND_ORACLE_TOKEN", "oracle-secret"), + patch("app.services.payments.release_escrow_via_oracle") as mock_release, + ): + response = client.post( + "/api/v1/payments/oracle/auto-release", + headers={"X-Oracle-Token": "oracle-secret"}, + json={ + "booking_id": str(booking.id), + "engagement_id": 19, + "token_address": "CDLZFC3SY3TOKEN", + "confidence_score": 0.89, + "test_results": ["visual check passed", "confidence below threshold"], + }, + ) + + assert response.status_code == 200 + payload = response.json() + assert payload["status"] == "skipped" + assert payload["auto_released"] is False + mock_release.assert_not_called() + + +def test_oracle_auto_release_requires_valid_token(client, db_session): + booking = _seed_booking_with_held_payment(db_session) + + with patch("app.api.v1.endpoints.payments.settings.BACKEND_ORACLE_TOKEN", "oracle-secret"): + response = client.post( + "/api/v1/payments/oracle/auto-release", + headers={"X-Oracle-Token": "wrong-token"}, + json={ + "booking_id": str(booking.id), + "engagement_id": 21, + "token_address": "CDLZFC3SY3TOKEN", + "confidence_score": 0.97, + "test_results": "oracle result bundle", + }, + ) + + assert response.status_code == 401 + assert response.json()["detail"] == "Invalid backend oracle token" diff --git a/contracts/escrow/src/lib.rs b/contracts/escrow/src/lib.rs index b223b3a..4312797 100644 --- a/contracts/escrow/src/lib.rs +++ b/contracts/escrow/src/lib.rs @@ -34,6 +34,7 @@ pub enum DataKey { Escrow(u64), NextId, Arbitrator, + Oracle, } #[contracttype] @@ -187,7 +188,7 @@ impl EscrowContract { /// Can only be called by the client and only when escrow is funded. /// Also verifies that the deadline has not passed; after the deadline the client /// must use `reclaim` to retrieve funds instead. - pub fn release(env: Env, engagement_id: u64, token: Address) { + pub fn release(env: Env, engagement_id: u64, token: Address, authority: Address) { let key = DataKey::Escrow(engagement_id); let mut escrow: Escrow = env .storage() @@ -195,8 +196,18 @@ impl EscrowContract { .get(&key) .expect("Escrow not found"); - // Auth: Require the client's signature - escrow.client.require_auth(); + authority.require_auth(); + + let oracle: Option
= env.storage().persistent().get(&DataKey::Oracle); + let is_client = authority == escrow.client; + let is_oracle = oracle + .as_ref() + .map(|configured_oracle| *configured_oracle == authority) + .unwrap_or(false); + + if !is_client && !is_oracle { + panic!("Only the client or configured oracle can release funds"); + } // Deadline check: prevent releasing funds after deadline has passed let current_time = env.ledger().timestamp(); @@ -225,6 +236,17 @@ impl EscrowContract { .extend_ttl(&key, TTL_THRESHOLD, ESCROW_TTL); } + /// Set the backend oracle address that may auto-release funded escrows. + pub fn set_oracle(env: Env, admin: Address, oracle: Address) { + admin.require_auth(); + + let oracle_key = DataKey::Oracle; + env.storage().persistent().set(&oracle_key, &oracle); + env.storage() + .persistent() + .extend_ttl(&oracle_key, TTL_THRESHOLD, NEXT_ID_TTL); + } + /// Allow the client to reclaim funds after the deadline has passed when an escrow is still funded. /// /// Transfers the amount back to the client, updates the status to `Refunded`, and emits a @@ -372,11 +394,7 @@ impl EscrowContract { // Transfer funds to the winner let token_client = token::Client::new(&env, &token); - token_client.transfer( - &env.current_contract_address(), - &winner, - &escrow.amount, - ); + token_client.transfer(&env.current_contract_address(), &winner, &escrow.amount); // Update status based on winner if winner == escrow.client { @@ -772,7 +790,7 @@ mod test_legacy { token_client.transfer(&client_address, &contract_id, &amount); // Releasing after deadline should panic - client.release(&engagement_id, &token_address); + client.release(&engagement_id, &token_address, &client_address); } #[test] @@ -1009,7 +1027,7 @@ mod test_legacy { token_client.transfer(&client_address, &contract_id, &amount); // release back to artist first - client.release(&engagement_id, &token_address); + client.release(&engagement_id, &token_address, &client_address); // attempt reclaim after it's been released client.reclaim(&engagement_id, &token_address); } @@ -1062,7 +1080,7 @@ mod test_legacy { assert_eq!(token_client.balance(&artisan_address), 0); assert_eq!(token_client.balance(&contract_id), amount); - client.release(&engagement_id, &token_address); + client.release(&engagement_id, &token_address, &client_address); assert_eq!(token_client.balance(&artisan_address), amount); assert_eq!(token_client.balance(&contract_id), 0); @@ -1101,7 +1119,7 @@ mod test_legacy { .set(&DataKey::Escrow(engagement_id), &escrow); }); - client.release(&engagement_id, &token_address); + client.release(&engagement_id, &token_address, &client_address); } #[test] @@ -1143,7 +1161,7 @@ mod test_legacy { token_client.transfer(&client_address, &contract_id, &amount); - client.release(&engagement_id, &token_address); - client.release(&engagement_id, &token_address); + client.release(&engagement_id, &token_address, &client_address); + client.release(&engagement_id, &token_address, &client_address); } } diff --git a/contracts/escrow/src/test.rs b/contracts/escrow/src/test.rs index 2ca3684..31ef188 100644 --- a/contracts/escrow/src/test.rs +++ b/contracts/escrow/src/test.rs @@ -73,8 +73,17 @@ mod happy_path_tests { /// Release funds from an escrow fn release_funds(&self, engagement_id: u64) { + self.client_contract.release( + &engagement_id, + &self.token_address, + &self.get_escrow(engagement_id).client, + ); + } + + /// Release funds from an escrow using a configured oracle signer + fn release_funds_as_oracle(&self, engagement_id: u64, oracle: &Address) { self.client_contract - .release(&engagement_id, &self.token_address); + .release(&engagement_id, &self.token_address, oracle); } /// Full workflow: initialize, mint, deposit @@ -360,6 +369,30 @@ mod happy_path_tests { assert_eq!(escrow.artisan, artisan); } + /// Test 10b: Backend oracle can auto-release a funded escrow + #[test] + fn test_happy_path_oracle_release_funds_to_artisan() { + let ctx = TestContext::new(); + let (client, artisan) = create_addresses(&ctx.env); + let admin = Address::generate(&ctx.env); + let oracle = Address::generate(&ctx.env); + let amount: i128 = 5000; + + ctx.client_contract.set_oracle(&admin, &oracle); + let engagement_id = ctx.full_deposit_workflow(&client, &artisan, amount); + + let artisan_balance_before = ctx.token_client.balance(&artisan); + ctx.release_funds_as_oracle(engagement_id, &oracle); + + assert_eq!( + ctx.token_client.balance(&artisan), + artisan_balance_before + amount, + "Oracle release should deliver the escrow amount" + ); + assert_eq!(ctx.token_client.balance(&ctx.contract_id), 0); + assert_eq!(ctx.get_escrow(engagement_id).status, Status::Released); + } + /// Test 11: Deadline lifecycle – deposit before deadline, reclaim after expiry #[test] fn test_happy_path_deadline_lifecycle() { @@ -495,8 +528,8 @@ mod happy_path_tests { let engagement_id = ctx.full_deposit_workflow(&client, &artisan, amount); // Third party attempts to dispute - let unauthorized = Address::generate(&ctx.env); - ctx.client_contract.dispute(&engagement_id, &unauthorized); + let _unauthorized = Address::generate(&ctx.env); + ctx.client_contract.dispute(&engagement_id, &_unauthorized); } /// Test 17: Arbitrate - arbitrator resolves dispute in favor of client (refund) @@ -660,4 +693,5 @@ mod happy_path_tests { // Try to arbitrate without arbitrator set ctx.client_contract .arbitrate(&engagement_id, &client, &ctx.token_address); - }} + } +} diff --git a/contracts/escrow/test_snapshots/test/happy_path_tests/test_happy_path_oracle_release_funds_to_artisan.1.json b/contracts/escrow/test_snapshots/test/happy_path_tests/test_happy_path_oracle_release_funds_to_artisan.1.json new file mode 100644 index 0000000..df3395c --- /dev/null +++ b/contracts/escrow/test_snapshots/test/happy_path_tests/test_happy_path_oracle_release_funds_to_artisan.1.json @@ -0,0 +1,1732 @@ +{ + "generators": { + "address": 7, + "nonce": 0 + }, + "auth": [ + [ + [ + "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGO6V", + { + "function": { + "contract_fn": { + "contract_address": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "function_name": "set_admin", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ] + } + }, + "sub_invocations": [] + } + ] + ], + [ + [ + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMDR4", + { + "function": { + "contract_fn": { + "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "function_name": "set_oracle", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMDR4" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOLZM" + } + ] + } + }, + "sub_invocations": [] + } + ] + ], + [], + [ + [ + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + { + "function": { + "contract_fn": { + "contract_address": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "function_name": "mint", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + }, + { + "i128": { + "hi": 0, + "lo": 5000 + } + } + ] + } + }, + "sub_invocations": [] + } + ] + ], + [ + [ + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + { + "function": { + "contract_fn": { + "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "function_name": "deposit", + "args": [ + { + "u64": 1 + }, + { + "address": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF" + } + ] + } + }, + "sub_invocations": [ + { + "function": { + "contract_fn": { + "contract_address": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "function_name": "transfer", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM" + }, + { + "i128": { + "hi": 0, + "lo": 5000 + } + } + ] + } + }, + "sub_invocations": [] + } + ] + } + ] + ], + [], + [ + [ + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOLZM", + { + "function": { + "contract_fn": { + "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "function_name": "release", + "args": [ + { + "u64": 1 + }, + { + "address": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOLZM" + } + ] + } + }, + "sub_invocations": [] + } + ] + ], + [], + [], + [] + ], + "ledger": { + "protocol_version": 21, + "sequence_number": 0, + "timestamp": 0, + "network_id": "0000000000000000000000000000000000000000000000000000000000000000", + "base_reserve": 0, + "min_persistent_entry_ttl": 4096, + "min_temp_entry_ttl": 16, + "max_entry_ttl": 6312000, + "ledger_entries": [ + [ + { + "account": { + "account_id": "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGO6V" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "account": { + "account_id": "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGO6V", + "balance": 0, + "seq_num": 0, + "num_sub_entries": 0, + "inflation_dest": null, + "flags": 0, + "home_domain": "", + "thresholds": "01010101", + "signers": [], + "ext": "v0" + } + }, + "ext": "v0" + }, + null + ] + ], + [ + { + "contract_data": { + "contract": "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGO6V", + "key": { + "ledger_key_nonce": { + "nonce": 801925984706572462 + } + }, + "durability": "temporary" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGO6V", + "key": { + "ledger_key_nonce": { + "nonce": 801925984706572462 + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + 6311999 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "vec": [ + { + "symbol": "Escrow" + }, + { + "u64": 1 + } + ] + }, + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "vec": [ + { + "symbol": "Escrow" + }, + { + "u64": 1 + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "amount" + }, + "val": { + "i128": { + "hi": 0, + "lo": 5000 + } + } + }, + { + "key": { + "symbol": "artisan" + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM" + } + }, + { + "key": { + "symbol": "client" + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + } + }, + { + "key": { + "symbol": "deadline" + }, + "val": { + "u64": 86400 + } + }, + { + "key": { + "symbol": "status" + }, + "val": { + "vec": [ + { + "symbol": "Released" + } + ] + } + } + ] + } + } + }, + "ext": "v0" + }, + 1036800 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "vec": [ + { + "symbol": "NextId" + } + ] + }, + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "vec": [ + { + "symbol": "NextId" + } + ] + }, + "durability": "persistent", + "val": { + "u64": 2 + } + } + }, + "ext": "v0" + }, + 6220800 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "vec": [ + { + "symbol": "Oracle" + } + ] + }, + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "vec": [ + { + "symbol": "Oracle" + } + ] + }, + "durability": "persistent", + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOLZM" + } + } + }, + "ext": "v0" + }, + 6220800 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": "ledger_key_contract_instance", + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": null + } + } + } + }, + "ext": "v0" + }, + 4095 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "ledger_key_nonce": { + "nonce": 1033654523790656264 + } + }, + "durability": "temporary" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "ledger_key_nonce": { + "nonce": 1033654523790656264 + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + 6311999 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "key": { + "ledger_key_nonce": { + "nonce": 4837995959683129791 + } + }, + "durability": "temporary" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "key": { + "ledger_key_nonce": { + "nonce": 4837995959683129791 + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + 6311999 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMDR4", + "key": { + "ledger_key_nonce": { + "nonce": 5541220902715666415 + } + }, + "durability": "temporary" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMDR4", + "key": { + "ledger_key_nonce": { + "nonce": 5541220902715666415 + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + 6311999 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOLZM", + "key": { + "ledger_key_nonce": { + "nonce": 2032731177588607455 + } + }, + "durability": "temporary" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOLZM", + "key": { + "ledger_key_nonce": { + "nonce": 2032731177588607455 + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + 6311999 + ] + ], + [ + { + "contract_data": { + "contract": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "key": { + "vec": [ + { + "symbol": "Balance" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM" + } + ] + }, + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "key": { + "vec": [ + { + "symbol": "Balance" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM" + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "amount" + }, + "val": { + "i128": { + "hi": 0, + "lo": 0 + } + } + }, + { + "key": { + "symbol": "authorized" + }, + "val": { + "bool": true + } + }, + { + "key": { + "symbol": "clawback" + }, + "val": { + "bool": false + } + } + ] + } + } + }, + "ext": "v0" + }, + 518400 + ] + ], + [ + { + "contract_data": { + "contract": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "key": { + "vec": [ + { + "symbol": "Balance" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + } + ] + }, + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "key": { + "vec": [ + { + "symbol": "Balance" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "amount" + }, + "val": { + "i128": { + "hi": 0, + "lo": 0 + } + } + }, + { + "key": { + "symbol": "authorized" + }, + "val": { + "bool": true + } + }, + { + "key": { + "symbol": "clawback" + }, + "val": { + "bool": false + } + } + ] + } + } + }, + "ext": "v0" + }, + 518400 + ] + ], + [ + { + "contract_data": { + "contract": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "key": { + "vec": [ + { + "symbol": "Balance" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM" + } + ] + }, + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "key": { + "vec": [ + { + "symbol": "Balance" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM" + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "amount" + }, + "val": { + "i128": { + "hi": 0, + "lo": 5000 + } + } + }, + { + "key": { + "symbol": "authorized" + }, + "val": { + "bool": true + } + }, + { + "key": { + "symbol": "clawback" + }, + "val": { + "bool": false + } + } + ] + } + } + }, + "ext": "v0" + }, + 518400 + ] + ], + [ + { + "contract_data": { + "contract": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "key": "ledger_key_contract_instance", + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": "stellar_asset", + "storage": [ + { + "key": { + "symbol": "METADATA" + }, + "val": { + "map": [ + { + "key": { + "symbol": "decimal" + }, + "val": { + "u32": 7 + } + }, + { + "key": { + "symbol": "name" + }, + "val": { + "string": "aaa:GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGO6V" + } + }, + { + "key": { + "symbol": "symbol" + }, + "val": { + "string": "aaa" + } + } + ] + } + }, + { + "key": { + "vec": [ + { + "symbol": "Admin" + } + ] + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + }, + { + "key": { + "vec": [ + { + "symbol": "AssetInfo" + } + ] + }, + "val": { + "vec": [ + { + "symbol": "AlphaNum4" + }, + { + "map": [ + { + "key": { + "symbol": "asset_code" + }, + "val": { + "string": "aaa\\0" + } + }, + { + "key": { + "symbol": "issuer" + }, + "val": { + "bytes": "0000000000000000000000000000000000000000000000000000000000000003" + } + } + ] + } + ] + } + } + ] + } + } + } + }, + "ext": "v0" + }, + 120960 + ] + ], + [ + { + "contract_code": { + "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_code": { + "ext": "v0", + "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "code": "" + } + }, + "ext": "v0" + }, + 4095 + ] + ] + ] + }, + "events": [ + { + "event": { + "ext": "v0", + "contract_id": null, + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_call" + }, + { + "bytes": "692c360a04a982db02db346a106cbf008ad9e058c384bdaaf77bc0c48799b3a4" + }, + { + "symbol": "init_asset" + } + ], + "data": { + "bytes": "0000000161616100000000000000000000000000000000000000000000000000000000000000000000000003" + } + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": "692c360a04a982db02db346a106cbf008ad9e058c384bdaaf77bc0c48799b3a4", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_return" + }, + { + "symbol": "init_asset" + } + ], + "data": "void" + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": null, + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_call" + }, + { + "bytes": "692c360a04a982db02db346a106cbf008ad9e058c384bdaaf77bc0c48799b3a4" + }, + { + "symbol": "set_admin" + } + ], + "data": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": "692c360a04a982db02db346a106cbf008ad9e058c384bdaaf77bc0c48799b3a4", + "type_": "contract", + "body": { + "v0": { + "topics": [ + { + "symbol": "set_admin" + }, + { + "address": "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGO6V" + }, + { + "string": "aaa:GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGO6V" + } + ], + "data": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": "692c360a04a982db02db346a106cbf008ad9e058c384bdaaf77bc0c48799b3a4", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_return" + }, + { + "symbol": "set_admin" + } + ], + "data": "void" + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": null, + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_call" + }, + { + "bytes": "0000000000000000000000000000000000000000000000000000000000000001" + }, + { + "symbol": "set_oracle" + } + ], + "data": { + "vec": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMDR4" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOLZM" + } + ] + } + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": "0000000000000000000000000000000000000000000000000000000000000001", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_return" + }, + { + "symbol": "set_oracle" + } + ], + "data": "void" + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": null, + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_call" + }, + { + "bytes": "0000000000000000000000000000000000000000000000000000000000000001" + }, + { + "symbol": "initialize" + } + ], + "data": { + "vec": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM" + }, + { + "i128": { + "hi": 0, + "lo": 5000 + } + }, + { + "u64": 86400 + } + ] + } + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": "0000000000000000000000000000000000000000000000000000000000000001", + "type_": "contract", + "body": { + "v0": { + "topics": [], + "data": { + "map": [ + { + "key": { + "symbol": "artisan" + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM" + } + }, + { + "key": { + "symbol": "client" + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + } + }, + { + "key": { + "symbol": "id" + }, + "val": { + "u64": 1 + } + } + ] + } + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": "0000000000000000000000000000000000000000000000000000000000000001", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_return" + }, + { + "symbol": "initialize" + } + ], + "data": { + "u64": 1 + } + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": null, + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_call" + }, + { + "bytes": "692c360a04a982db02db346a106cbf008ad9e058c384bdaaf77bc0c48799b3a4" + }, + { + "symbol": "mint" + } + ], + "data": { + "vec": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + }, + { + "i128": { + "hi": 0, + "lo": 5000 + } + } + ] + } + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": "692c360a04a982db02db346a106cbf008ad9e058c384bdaaf77bc0c48799b3a4", + "type_": "contract", + "body": { + "v0": { + "topics": [ + { + "symbol": "mint" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + }, + { + "string": "aaa:GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGO6V" + } + ], + "data": { + "i128": { + "hi": 0, + "lo": 5000 + } + } + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": "692c360a04a982db02db346a106cbf008ad9e058c384bdaaf77bc0c48799b3a4", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_return" + }, + { + "symbol": "mint" + } + ], + "data": "void" + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": null, + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_call" + }, + { + "bytes": "0000000000000000000000000000000000000000000000000000000000000001" + }, + { + "symbol": "deposit" + } + ], + "data": { + "vec": [ + { + "u64": 1 + }, + { + "address": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF" + } + ] + } + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": "0000000000000000000000000000000000000000000000000000000000000001", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_call" + }, + { + "bytes": "692c360a04a982db02db346a106cbf008ad9e058c384bdaaf77bc0c48799b3a4" + }, + { + "symbol": "transfer" + } + ], + "data": { + "vec": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM" + }, + { + "i128": { + "hi": 0, + "lo": 5000 + } + } + ] + } + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": "692c360a04a982db02db346a106cbf008ad9e058c384bdaaf77bc0c48799b3a4", + "type_": "contract", + "body": { + "v0": { + "topics": [ + { + "symbol": "transfer" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM" + }, + { + "string": "aaa:GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGO6V" + } + ], + "data": { + "i128": { + "hi": 0, + "lo": 5000 + } + } + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": "692c360a04a982db02db346a106cbf008ad9e058c384bdaaf77bc0c48799b3a4", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_return" + }, + { + "symbol": "transfer" + } + ], + "data": "void" + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": "0000000000000000000000000000000000000000000000000000000000000001", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_return" + }, + { + "symbol": "deposit" + } + ], + "data": "void" + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": null, + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_call" + }, + { + "bytes": "692c360a04a982db02db346a106cbf008ad9e058c384bdaaf77bc0c48799b3a4" + }, + { + "symbol": "balance" + } + ], + "data": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM" + } + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": "692c360a04a982db02db346a106cbf008ad9e058c384bdaaf77bc0c48799b3a4", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_return" + }, + { + "symbol": "balance" + } + ], + "data": { + "i128": { + "hi": 0, + "lo": 0 + } + } + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": null, + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_call" + }, + { + "bytes": "0000000000000000000000000000000000000000000000000000000000000001" + }, + { + "symbol": "release" + } + ], + "data": { + "vec": [ + { + "u64": 1 + }, + { + "address": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOLZM" + } + ] + } + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": "0000000000000000000000000000000000000000000000000000000000000001", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_call" + }, + { + "bytes": "692c360a04a982db02db346a106cbf008ad9e058c384bdaaf77bc0c48799b3a4" + }, + { + "symbol": "transfer" + } + ], + "data": { + "vec": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM" + }, + { + "i128": { + "hi": 0, + "lo": 5000 + } + } + ] + } + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": "692c360a04a982db02db346a106cbf008ad9e058c384bdaaf77bc0c48799b3a4", + "type_": "contract", + "body": { + "v0": { + "topics": [ + { + "symbol": "transfer" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM" + }, + { + "string": "aaa:GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGO6V" + } + ], + "data": { + "i128": { + "hi": 0, + "lo": 5000 + } + } + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": "692c360a04a982db02db346a106cbf008ad9e058c384bdaaf77bc0c48799b3a4", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_return" + }, + { + "symbol": "transfer" + } + ], + "data": "void" + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": "0000000000000000000000000000000000000000000000000000000000000001", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_return" + }, + { + "symbol": "release" + } + ], + "data": "void" + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": null, + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_call" + }, + { + "bytes": "692c360a04a982db02db346a106cbf008ad9e058c384bdaaf77bc0c48799b3a4" + }, + { + "symbol": "balance" + } + ], + "data": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM" + } + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": "692c360a04a982db02db346a106cbf008ad9e058c384bdaaf77bc0c48799b3a4", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_return" + }, + { + "symbol": "balance" + } + ], + "data": { + "i128": { + "hi": 0, + "lo": 5000 + } + } + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": null, + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_call" + }, + { + "bytes": "692c360a04a982db02db346a106cbf008ad9e058c384bdaaf77bc0c48799b3a4" + }, + { + "symbol": "balance" + } + ], + "data": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM" + } + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": "692c360a04a982db02db346a106cbf008ad9e058c384bdaaf77bc0c48799b3a4", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_return" + }, + { + "symbol": "balance" + } + ], + "data": { + "i128": { + "hi": 0, + "lo": 0 + } + } + } + } + }, + "failed_call": false + } + ] +} \ No newline at end of file