Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

实战案例 18:跨联盟外交合约(停火与资源条约)

目标: 构建链上外交合约——两个联盟可以签署条约(停火、资源共享、贸易协定),条约由双方 Leader 多签生效,违约可在链上举证,有效期内强制执行。


状态:教学示例。正文覆盖条约状态机,完整目录以 book/src/code/example-18/ 为准。

对应代码目录

最小调用链

一方发起提案 -> 双方存押金并签署 -> 条约生效 -> 发生违约/撕约 -> 扣罚或退还押金

测试闭环

  • 发起提案:确认 TreatyProposal 创建成功并发出事件
  • 双签生效:确认 effective_at_ms 写入,双方押金对等
  • 提前通知与终止:确认通知期未成熟前无法终止,成熟后押金退回
  • 举报违约:确认罚款从违约方押金扣出并转给对方

需求分析

场景: 联盟 Alpha 和联盟 Beta 爆发冲突,双方决定谈判:

  1. 停火协议:72 小时内双方炮塔不对对方成员开火
  2. 过路协议:Alpha 成员可免费使用 Beta 的星门(反之亦然)
  3. 资源分享:双方每日互相转账 100 WAR Token
  4. 任一方可以单方面撕毁条约(需提前 24 小时通知链上)
  5. 违约行为(如炮塔非法开火)可以通过服务器签名举报,罚款押金

合约

module diplomacy::treaty;

use sui::object::{Self, UID, ID};
use sui::clock::Clock;
use sui::coin::{Self, Coin};
use sui::sui::SUI;
use sui::balance::{Self, Balance};
use sui::transfer;
use sui::event;
use std::string::{Self, String, utf8};

// ── 常量 ──────────────────────────────────────────────────

const NOTICE_PERIOD_MS: u64 = 24 * 60 * 60 * 1000;  // 撕约提前通知 24 小时
const BREACH_FINE: u64 = 100_000_000_000;             // 违约罚款 100 SUI(从押金扣)

// 条约类型
const TREATY_CEASEFIRE: u8 = 0;       // 停火协议
const TREATY_PASSAGE: u8 = 1;         // 过路权协议
const TREATY_RESOURCE_SHARE: u8 = 2;  // 资源共享

// ── 数据结构 ───────────────────────────────────────────────

/// 外交条约(共享对象)
public struct Treaty has key {
    id: UID,
    treaty_type: u8,
    party_a: address,          // 联盟 A 的 Leader 地址
    party_b: address,          // 联盟 B 的 Leader 地址
    party_a_signed: bool,
    party_b_signed: bool,
    effective_at_ms: u64,      // 生效时间(双签后)
    expires_at_ms: u64,        // 到期时间(0 = 无限期)
    termination_notice_ms: u64, // 撕约通知时间(0 = 未通知)
    party_a_deposit: Balance<SUI>,  // A 方押金(用于违约赔偿)
    party_b_deposit: Balance<SUI>,  // B 方押金
    breach_count_a: u64,
    breach_count_b: u64,
    description: String,
}

/// 条约提案(由一方发起,等待对方签署)
public struct TreatyProposal has key {
    id: UID,
    proposed_by: address,
    counterparty: address,
    treaty_type: u8,
    duration_days: u64,        // 有效期(天),0 = 无限期
    deposit_required: u64,      // 要求各方押金
    description: String,
}

// ── 事件 ──────────────────────────────────────────────────

public struct TreatyProposed has copy, drop { proposal_id: ID, proposer: address, counterparty: address }
public struct TreatySigned has copy, drop { treaty_id: ID, party: address }
public struct TreatyEffective has copy, drop { treaty_id: ID, treaty_type: u8 }
public struct TreatyTerminated has copy, drop { treaty_id: ID, terminated_by: address }
public struct BreachReported has copy, drop { treaty_id: ID, breaching_party: address, fine: u64 }

// ── 发起条约提案 ──────────────────────────────────────────

public fun propose_treaty(
    counterparty: address,
    treaty_type: u8,
    duration_days: u64,
    deposit_required: u64,
    description: vector<u8>,
    ctx: &mut TxContext,
) {
    let proposal = TreatyProposal {
        id: object::new(ctx),
        proposed_by: ctx.sender(),
        counterparty,
        treaty_type,
        duration_days,
        deposit_required,
        description: utf8(description),
    };
    let proposal_id = object::id(&proposal);
    transfer::share_object(proposal);
    event::emit(TreatyProposed {
        proposal_id,
        proposer: ctx.sender(),
        counterparty,
    });
}

// ── 接受提案(发起方签署 + 押金)────────────────────────

