Skip to main content

地址餘額 (Address Balances)

Coin 是一種物件:若要花費它,交易必須透過其 ID 參考、擷取並傳入它。這對離散資產很適用,但會讓帳戶的資金成為一組必須追蹤、合併及分割的個別物件。地址餘額提供了不同的模型:可互換價值會以累計總額的形式直接持有於地址,不需要管理任何物件。

在底層,該價值存在鏈上以 (address, type) 配對為索引鍵的累加器中。地址上的 T 餘額是一個單一數值;資金轉入時增加,提領時減少——比起裝滿代幣的錢包,更接近銀行帳戶的運作方式。

地址餘額是 Sui Framework 最近新增的功能。本節涵蓋核心的 send_funds / redeem_funds API、從物件提領資金,以及保護提領操作免於重放的交易層級規則。

將資金傳送至地址 (Sending Funds to an Address)​

任何 Coin<T> 或 Balance<T> 都可透過 send_funds 存入地址餘額。該價值會被消耗,並記入收款者的 T 餘額:

/// Deposit a coin into the recipient's address balance.
public fun pay<T>(coin: Coin<T>, recipient: address) {
coin.send_funds(recipient);
}

send_funds 同時定義於 Coin 和 Balance。對 Coin 而言,它會將代幣轉為 Balance,並加入收款者的累加器;不會留下任何物件,收款者也不需要「接受」任何東西——餘額會直接增加。

地址餘額的目前價值可在 Move 中透過 balance::settled_funds_value 讀取,並須提供系統 AccumulatorRoot 物件的參考。如名稱所示,它會回報目前共識提交開始時已結算的資金——在該提交期間所存入的資金尚無法從中看見。

提領資金 (Withdrawing Funds)​

反方向操作——從地址餘額取出價值——受到刻意更嚴格的限制。你無法讀取任意地址的餘額並從中鑄造代幣;相反地,提領會由 Withdrawal<Balance<T>> 值表示,它定義於 Sui Framework 的 sui::funds_accumulator 模組中:

/// 從 `owner` 提領最多 `limit` 單位 `T` 的權限。
public struct Withdrawal<phantom T: store> has drop {
owner: address,
limit: u256,
}

Withdrawal 是一種授權,不是資金本身。它記錄要從誰的餘額扣除(owner),以及可取出的最大數量(limit)。它具有 drop,因此未使用的值可直接丟棄。交易會提供它——交易建置工具會將交易傳送者的 Withdrawal 作為輸入提供,其概念與 gas 代幣或接收的物件相同。使用者原始碼中沒有可建立它的建構子。

因此,從傳送者地址餘額花費資金的交易如下:Withdrawal 會作為輸入傳入,在簽署時依傳送者餘額驗證,並由一個命令將它轉為 Coin:

// 從傳送者的地址餘額花費 1_000 MIST
// 輸入 0:Withdrawal<Balance<SUI>> { owner: sender, limit: 1_000 }
// 輸入 1:收款者地址
0: sui::coin::redeem_funds<SUI>(Input(0)); // -> Coin<SUI>
1: TransferObjects([Result(0)], Input(1));

函式一旦取得 Withdrawal,便可使用 redeem_funds 將其兌換為真正的 Coin:

/// Redeem a withdrawal provided by the transaction into a spendable coin.
public fun collect<T>(w: Withdrawal<Balance<T>>, ctx: &mut TxContext): Coin<T> {
coin::redeem_funds(w, ctx)
}

兌換時才會真正將數量從累加器中移出。它只能由定義被提領型別的模組執行——這由內部許可機制強制執行;這也正是 sui::coin 與 sui::balance(定義 Coin 與 Balance 的模組)會公開 redeem_funds 的原因。

檢查與分割 Withdrawal (Inspecting and Splitting a Withdrawal)​

在兌換前,可檢查並分割 Withdrawal。這在單一提領需支應多項操作時很有用:

public fun inspect_and_split<T>(w: &mut Withdrawal<Balance<T>>): (address, u256) {
// Read who the funds belong to and how much may still be withdrawn.
let owner = w.owner();
let remaining = w.limit();

// Carve off a sub-withdrawal with its own, smaller limit. The
// parent's limit is reduced by the same amount.
let sub: Withdrawal<Balance<T>> = w.split(100);

// Withdrawals from the same owner can be joined back together,
// adding the limits up.
w.join(sub);

(owner, remaining)
}

分割與合併 Withdrawal 只會移動上限;在呼叫 redeem_funds 前,不會有任何資金轉移。合併要求兩個提領具有相同的 owner,否則會中止。

從物件提領資金 (Withdrawing from an Object)​

Withdrawal 的擁有者不一定是帳戶——也可以是物件。具有地址餘額的物件可透過 withdraw_funds_from_object 從自身資金建立提領,並傳入其 UID 的可變參考:

/// Withdraw `value` units of `T` held at this object's address.
public fun withdraw<T>(id: &mut UID, value: u64): Withdrawal<Balance<T>> {
balance::withdraw_funds_from_object<T>(id, value)
}

這讓任何物件——共享金庫、託管帳戶或國庫——都能持有並支付可互換價值,而無須封裝個別的 Coin 物件。它所建立的提領會以與傳送者提領相同的方式透過 redeem_funds 兌換。

重放保護與平行執行 (Replay Protection and Parallel Execution)​

地址餘額也改變了交易證明其唯一性且無法被重放的方式。通常的錨點是受擁有物件:每個物件都帶有系統會在每次變更時遞增的版本,因此參考該物件的已簽署交易只能執行一次——版本變更後,交易便不再相符。gas 代幣通常會免費提供這個錨點。

沒有受擁有物件輸入的交易——例如直接從地址餘額支付 gas,或輸入僅包含共享物件的交易——沒有可用的錨點,因此必須自行攜帶保護機制。交易資料的兩個欄位涵蓋此需求。SDK 會在建置這類交易時設定它們,因此這關乎交易如何被建立,而非 Move 原始碼中的任何內容:

  • 到期時間 (ValidDuring)。 交易會將其到期時間設定為 TransactionExpiration::ValidDuring,並指定 min_epoch 與 max_epoch,範圍最多跨越一個 epoch(max_epoch <= min_epoch + 1)。將有效性限制於狹窄的 epoch 視窗,可限制交易可能遭重放的時間視窗,取代保護受擁有物件的版本檢查。
  • Nonce。 交易包含 nonce——其唯一用途是讓兩筆原本完全相同的交易有所區別的任意值。與帳戶型區塊鏈的 nonce 不同,它不具連續性,也沒有間隔問題;它只是讓原本會共用摘要的交易能夠共存。

這些相同屬性也使這類交易可平行化:藉由唯一摘要,以及 Sui 已採用的逐物件排序,沒有衝突的提領操作永遠不必彼此等待。

總結 (Summary)​

  • 地址餘額是直接以鏈上累加器形式持有於地址上的 T 型別可互換價值,而非 Coin 物件;
  • coin.send_funds(recipient)(或 balance.send_funds)會將價值存入地址餘額,並消耗該代幣;
  • 提領需要 Withdrawal<Balance<T>>——具有 owner 與 limit 的授權——交易會為傳送者提供它,或物件會為自身提供它;
  • coin::redeem_funds 會將 Withdrawal 轉為 Coin,且只能透過內部許可機制,從定義該型別的模組中呼叫;
  • 沒有受擁有物件輸入的交易(從地址餘額支付 gas,或僅使用共享物件)會自行攜帶重放保護:限制於一個 epoch 的 ValidDuring 到期時間,以及使其摘要唯一的 nonce。

延伸閱讀 (Further Reading)​