Cycle

The Scriptorium

Smart Assembly code templates and tools for on-chain development in Eve Frontier.

Move Extension Basics
BeginnerChapter 4 of 415 min read

Extension Authorization Flow

EVE Frontier Smart Storage Units (SSUs) support third-party extensions -- external Move packages that can read and write inventory data. But an SSU will not let just any code touch its inventories. Extensions must be explicitly authorized by the SSU owner.

This chapter walks through the full authorization lifecycle from deployment to first use.

The Big Picture

``

  • Developer publishes Move package
  • └─ init() creates AdminCap + ExtensionConfig

    └─ Package defines XAuth witness type

  • SSU Owner authorizes the extension
  • └─ Calls storage_unit::authorize_extension(ssu, owner_cap)

    └─ SSU now trusts code that can produce XAuth values

  • Admin configures the extension
  • └─ Calls set_tribe_config(config, admin_cap, tribe_id)

    └─ Extension knows which tribe to gate for

  • Players use the extension
  • └─ Call share() or withdraw()

    └─ Extension passes XAuth{} to gated SSU functions

    `

    Step 1: Deploy the Package

    When you publish the tribe_storage_access package with sui client publish, the init function runs automatically:

    move
    fun init(ctx: &mut TxContext) {
        let admin_cap = AdminCap { id: object::new(ctx) };
        transfer::transfer(admin_cap, ctx.sender());
    
        let config = ExtensionConfig { id: object::new(ctx) };
        transfer::share_object(config);
    }

    After publishing, you have:

    • An AdminCap object owned by your wallet address
    • An ExtensionConfig shared object accessible to everyone
    • A package containing the XAuth type at ::config::XAuth

    Step 2: SSU Owner Authorizes the Extension

    The SSU owner must explicitly grant your extension access. This is done by calling storage_unit::authorize_extension with your XAuth type as the type argument.

    This is a multi-step Programmable Transaction Block (PTB) because the owner's capability token (OwnerCap) is stored inside their Character object and must be borrowed:

    typescript
    const { Transaction, Inputs } = await import('@mysten/sui/transactions');
    const tx = new Transaction();
    
    const xAuthType = ${TRIBE_STORAGE_PACKAGE_ID}::config::XAuth;
    const ssuType = ${EVE_FRONTIER_PACKAGE_ID}::storage_unit::StorageUnit;
    
    // Step 1: Borrow the OwnerCap<StorageUnit> from the Character
    const [borrowedCap, receipt] = tx.moveCall({
      target: ${EVE_FRONTIER_PACKAGE_ID}::character::borrow_owner_cap,
      typeArguments: [ssuType],
      arguments: [
        tx.object(characterObjectId),
        tx.object(Inputs.ReceivingRef({
          objectId: ownerCapId,
          version: capRef.version,
          digest: capRef.digest,
        })),
      ],
    });
    
    // Step 2: Authorize the extension on the SSU
    tx.moveCall({
      target: ${EVE_FRONTIER_PACKAGE_ID}::storage_unit::authorize_extension,
      typeArguments: [xAuthType],
      arguments: [tx.object(ssuId), borrowedCap],
    });
    
    // Step 3: Return the borrowed OwnerCap
    tx.moveCall({
      target: ${EVE_FRONTIER_PACKAGE_ID}::character::return_owner_cap,
      typeArguments: [ssuType],
      arguments: [tx.object(characterObjectId), borrowedCap, receipt],
    });

    The Borrow-Use-Return Pattern

    EVE Frontier's OwnerCap objects live inside Character objects as received objects. You cannot use them directly -- you must:

  • Borrow the cap with borrow_owner_cap, which returns (OwnerCap, BorrowReceipt)
  • Use the cap for your operation (in this case, authorize_extension)
  • Return the cap with return_owner_cap, passing back the receipt
  • The receipt ensures the cap is always returned. If you try to drop it, compilation fails (it lacks drop ability). This is the "hot potato" pattern -- you are forced to deal with it.

    Step 3: Configure the Extension

    After the SSU is authorized, the admin sets the tribe restriction:

    move
    public fun set_tribe_config(
        extension_config: &mut ExtensionConfig,
        admin_cap: &AdminCap,
        tribe_id: u32,
    ) {
        config::set_rule<TribeConfigKey, TribeConfig>(
            extension_config,
            admin_cap,
            TribeConfigKey {},
            TribeConfig { tribe: tribe_id },
        );
    }

    This stores the tribe ID as a dynamic field on ExtensionConfig. Only the AdminCap holder can call this.

    From TypeScript:

    typescript
    const tx = new Transaction();
    
    tx.moveCall({
      target: ${TRIBE_STORAGE_PACKAGE_ID}::tribe_storage::set_tribe_config,
      arguments: [
        tx.object(TRIBE_STORAGE_CONFIG_ID),  // shared ExtensionConfig
        tx.object(adminCapId),                // owned AdminCap
        tx.pure.u32(tribeId),                 // tribe ID to gate for
      ],
    });

    Step 4: Players Use the Extension

    Now tribe members can share and withdraw items. Here is the Move-side share() function:

    move
    public fun share<T: key>(
        extension_config: &ExtensionConfig,
        storage_unit: &mut StorageUnit,
        character: &Character,
        owner_cap: &OwnerCap<T>,
        type_id: u64,
        quantity: u32,
        ctx: &mut TxContext,
    ) {
        verify_tribe(extension_config, character);
    
        let item = storage_unit::withdraw_by_owner(
            storage_unit, character, owner_cap,
            type_id, quantity, ctx,
        );
    
        storage_unit::deposit_to_open_inventory<XAuth>(
            storage_unit, character, item,
            config::x_auth(),    // <-- XAuth witness proves authorization
            ctx,
        );
    }

    The key line is config::x_auth(). This produces an XAuth {} value that the SSU's deposit_to_open_inventory function checks against its authorized extensions list. Since the SSU owner authorized XAuth in step 2, this call succeeds.

    Authorization Security Model

    The security of this system relies on three properties:

  • Type uniqueness: XAuth is defined in your package. No other package can define the same fully-qualified type ::config::XAuth.
  • Visibility restriction: x_auth() is public(package), so only your modules can produce XAuth values. Even if someone knows the type, they cannot instantiate it from outside your package.
  • Explicit opt-in: The SSU owner must call authorize_extension to allow your type. Your code cannot authorize itself.
  • `

    ┌─────────────────────────────────────────────────┐

    │ Can produce XAuth{}? │

    │ │

    │ tribe_storage_access::tribe_storage ✓ YES │

    │ tribe_storage_access::config ✓ YES │

    │ some_other_package::evil_module ✗ NO │

    │ direct PTB transaction ✗ NO │

    └─────────────────────────────────────────────────┘

    `

    Revoking Authorization

    The SSU owner can revoke an extension's authorization at any time:

    move
    storage_unit::revoke_extension<XAuth>(ssu, owner_cap);

    After revocation, any calls using XAuth will fail, even though the extension's code is still published on-chain.

    Key Takeaways

    • Extensions are authorized per-SSU by the SSU owner calling authorize_extension().
    • The XAuth witness type is the trust anchor -- only the defining package can produce values of this type.
    • The borrow-use-return pattern with OwnerCap is mandatory for SSU operations, enforced by the hot potato BorrowReceipt`.
    • Authorization is a three-party protocol: the developer deploys the package, the SSU owner authorizes it, and players use it.
    • Authorization can be revoked at any time by the SSU owner.

    Sign in to track your progress.