public fun accept_and_sign_a(
    proposal: &TreatyProposal,
    mut deposit: Coin<SUI>,
    clock: &Clock,
    ctx: &mut TxContext,
) {
    assert!(ctx.sender() == proposal.proposed_by, ENotParty);

    let deposit_amt = coin::value(&deposit);
    assert!(deposit_amt >= proposal.deposit_required, EInsufficientDeposit);

    let deposit_coin = deposit.split(proposal.deposit_required, ctx);
    if coin::value(&deposit) > 0 {
        transfer::public_transfer(deposit, ctx.sender());
    } else { coin::destroy_zero(deposit); }

    let expires = if proposal.duration_days > 0 {
        clock.timestamp_ms() + proposal.duration_days * 86_400_000
    } else { 0 };

    let treaty = Treaty {
        id: object::new(ctx),
        treaty_type: proposal.treaty_type,
        party_a: proposal.proposed_by,
        party_b: proposal.counterparty,
        party_a_signed: true,
        party_b_signed: false,
        effective_at_ms: 0,
        expires_at_ms: expires,
        termination_notice_ms: 0,
        party_a_deposit: coin::into_balance(deposit_coin),
        party_b_deposit: balance::zero(),
        breach_count_a: 0,
        breach_count_b: 0,
        description: proposal.description,
    };
    let treaty_id = object::id(&treaty);
    transfer::share_object(treaty);
    event::emit(TreatySigned { treaty_id, party: ctx.sender() });
}

/// 对方联盟签署(条约正式生效)
public fun countersign(
    treaty: &mut Treaty,
    mut deposit: Coin<SUI>,
    clock: &Clock,
    ctx: &mut TxContext,
) {
    assert!(ctx.sender() == treaty.party_b, ENotParty);
    assert!(treaty.party_a_signed, ENotYetSigned);
    assert!(!treaty.party_b_signed, EAlreadySigned);

    let required = balance::value(&treaty.party_a_deposit); // 对等押金
    assert!(coin::value(&deposit) >= required, EInsufficientDeposit);

    let dep = deposit.split(required, ctx);
    balance::join(&mut treaty.party_b_deposit, coin::into_balance(dep));
    if coin::value(&deposit) > 0 {
        transfer::public_transfer(deposit, ctx.sender());
    } else { coin::destroy_zero(deposit); }

    treaty.party_b_signed = true;
    treaty.effective_at_ms = clock.timestamp_ms();

    event::emit(TreatyEffective { treaty_id: object::id(treaty), treaty_type: treaty.treaty_type });
    event::emit(TreatySigned { treaty_id: object::id(treaty), party: ctx.sender() });
}

// ── 验证条约是否生效(炮塔/星门扩展调用)───────────────

public fun is_treaty_active(treaty: &Treaty, clock: &Clock): bool {
    if !treaty.party_a_signed || !treaty.party_b_signed { return false };
    if treaty.expires_at_ms > 0 && clock.timestamp_ms() > treaty.expires_at_ms { return false };
    // 撕约通知期内,条约仍然有效
    true
}

/// 检查某地址是否在条约保护下
public fun is_protected_by_treaty(
    treaty: &Treaty,
    protected_member: address, // 受保护的联盟成员(通过 FactionNFT.owner 或 member 列表核查)
    aggressor_faction: address,
    clock: &Clock,
): bool {
    is_treaty_active(treaty, clock)
    // 真实场景中需要额外核查成员与联盟的关联
}

// ── 提交撕约通知(24 小时后生效)───────────────────────

public fun give_termination_notice(
    treaty: &mut Treaty,
    clock: &Clock,
    ctx: &TxContext,
) {
    assert!(ctx.sender() == treaty.party_a || ctx.sender() == treaty.party_b, ENotParty);
    assert!(is_treaty_active(treaty, clock), ETreatyNotActive);
    treaty.termination_notice_ms = clock.timestamp_ms();
    event::emit(TreatyTerminated { treaty_id: object::id(treaty), terminated_by: ctx.sender() });
}

/// 通知期满后正式终止条约,双方取回押金
public fun finalize_termination(
    treaty: &mut Treaty,
    clock: &Clock,
    ctx: &mut TxContext,
) {
    assert!(treaty.termination_notice_ms > 0, ENoNoticeGiven);
    assert!(
        clock.timestamp_ms() >= treaty.termination_notice_ms + NOTICE_PERIOD_MS,
        ENoticeNotMature,
    );
    // 退还押金
    let a_dep = balance::withdraw_all(&mut treaty.party_a_deposit);
    let b_dep = balance::withdraw_all(&mut treaty.party_b_deposit);
    if balance::value(&a_dep) > 0 {
        transfer::public_transfer(coin::from_balance(a_dep, ctx), treaty.party_a);
    } else { balance::destroy_zero(a_dep); }
    if balance::value(&b_dep) > 0 {
        transfer::public_transfer(coin::from_balance(b_dep, ctx), treaty.party_b);
    } else { balance::destroy_zero(b_dep); }
}

