--- title: Sui package description: "The Hular Sui Move package: entry functions, events, markers, and abort codes." --- import { PACKAGE_SUI, ESCROW_SUI, TREASURY_SUI } from "/snippets/vars.mdx"; One Move package covers the Sui side. A single shared `Escrow` object, created when the package is published, holds the deposit float, the referral voucher float, the settlement marker table, and the redeemed voucher table. Deposits pay into the escrow and refunds pay out of it; fulfillments are paid by the treasury address from its own balance, with the package enforcing exactly-once settlement and emitting the events the indexer reads. | | Value | | --- | --- | | Package id | {PACKAGE_SUI} | | Escrow (shared object) | {ESCROW_SUI} | | Treasury | {TREASURY_SUI} | Prefer the `router_address` (package id) returned on the quote and the `escrow` and `treasury` returned by `GetChains` over these constants. All functions live in module `hular` and take the coin type as a type argument; the only configured token is native USDC. ## Deposit ``` public fun deposit(escrow: &mut Escrow, quote_hash: vector, coin: Coin, ctx: &TxContext) ``` Joins the coin into the escrow's balance for `T` and emits `Deposit` with the amount and sender. The quote hash must be exactly 32 bytes. There is no swap-first variant and no native SUI variant on Sui. ## Operator functions Restricted to the treasury address, listed for completeness. ``` public fun fulfill(escrow: &mut Escrow, quote_hash: vector, coin: Coin, gas: Coin, recipient: address, ctx: &TxContext) public fun refund(escrow: &mut Escrow, quote_hash: vector, amount: u64, recipient: address, ctx: &mut TxContext) public fun release(escrow: &mut Escrow, amount: u64, ctx: &mut TxContext) ``` `fulfill` transfers the payout coin and, when nonzero, the SUI gas drop coin to the recipient. `refund` pays the recipient out of the escrow's deposit balance. Both insert a settlement marker keyed by the quote hash and abort if one already exists: a quote hash settles exactly once per chain, whichever of fulfill or refund lands first. `release` moves settled deposit float from the escrow to the treasury. ## Settlement markers and pruning Each marker stores the epoch it was settled in. Markers are the on-chain replay guard, and because every operator transaction also carries an expiration epoch, a marker older than the retention window can be safely removed to reclaim its storage rebate: ``` public fun prune(escrow: &mut Escrow, quote_hash: vector, ctx: &TxContext) ``` `prune` is permissionless and aborts with `ERetentionNotElapsed` until 180 epochs (about six months) have passed since settlement. The storage rebate is credited to the pruning transaction. ## Voucher redemption Referral fees accrue off-chain and are paid on Sui through signed vouchers, redeemed by the referrer's own wallet against a treasury-funded float held inside the escrow: ``` public fun redeem_voucher(escrow: &mut Escrow, id: vector, referrer: address, amount: u64, signature: vector, ctx: &mut TxContext) public fun fund_vouchers(escrow: &mut Escrow, coin: Coin) public fun defund_vouchers(escrow: &mut Escrow, amount: u64, ctx: &mut TxContext) ``` The signature is ed25519 over the following bytes, verified against any registered voucher signer key: | Bytes | Content | | --- | --- | | 13 | `hular-voucher` | | 1 | 3, the length of the chain tag | | 3 | `sui` | | 32 | escrow object id | | 32 | voucher id | | 2 | coin type length, u16 little-endian | | variable | coin type ascii, full 64-hex address with no `0x` prefix | | 32 | referrer address | | 8 | amount, u64 little-endian | Voucher ids are marked in a permanent table, so each voucher redeems exactly once. Vouchers never expire, and the redeemed-id table is never pruned. `fund_vouchers` is permissionless; `defund_vouchers` returns float to the treasury. ## Admin ``` public fun set_treasury(_: &AdminCap, escrow: &mut Escrow, treasury: address) public fun add_voucher_signer(_: &AdminCap, escrow: &mut Escrow, pubkey: vector) ``` The `AdminCap` is transferred to the treasury at deploy time. Voucher signers are append-only: rotating the key adds the new one, and vouchers signed by earlier keys stay redeemable. ## Events | Event | Fields | | --- | --- | | `Deposit` | `quote_hash`, `coin_type`, `amount`, `from` | | `Fulfilled` | `quote_hash`, `coin_type`, `amount`, `recipient`, `gas_drop` | | `Refunded` | `quote_hash`, `coin_type`, `amount`, `recipient` | | `Released` | `coin_type`, `amount` | | `VoucherRedeemed` | `id`, `coin_type`, `referrer`, `amount` | `coin_type` is the full coin type string of the type argument, without a `0x` prefix. ## Abort codes | Code | Name | Meaning | | --- | --- | --- | | 0 | `EUnauthorized` | Caller is not the treasury. | | 1 | `EAlreadySettled` | The quote hash already has a settlement marker. | | 2 | `EVoucherRedeemed` | The voucher id was already redeemed. | | 3 | `EInvalidVoucherSignature` | No registered signer verifies the message. | | 4 | `EInvalidLength` | A hash, id, signature, or pubkey has the wrong length. | | 5 | `ERetentionNotElapsed` | The marker is younger than the 180-epoch retention window. |