객체로 전송
object를 address로 전송하는 것과 동일한 방식으로, 동일한 함수를 사용하여 object ID로 object를 전송할 수 있다. 이는 Sui에서 address와 object가 모두 32바이트 ID를 사용하며, 두 ID가 구분되지 않기 때문이다(이들은 중복되지 않음이 보장된다). transfer to object 작업은 이 특성을 활용하여 전송 작업의 address 입력에 object ID를 제공할 수 있도록 한다.
Party 객체의 경우, 소유 address가 object ID에 해당하는 경우 객체로 전송 메커니즘이 지원되지 않는다.
동일한 ID 구조로 인해 object를 전송할 때 address 필드에 object ID를 사용할 수 있다. 실제로 address가 소유한 object와 관련된 모든 기능은 다른 object가 소유한 object에도 동일하게 작동하며, 단지 address를 object ID로 교체하면 된다.
object를 다른 object로 전송하는 것은 기본적으로 부모–자식 인증 관계를 설정하는 것이다. 다른 object로 전송된 object는 부모 object의(전이적일 수 있는) 소유자가 수신할 수 있다. 부모(수신) object의 타입을 정의한 모듈이 자식 object 수신에 대한 접근 제어를 정의한다.
전송된 자식 object에 대한 접근 제한은 transaction 실행 중 부모 object의 UID에 대한 mutable 접근을 허용함으로써 동적으로 적용된다. 이로 인해 owned object, dynamic field object, wrapped object 및 shared object로 object를 전송하고 이들로부터 object를 수신할 수 있다.
transfer to object 작업의 이점 중 하나는, 예를 들어 온체인 지갑이나 계정이 안정적인 ID를 유지할 수 있다는 점이다. object의 전송은 수신 object의 상태와 관계없이 해당 ID에 영향을 주지 않는다. object를 전송하면 해당 object의 모든 자식 object가 함께 이동하며, object의 address는 전송하든, wrap하든, dynamic field로 보유하든 동일하다.
Transferring to object
일반적인 object 전송과 마찬가지로 수신 object ID가 실제로 존재하는지 확인해야 한다. 또한 수신 object가 immutable object가 아닌지도 확인해야 한다. immutable object로 전송된 object에는 접근할 수 없다.
전송하려는 object와 수신 object의 타입 모두에 유의해야 한다. 수신(부모) object는 항상 다음을 수행할 수 있다:
- 전송된 object에 접근하기 위해 동적으로 검사 가능한 조건자를 정의한다.
- 전송된 object에 대한 접근을 지원하지 않는다. 해당 패키지의 향후 버전에서 이 기능을 지원할 수 있으나, 이를 포함할지는 패키지 작성자에게 달려 있다.
전송되는 object가 key ability만 가진 경우:
- 전송되는 object를 정의하는 모듈은 custom transfer 함수와 유사하게 해당 object에 대한 custom receive 함수를 구현해야 한다. 전송되 는 object를 정의하는 모듈은 custom transfer 함수와 유사하게 해당 object에 대한 custom receive 함수를 구현해야 한다. custom transfer 함수와 마찬가지로, custom receivership 함수는 개발자가 강제할 수 있는 임의의 제약을 가질 수 있고, 사용자가 이를 인지해야 할 수도 있다. 또는 이러한 함수가 존재하지 않을 수도 있다.
- 전송 후에는 부모(수신 대상) object 모듈이 수신 함수를 정의하고, 자식(전송되는) object 모듈 또한 수신 함수를 정의하며, 두 함수가 정의한 제약이 모두 충족되어야만 전송된 object에 접근하거나 사용할 수 있다.
// 0xADD는 address이다.
// 0x0B는 object ID이다.
// b와 c는 object이다.
// object `b`를 address 0xADD로 전송한다.
transfer::public_transfer(b, @0xADD);
// object `c`를 object ID 0x0B를 가진 object로 전송한다.
transfer::public_transfer(c, @0x0B);
object를 object ID로 전송하면 address로 전송한 것과 동일한 결과가 나오며, object의 소유자는 제공된 32바이트 address 또는 object ID가 된다. 또한 object 전송의 결과에 차이가 없기 때문에 32바이트 ID에 대해 기존 RPC 메서드(예: getOwnedObjects)를 사용할 수 있다. ID가 address를 나타내면 메서드는 해당 address가 소유한 object를 반환하고, ID가 object ID이면 메서드는 해당 object ID 가 소유한 object(전송된 object)를 반환한다.
// address 0xADD가 소유한 object를 가져온다. `b`를 반환한다.
{
"jsonrpc": "2.0",
"id": 1,
"method": "suix_getOwnedObjects",
"params": ["0xADD"]
}
// object ID 0x0B를 가진 object가 소유한 object를 가져온다. `c`를 반환한다.
{
"jsonrpc": "2.0",
"id": 1,
"method": "suix_getOwnedObjects",
"params": ["0x0B"]
}
Receiving objects
object c가 다른 object p로 전송된 후, p는 이를 사용하기 위해 c를 수신해야 한다. object c를 수신하려면 programmable transaction blocks(PTBs)에서 Receiving(o: ObjectRef) 인자 타입을 사용하며, 이는 수신될 object의 ObjectID, Version, Digest를 포함하는 object 참조를 받는다(이는 PTB의 owned object 인자와 동일한 방식이다). 그러나 Receiving PTB 인자는 transaction 내에서 owned 값이나 mutable 참조로 전달되지 않는다.
자세히 설명하자면, Sui 프레임워크의 transfer 모듈에 정의된 Move의 수신 인터페이스 핵심은 다음과 같다:
module sui::transfer;
/// 타입 `T`의 object를 수신할 수 있는 능력을 나타낸다. 저장할 수 없다.
public struct Receiving<phantom T: key> has drop { ... }
/// `parent`에 대한 가변(즉, locked) 접근과 `parent`가 소유한 object를
/// 참조하는 `Receiving` object 가 주어지면 티켓을 사용하여 해당 object를 반환한다.
///
/// 이 함수에는 Sui Move 바이트코드 verifier가 적용하는 custom 규칙이 있으며,
/// `T`가 `receive`가 호출되는 모듈에 정의된 object임을 보장한다.
/// 정의 모듈 외부에서 `store`를 가진 object를 수신하려면 `public_receive`를 사용한다.
///
/// 참고: &mut UID는 수신 object의 타입을 정의한 모듈이 자신이 정의한 타입으로 전송된
/// object를 수신하는 데 필요한 사용자 정의 접근·권한 정책을 정의하도록 허용한다.
public native fun receive<T: key>(parent: &mut UID, object: Receiving<T>): T;
/// `parent`에 대한 가변(locked) 접근과 `parent`가 소유한 타입 `T`의 object를
/// 참조하는 `Receiving` 인자가 주어지면 이를 수신하여 반환한다.
/// object `T`는 이 함수로 수신되려면 `store`를 가져야 하며,
/// 이는 `T`의 정의 모듈 외부에서도 호출될 수 있다.
public native fun public_receive<T: key + store>(parent: &mut UID, object: Receiving<T>): T;
...
PTB에서 타입 T의 전송된 object를 참조하는 각 Receiving 인자는 정확히 하나의 Move 타입 sui::transfer::Receiving<T> 인자로 변환되며, 그런 다음 이 인자를 사용해 transfer::receive 함수로 타입 T의 전송된 object를 수신할 수 있다.
transfer::receive를 호출할 때는 부모 object의 UID에 대한 가변 참조를 전달해야 한다. 그러나 object의 정의 모듈이 이를 노출하지 않으면 UID의 가변 참조를 얻을 수 없다. 결과적으로 자식 object를 수신하는 부모 object의 타입을 정의하는 모듈이, 자신에게 전송된 object를 수신하는 데 대한 접근 제어 정책 및 기타 제약을 정의한다(자세한 내용은 authorization example 참고). 전달된 UID가 실제로 Receiving 인자가 참조하는 object를 소유하는지는 런타임에서 동적으로 검증되어 적용된다. 이는 예를 들어 소유권 체인이 동적으로만 확립될 수 있는 dynamic field로 전송된 object에 대한 접근을 가능하게 한다.
sui::transfer::Receiving은 drop ability만 가지므로 Receiving<T> 인자의 존재는 T 유형의 object를 수신할 수 있는 능력을 나타내지만, PTB 내 object 참조로 지정된 해당 object를 반드시 수신해야 하는 의무는 아니다. Receiving 인자는 해당 transaction 동안 PTB에서 Receiving 인자를 일부만 사용해도, 전혀 사용하지 않아도, 모두 사용해도 문제가 없다. Receiving 인자에 해당하는 object는 수신되지 않는 한 그대로 유지된다(특히 object 참조가 동일하게 유지됨).
Custom receiving rules
custom transfer policies와 마찬가지로 Sui는 key-only object에 대한 custom receivership 규칙의 정의를 허용한다. 특히 transfer::receive 함수는 transfer::receive가 호출되는 모듈에 정의된 object에만 사용할 수 있으며, 이는 transfer::transfer 함수 사용 규칙과 동일하다.
마찬가지로 store ability를 가진 object는 누구나 transfer::public_receive로 수신할 수 있다. 이는 transfer::public_transfer가 store ability를 가진 모든 object를 전송할 수 있는 것과 동일하다.
부모 object가 receivership에 대한 custom 규칙을 정의할 수 있다는 사실과 결합될 때, 자식 object의 ability를 기반으로 object 수신 및 전송되는 object의 ability에 관한 다음의 권한 매트릭스를 반드시 고려해야 한다.
| 자식의 ability | 부모의 접근 제한 가능 | 자식의 접근 제한 가능 |
|---|---|---|
key | Yes | Yes |
key + store | Yes | No |
custom transfer 정책과 마찬가지로 이러한 제약을 결합해 강력한 정책을 설계할 수 있다. 예를 들어 soul-bound objects는 custom transfer와 receivership 규칙을 함께 사용해 구현할 수 있다.
Using SDKs
Transaction 생성 시 Receiving 입력은 Sui TypeScript SDK의 다른 object 인자와 거의 동일하게 다룬다. 예를 들어 Simple Account 예제에서 ID가 0xc0ffee 인 coin object를 당신의 account인 0xcafe로 수신하는 transaction을 보내려면 Sui TypeScript SDK 또는 Sui Rust SDK를 사용하여 다음과 같이 수행할 수 있다:
- TypeScript
- Rust
... // Setup TypeScript SDK as normal.
const tx = new Transaction();
tx.moveCall({
target: `${examplePackageId}::account::accept_payment`,
arguments: [tx.object("0xcafe"), tx.object("0xc0ffee")]
});
const result = await client.signAndExecuteTransaction({
transaction: tx,
});
...
... // setup Rust SDK client as normal
client
.transaction_builder()
.move_call(
sending_account,
example_package_id,
"account",
"accept_payment",
vec!["0x2::sui::SUI"],
vec![
SuiJsonValue::from_object_id("0xcafe"),
SuiJsonValue::from_object_id("0xc0ffee") // 0xcoffee is turned into the `Receiving<...>` argument of `accept_payment` by the SDK
])
...
또한 일반 object 인자에 명시적 object ID, version, digest를 제공하는 ObjectRef 생성자가 있는 것처럼, Receiving 인자에도 동일한 인자를 받는 ReceivingRef 생성자가 있다.