// ── 举报违约(由游戏服务器验证并签名)──────────────────

public fun report_breach(
    treaty: &mut Treaty,
    breaching_party: address,  // 违约联盟的 Leader 地址
    admin_acl: &AdminACL,
    ctx: &mut TxContext,
) {
    verify_sponsor(admin_acl, ctx);  // 服务器证明违约事件真实发生

    let fine = BREACH_FINE;

    if breaching_party == treaty.party_a {
        treaty.breach_count_a = treaty.breach_count_a + 1;
        // 从 A 的押金中扣除罚款转给 B
        if balance::value(&treaty.party_a_deposit) >= fine {
            let fine_coin = coin::take(&mut treaty.party_a_deposit, fine, ctx);
            transfer::public_transfer(fine_coin, treaty.party_b);
        }
    } else if breaching_party == treaty.party_b {
        treaty.breach_count_b = treaty.breach_count_b + 1;
        if balance::value(&treaty.party_b_deposit) >= fine {
            let fine_coin = coin::take(&mut treaty.party_b_deposit, fine, ctx);
            transfer::public_transfer(fine_coin, treaty.party_a);
        }
    } else abort ENotParty;

    event::emit(BreachReported {
        treaty_id: object::id(treaty),
        breaching_party,
        fine,
    });
}

const ENotParty: u64 = 0;
const EInsufficientDeposit: u64 = 1;
const ENotYetSigned: u64 = 2;
const EAlreadySigned: u64 = 3;
const ETreatyNotActive: u64 = 4;
const ENoNoticeGiven: u64 = 5;
const ENoticeNotMature: u64 = 6;

dApp(外交中心)

// DiplomacyCenter.tsx
import { useState } from 'react'
import { useCurrentClient } from '@mysten/dapp-kit-react'
import { useQuery } from '@tanstack/react-query'

const DIP_PKG = "0x_DIPLOMACY_PACKAGE_"

const TREATY_TYPES = [
  { id: 0, name: '⚔️ 停火协议', desc: '双方在有效期内不得发起攻击' },
  { id: 1, name: '🚪 过路权协议', desc: '双方成员可免费使用对方星门' },
  { id: 2, name: '💰 资源共享协议', desc: '定期相互转移资源' },
]

export function DiplomacyCenter() {
  const client = useCurrentClient()
  const [proposing, setProposing] = useState(false)

  const { data: treaties } = useQuery({
    queryKey: ['active-treaties'],
    queryFn: async () => {
      const events = await client.queryEvents({
        query: { MoveEventType: `${DIP_PKG}::treaty::TreatyEffective` },
        limit: 20,
      })
      return events.data
    },
    refetchInterval: 30_000,
  })

  return (
    <div className="diplomacy-center">
      <header>
        <h1>🌐 跨联盟外交中心</h1>
        <p>在链上签署具有法律效力的联盟条约</p>
      </header>

      <section className="treaty-types">
        <h3>可签署的条约类型</h3>
        <div className="types-grid">
          {TREATY_TYPES.map(t => (
            <div key={t.id} className="type-card">
              <h4>{t.name}</h4>
              <p>{t.desc}</p>
            </div>
          ))}
        </div>
      </section>

      <section className="active-treaties">
        <h3>当前生效条约</h3>
        {treaties?.length === 0 && <p>暂无条约</p>}
        {treaties?.map(e => {
          const { treaty_id, treaty_type } = e.parsedJson as any
          const type = TREATY_TYPES[Number(treaty_type)]
          return (
            <div key={treaty_id} className="treaty-card">
              <span className="treaty-type">{type?.name}</span>
              <span className="treaty-id">{treaty_id.slice(0, 12)}...</span>
              <span className="treaty-status active">✅ 生效中</span>
            </div>
          )
        })}
      </section>

      <button className="propose-btn" onClick={() => setProposing(true)}>
        📝 提议新条约
      </button>
    </div>
  )
}

🎯 关键设计亮点

机制实现方式
双签生效party_a_signed + party_b_signed 都为 true 才生效
押金约束争端双方各存押金,违约自动扣罚
撕约通知termination_notice_ms + 24 小时冷静期
违约举证游戏服务器 AdminACL 签名证明,自动执行罚款
条约核查is_treaty_active() 供炮塔/星门扩展调用

📚 关联文档