Rust区块链开发库pallet-tips的使用,实现去中心化小费支付和激励机制
Rust区块链开发库pallet-tips的使用,实现去中心化小费支付和激励机制
Tipping Pallet (pallet-tips)
注意:此pallet与pallet-treasury紧密耦合
这是一个允许灵活"打赏"过程的子系统,可以在没有预先确定的利益相关者群体就应支付多少金额达成共识的情况下给予奖励。
通过配置Config
确定一组Tippers
。当其中一半成员声明了他们认为某个特定报告理由应得的金额后,就会进入一个倒计时周期,在此期间任何剩余成员也可以声明他们的打赏金额。倒计时周期结束后,所有已声明打赏金额的中位数将被支付给报告的受益人,如果是公开(且有质押)的原始报告,还会支付任何发现者费用。
术语
- 打赏(Tipping):收集打赏金额声明并取中位数金额从国库转移到受益人账户的过程。
- 打赏理由(Tip Reason):打赏的原因;通常是一个URL,体现或解释了为什么特定个人(通过账户ID识别)值得国库的认可。
- 发现者(Finder):最初公开报告某些打赏理由的人。
- 发现者费用(Finders Fee):打赏金额的一部分,支付给打赏报告者而非主要受益人。
接口
可调度函数
report_awesome
- 报告值得打赏的内容并注册发现者费用。retract_tip
- 撤回之前(注册了发现者费用的)报告。tip_new
- 报告值得打赏的项目并声明特定打赏金额。tip
- 为特定理由声明或重新声明打赏金额。close_tip
- 关闭并支付打赏。slash_tip
- 移除并惩罚已开放的打赏。
完整示例demo
// 引入必要的模块
use frame_support::{decl_module, decl_event, decl_storage, dispatch::DispatchResult};
use frame_system::{self as system, ensure_signed};
use sp_runtime::traits::Saturating;
// 定义模块配置
pub trait Config: system::Config {
type Event: From<Event<Self>> + Into<<Self as system::Config>::Event>;
// 打赏者组
type Tippers: SortedMembers<Self::AccountId>;
// 打赏原因的最大长度
type TipReasonMaxLength: Get<u32>;
// 打赏金额的类型
type Currency: Currency<Self::AccountId>;
}
// 定义存储项
decl_storage! {
trait Store for Module<T: Config> as Tips {
// 存储开放的打赏
pub Tips get(fn tips): map hasher(blake2_128_concat) T::Hash => Option<OpenTip<T::AccountId, BalanceOf<T>, T::BlockNumber>>;
// 存储打赏原因
pub Reasons get(fn reasons): map hasher(blake2_128_concat) T::Hash => Option<Vec<u8>>;
}
}
// 定义事件
decl_event!(
pub enum Event<T> where AccountId = <T as system::Config>::AccountId, Balance = BalanceOf<T> {
/// 新打赏建议
NewTip(T::Hash),
/// 打赏关闭
TipClosed(T::Hash, AccountId, Balance),
/// 打赏声明
TipDeclared(T::Hash, AccountId, Balance),
}
);
// 定义模块
decl_module! {
pub struct Module<T: Config极 for enum Call where origin: T::Origin {
fn deposit_event() = default;
/// 报告值得打赏的内容并注册发现者费用
#[weight = 10_000]
pub fn report_awesome(origin, reason: Vec<u8>, who: T::AccountId) -> DispatchResult {
let finder = ensure_signed(origin)?;
// 验证原因长度
ensure!(
reason.len() <= T::TipReasonMaxLength::get() as usize,
"Reason too long"
);
// 生成哈希
let reason_hash = T::Hashing::hash(&reason);
// 存储原因
Reasons::<T>::insert(reason_hash, reason);
// 创建新打赏
let tip = OpenTip {
reason: reason_hash,
who,
finder: Some(finder.clone()),
deposit: Zero::zero(),
closes: None,
tips: vec![],
finders_fee: true,
};
Tips::<T>::insert(reason_hash, tip);
Self::deposit_event(RawEvent::NewTip(reason_hash));
Ok(())
}
/// 声明或重新声明打赏金额
#[weight = 10_000]
pub fn tip(origin, hash: T::Hash, tip_value: BalanceOf<T>) -> DispatchResult {
let tipper = ensure_signed(origin)?;
// 确保调用者是打赏者
ensure!(
T::Tippers::contains(&tipper),
"Caller is not a tipper"
);
// 获取打赏信息
let mut tip = Tips::<T>::get(hash).ok_or("No tip found")?;
// 更新或添加打赏金额
if let Some(pos) = tip.tips.iter().position(|(ref t, _)| t == &tipper) {
tip.tips[pos] = (tipper.clone(), tip_value);
} else {
tip.tips.push((tipper.clone(), tip_value));
}
// 检查是否达到半数打赏者
let threshold = (T::Tippers::count() + 1) / 2;
if tip.tips.len() >= threshold && tip.closes.is极one() {
tip.closes = Some(<system::Module<T>>::block_number() + T::BlockNumber::from(100u32));
}
Tips::<T>::insert(hash, tip);
Self::deposit_event(RawEvent::TipDeclared(hash, tipper, tip_value));
Ok(())
}
/// 关闭并支付打赏
#[weight = 10_000]
pub fn close_tip(origin, hash: T::Hash) -> DispatchResult {
let _ = ensure_signed(origin)?;
let tip = Tips::<T>::take(hash).ok_or("No tip found")?;
// 确保打赏已关闭
if let Some(close_at) = tip.closes {
ensure!(
<system::Module<T>>::block_number() >= close_at,
"Tip not ready to close"
);
} else {
Err("Tip not ready to close")?
}
// 计算中位数打赏金额
let mut tips = tip.tips.iter().map(|x| x.1).collect::<Vec<_>>();
tips.sort();
let median_tip = tips[tips.len() / 2];
// 支付给受益人
T::Currency::deposit_creating(&tip.who, median_tip);
// 支付发现者费用(如果有)
if let Some(finder) = tip.finder {
let finders_fee = median_tip.saturating_mul(10u32.into()) / 100u32.into();
T::Currency::deposit_creating(&finder, finders_fee);
}
Self::deposit_event(RawEvent::TipClosed(hash, tip.who, median_tip));
Ok(())
}
}
}
// 定义OpenTip结构
#[derive(Encode, Decode, Clone, PartialEq, Eq, RuntimeDebug)]
pub struct OpenTip<AccountId, Balance, BlockNumber> {
/// 打赏原因
reason: Hash,
/// 受益人
who: AccountId,
/// 发现者(如果有)
finder: Option<AccountId>,
/// 质押金额
deposit: Balance,
/// 关闭区块
closes: Option<BlockNumber>,
/// 打赏金额列表
tips: Vec<(AccountId, Balance)>,
/// 是否有发现者费用
finders_fee: bool,
}
这个示例展示了如何使用pallet-tips实现去中心化的小费支付和激励机制。主要功能包括:
- 报告值得打赏的内容(
report_awesome
) - 声明打赏金额(
tip
) - 关闭并支付打赏(
close_tip
)
打赏过程是去中心化的,由一组预定义的特权账户(Tippers
)投票决定支付金额,最终取中位数作为实际支付金额。
1 回复
以下是基于提供的内容整理的完整示例demo:
完整示例:实现小费支付系统
1. Runtime配置示例
// runtime/src/lib.rs
// 引入必要的依赖
pub use pallet_tips;
// 配置实现
parameter_types! {
pub const MaximumReasonLength: u32 = 512;
pub const DataDepositPerByte: Balance = 1 * DOLLARS;
pub const TipCountdown: BlockNumber = 14400; // 约24小时(假设6秒一个区块)
pub const TipFindersFee: Percent = Percent::from_percent(10);
pub const TipReportDepositBase: Balance = 1 * DOLLARS;
}
impl pallet_tips::Config for Runtime {
type Event = Event;
type DataDepositPerByte = DataDepositPerByte;
type MaximumReasonLength = MaximumReasonLength;
type Tippers = Elections; // 使用选举模块的成员作为评委
type TipCountdown = TipCountdown;
type TipFindersFee = TipFindersFee;
type TipReportDepositBase = TipReportDepositBase;
type WeightInfo = pallet_tips::weights::SubstrateWeight<Runtime>;
}
2. 完整后端实现
// src/tips.rs
use frame_support::{dispatch::DispatchResult, traits::Currency};
use frame_system::Config as SystemConfig;
use sp_runtime::traits::StaticLookup;
/// 小费模块完整实现
pub struct TipsModule<T: Config>(pallet_tips::Module<T>);
impl<T: Config> TipsModule<T>
where
T: pallet_tips::Config + SystemConfig,
T::AccountId: From<[u8; 32]> + Into<[u8; 32]>,
{
/// 发送小费并创建新提案
pub fn send_tip(
origin: T::Origin,
recipient: <T::Lookup as StaticLookup>::Source,
amount: BalanceOf<T>,
reason: Vec<u8>,
) -> DispatchResult {
// 验证reason长度
ensure!(reason.len() <= T::MaximumReasonLength::get() as usize, Error::<T>::ReasonTooBig);
// 报告优质内容
let hash = pallet_tips::Pallet::<T>::report_awesome(origin, recipient, reason)?;
// 初始小费
pallet_tips::Pallet::<T>::tip_new(origin, hash, amount)?;
Ok(())
}
/// 众筹小费
pub fn crowd_fund_tip(
origin: T::Origin,
reason: Vec<u8>,
beneficiary: <T::Lookup as StaticLookup>::Source,
) -> DispatchResult {
// 创建提案
let hash = pallet_tips::Pallet::<T>::report_awesome(origin, beneficiary, reason)?;
// 在这里可以添加事件通知其他用户参与众筹
Ok(())
}
/// 贡献小费到现有提案
pub fn contribute_to_tip(
origin: T::Origin,
hash: T::Hash,
amount: BalanceOf<T>,
) -> DispatchResult {
pallet_tips::Pallet::<T>::tip(origin, hash, amount)
}
/// 关闭并领取小费
pub fn close_and_claim(
origin: T::Origin,
hash: T::Hash,
) -> DispatchResult {
pallet_tips::Pallet::<T>::close_tip(origin, hash)
}
}
3. 完整前端集成示例
// src/TipsComponent.js
import React, { useState } from 'react';
import { useApi, useCall } from '@polkadot/react-hooks';
export function TipsComponent() {
const { api } = useApi();
const [reason, setReason] = useState('');
const [amount, setAmount] = useState(0);
const [beneficiary, setBeneficiary] = useState('');
// 发送小费
const sendTip = async () => {
try {
const hash = await api.tx.tips.reportAwesome(reason, beneficiary)
.signAndSend(sender);
await api.tx.tips.tipNew(hash, amount)
.signAndSend(sender);
} catch (e) {
console.error(e);
}
};
// 贡献到现有小费
const contributeTip = async (hash) => {
try {
await api.tx.tips.tip(hash, amount)
.signAndSend(sender);
} catch (e) {
console.error(e);
}
};
// 关闭小费
const closeTip = async (hash) => {
try {
await api.tx.tips.closeTip(hash)
.signAndSend(sender);
} catch (e) {
console.error(e);
}
};
return (
<div>
<h2>小费系统</h2>
<div>
<label>理由:</label>
<input value={reason} onChange={(e) => setReason(e.target.value)} />
</div>
<div>
<label>金额:</label>
<input type="number" value={amount} onChange={(e) => setAmount(e.target.value)} />
</div>
<div>
<label>受益人:</label>
<input value={beneficiary} onChange={(e) => setBeneficiary(e.target.value)} />
</div>
<button onClick={sendTip}>发送小费</button>
</div>
);
}
4. 测试用例
// tests/tips.rs
use frame_support::{assert_ok, assert_noop};
#[test]
fn test_send_tip_works() {
new_test_ext().execute_with(|| {
// 准备测试账户
let alice = 1;
let bob = 2;
let reason = b"Great work".to_vec();
let amount = 100;
// 发送小费
assert_ok!(TipsModule::send_tip(
Origin::signed(alice),
bob.into(),
amount,
reason.clone()
));
// 验证存储
assert_eq!(Tips::tips(&hash).unwrap().value, amount);
});
}
#[test]
fn test_tip_with_too_long_reason_fails() {
new_test_ext().execute_with(|| {
let long_reason = vec![0; 513]; // 超过512字节限制
assert_noop!(
TipsModule::send_tip(Origin::signed(1), 2.into(), 100, long_reason),
Error::<Test>::ReasonTooBig
);
});
}
关键点说明
-
Runtime配置:需要正确配置所有参数类型,特别是评委来源(Tippers)和小费生命周期(TipCountdown)
-
链上存储:小费理由会存储在链上,因此需要控制长度以节省存储空间
-
安全机制:
- 只有评委可以关闭小费
- 小费有最小/最大金额限制
- 小费提案有生命周期限制
-
前端集成:提供简单易用的界面让用户可以方便地发送和贡献小费
这个完整示例展示了如何从头到尾实现一个基于pallet-tips的小费支付系统,包括链上逻辑、前端界面和测试用例。