본문으로 건너뛰기

Kiosk Apps

Kiosk Apps는 핵심 기능을 그대로 유지하면서 Sui Kiosk의 기능을 확장하는 방식이다. 핵심 코드를 수정하거나 자산을 다른 곳으로 옮기지 않고도 Kiosk에 새 기능을 추가하는 앱을 개발할 수 있다.

앱에는 두 가지 유형이 있다:

Basic apps

Basic Kiosk Apps는 동작하기 위해 Kiosk Apps API가 필요하지 않다. 일반적으로 Kiosk에 사용자 정의 메타데이터를 추가하거나 Kiosk 또는 KioskOwnerCap 같은 기존 object를 래핑하거나 함께 사용하는 목적에 쓰인다. API가 필요 없는 앱의 예로는 Personal Kiosk 앱이 있다.

UID access via the uid_mut

Kiosk는 Sui의 다른 모든 object처럼 id: UID 필드를 가지며, 이를 통해 해당 object를 고유하게 식별하고 사용자 정의 dynamic fields 및 dynamic object fields를 담을 수 있다. Kiosk 자체는 dynamic fields를 기반으로 구축되어 있으며 place, list 같은 기능은 dynamic object fields를 기반으로 구축되어 있다.

The uid_mut_as_owner function

Kiosk는 추가 dynamic fields 및 dynamic object fields를 담을 수 있다. uid_mut_as_owner 함수는 Kiosk 소유자가 Kiosk object의 UID를 변경 가능하게 접근하여 사용자 정의 필드를 추가하거나 제거할 수 있게 한다.

함수 시그니처:

kiosk::uid_mut_as_owner(self: &mut Kiosk, cap: &KioskOwnerCap): &mut UID

The public uid getter

누구나 kiosk의 uid를 읽을 수 있다. 이렇게 하면 허용된 경우 제3자 모듈이 Kiosk의 필드를 읽을 수 있다. 따라서 object capability 및 기타 패턴을 사용할 수 있다.

Basic app ideas

누구나 읽을 수 있지만 수정은 본인만 가능한 사용자 정의 dynamic fields를 Kiosk에 붙일 수 있으며, 이를 통해 Basic 앱을 구현할 수 있다. 예를 들어, Kiosk 소유자가 Kiosk 이름을 설정하고 dynamic field로 붙여 누구나 읽을 수 있게 하는 Kiosk Name 앱이 있다.

module examples::kiosk_name_ext;

use std::string::String;
use sui::dynamic_field as df;
use sui::kiosk::{Self, Kiosk, KioskOwnerCap};

/// Kiosk Name Extension의 dynamic field 키
/// Kiosk Name Extension의 dynamic field 키
struct KioskName has copy, store, drop {}

/// Kiosk에 name을 추가한다(이 구현에서는 한 번만 호출할 수 있다)
public fun add(self: &mut Kiosk, cap: &KioskOwnerCap, name: String) {
let uid_mut = self.uid_mut_as_owner(cap);
df::add(uid_mut, KioskName {}, name)
}

/// Kiosk의 name을 읽어 보며, 설정되어 있으면 Some(String)을, 아니면 None을 반환한다
public fun name(self: &Kiosk): Option<String> {
if (df::exists_(self.uid(), KioskName {})) {
option::some(*df::borrow(self.uid(), KioskName {}))
} else {
option::none()
}
}

Permissioned apps using the Kiosk Apps API

Permissioned 앱은 Kiosk에서 동작을 수행하기 위해 Kiosk Apps API를 사용한다. 일반적으로 제3자와의 상호작용을 전제로 하며, 스토리지 접근에 대한 보장을 제공한다(판매자의 악의적 행위 방지).

보안 제한 때문에 uid에 대한 접근만으로는 앱을 구축하기에 부족한 경우가 많다. Kiosk 소유자만 uid에 대한 전체 접근 권한을 가지므로, 제3자가 관여하는 앱은 과정의 모든 단계에서 Kiosk 소유자의 참여가 필요하다.

제한적이고 제약된 스토리지 접근에 더해, 앱 권한도 소유자에 따라 달라진다. 기본 설정에서는 Kiosk 소유자의 동의 없이는 어떤 당사자도 Kiosk에 item을 place하거나 lock할 수 없다. 그 결과, 컬렉션 입찰(컬렉션 내 아무 item에 대해 X SUI 제안) 같은 일부 사례에서는 Kiosk 소유자가 입찰을 승인해야 한다.

kiosk_extension module

kiosk_extension 모듈은 소유자 병목에 대한 우려를 해소하고 스토리지 접근에 대한 보장을 더 제공한다. 이 모듈은 Kiosk 소유자의 관여 없이 Kiosk에서 특정 동작을 수행할 수 있게 하며, 앱 스토리지가 변조되지 않음을 보장하는 함수 세트를 제공한다.

module example::my_extension;

use sui::kiosk_extension;

// ...

App lifecycle

Sui Kiosk 앱의 생명주기에서 핵심 사항은 다음과 같다:

  • kiosk_extension 모듈에서 명시적 호출로만 앱을 설치할 수 있다.
  • Kiosk 소유자는 disable 함수를 호출해 언제든 앱의 권한을 회수할 수 있다.
  • Kiosk 소유자는 enable 함수를 호출해 언제든 비활성화된 앱을 다시 활성화할 수 있다.
  • 앱 스토리지가 비어 있을 때만(모든 item이 제거되었을 때만) 앱을 제거할 수 있다.

Adding an app

앱이 동작하려면 Kiosk 소유자가 먼저 설치해야 한다. 이를 위해 앱은 Kiosk 소유자가 필요한 모든 권한을 요청하기 위해 호출하는 add 함수를 구현해야 한다.

Implementing add function

kiosk_extension::add 함수의 시그니처는 앱 witness를 요구하므로, 명시적 구현 없이는 앱을 설치할 수 없다. 다음 예는 place 권한이 필요한 앱에 대해 add 함수를 구현하는 방법을 보여 준다:

module examples::letterbox_ext;

use sui::kiosk_extension;

// ... 의존성

/// extension에 필요한 권한 집합으로, `place`가 필요하다.
const PERMISSIONS: u128 = 1;

/// extension을 식별하고 권한을 부여하는 데 사용하는 Witness struct이다.
struct Extension has drop {}

/// Mallbox extension을 Kiosk에 설치한다.
public fun add(kiosk: &mut Kiosk, cap: &KioskOwnerCap, ctx: &mut TxContext) {
kiosk_extension::add(Extension {}, kiosk, cap, PERMISSIONS, ctx)
}

App permissions

앱은 설치 시 Kiosk 소유자에게 권한을 요청할 수 있다. 권한은 전부 아니면 전무 원칙을 따른다. Kiosk 소유자가 앱을 추가하면 요청한 모든 권한을 얻고, Kiosk 소유자가 앱을 비활성화하면 모든 권한을 잃는다.

Structure

권한은 비트맵을 저장하는 u128 정수로 표현된다. 각 비트는 권한 하나에 대응하며, 첫 번째 비트가 최하위 비트이다. 다음 표는 모든 권한과 해당 비트를 나열한다.

BitDecimalPermission
00000권한 없음
00011앱이 place 가능
00102앱이 place 및 lock 가능
00113앱이 place 및 lock 가능
정보

현재 Sui Kiosk에는 두 가지 권한만 있다: place (첫 번째 비트)와 lockplace (두 번째 비트). 나머지 비트는 향후 사용을 위해 예약되어 있다.

Using permissions in the add function

앱 권한을 담은 상수를 정의하는 것이 좋은 관행으로 여겨진다:

module examples::letterbox_ext;
// ... 의존성

/// app에 필요한 권한 집합으로, `place`가 필요하다.
/// app에 필요한 권한 집합으로, `place`가 필요하다.
const PERMISSIONS: u128 = 1;

/// app을 식별하고 권한을 부여하는 데 사용하는 witness struct이다.
struct Extension has drop {}

/// Mallbox app을 kiosk에 설치하고 `place` 권한을 요청한다.
public fun add(kiosk: &mut Kiosk, cap: &KioskOwnerCap, ctx: &mut TxContext) {
kiosk_extension::add(Extension {}, kiosk, cap, PERMISSIONS, ctx)
}

Accessing protected functions

앱이 권한을 요청하고 부여받았으며(비활성화되지 않았으면) protected 함수에 접근할 수 있다. 다음 예는 place 함수에 접근하는 방법을 보여 준다:

module examples::letterbox_ext;
// ...

/// 권한 없이 item을 place하려고 할 때 방출된다.
const ENotEnoughPermissions: u64 = 1;

/// `KioskOwnerCap` 없이 kiosk에 letter를 place한다.
public fun place(kiosk: &mut Kiosk, letter: Letter, policy: &TransferPolicy<T>) {
assert!(kiosk_extension::can_place<Extension>(kiosk), ENotEnoughPermissions)

kiosk_extension::place(Extension {}, kiosk, letter, policy)
}

현재 다음 두 함수를 사용할 수 있다:

  • place<Ext, T>(Ext, &mut Kiosk, T, &TransferPolicy<T>) - place와 유사
  • lock<Ext, T>(Ext, &mut Kiosk, T, &TransferPolicy<T>) - lock과 유사

Checking permissions

앱에 place 권한이 있는지 확인하려면 can_place<Ext>(kiosk: &Kiosk): bool 함수를 사용한다. 마찬가지로 앱에 lock 권한이 있는지 확인하려면 can_lock<Ext>(kiosk: &Kiosk): bool 함수를 사용할 수 있다. 두 함수 모두 앱이 활성화되어 있는지 확인하므로, 별도로 확인할 필요는 없다.

App storage

각 앱은 앱 모듈만 접근할 수 있는(앱 witness 제공 시) Bag 타입의 격리된 스토리지를 갖는다. Move에서 사용할 수 있는 Bag 같은 dynamic collection에 대한 자세한 내용은 The Move Book을 참고한다. 앱을 설치한 후 스토리지를 사용해 데이터를 저장할 수 있다. 이상적으로는 현재 진행 중인 거래나 기타 활동이 없을 때 앱을 Kiosk에서 제거할 수 있도록 스토리지를 관리하는 것이 좋다.

설치되어 있으면 스토리지는 항상 앱에 사용 가능하다. Kiosk 소유자는 해당 로직이 구현되어 있지 않으면 앱 스토리지에 접근할 수 없다.

Accessing the storage

설치된 앱은 다음 함수 중 하나를 사용해 스토리지에 변경 가능하게 또는 변경 불가하게 접근할 수 있다:

  • storage(_ext: Extension {}, kiosk: &Kiosk): Bag: 앱 스토리지에 대한 참조를 반환한다. 스토리지를 읽을 때 이 함수를 사용한다.
  • storage_mut(_ext: Extension {}, kiosk: &mut Kiosk): &mut Bag: 앱 스토리지에 대한 변경 가능 참조를 반환한다. 스토리지를 읽고 쓸 때 이 함수를 사용한다.

Disabling and removing

Kiosk 소유자는 언제든 어떤 앱이든 비활성화할 수 있다. 그러면 앱의 모든 권한이 회수되고 Kiosk에서 어떤 동작도 수행할 수 없게 된다. Kiosk 소유자는 언제든 앱을 다시 활성화할 수도 있다.

앱을 비활성화해도 Kiosk에서 제거되지는 않는다. 설치된 앱은 Kiosk에서 완전히 제거될 때까지 자신의 스토리지에 접근할 수 있다.

Disabling an app

앱을 비활성화하려면 disable<Ext>(kiosk: &mut Kiosk, cap: &KioskOwnerCap) 함수를 사용한다. 이 함수는 앱의 모든 권한을 회수하고 Kiosk에서 protected 동작을 수행하지 못하게 한다.

Example PTB

let txb = new TransactionBuilder();
let kioskArg = tx.object('<ID>');
let capArg = tx.object('<ID>');

txb.moveCall({
target: '0x2::kiosk_extension::disable',
arguments: [ kioskArg, capArg ],
typeArguments: '<letter_box_package>::letterbox_ext::Extension'
});

Removing an app

스토리지가 비어 있을 때만 앱을 제거할 수 있다. 제거를 수행하려면 remove<Ext>(kiosk: &mut Kiosk, cap: &KioskOwnerCap) 함수를 사용한다. 이 함수는 앱을 제거하고 앱 스토리지와 설정을 풀며 스토리지 비용을 Kiosk 소유자에게 리베이트한다. 이 동작은 Kiosk 소유자만 수행할 수 있다.

스토리지가 비어 있지 않으면 호출이 실패한다.

Example PTB

let txb = new TransactionBuilder();
let kioskArg = tx.object('<ID>');
let capArg = tx.object('<ID>');

txb.moveCall({
target: '0x2::kiosk_extension::remove',
arguments: [ kioskArg, capArg ],
typeArguments: '<letter_box_package>::letterbox_ext::Extension'
});