SUI Move官方示例合约实践——NFT类:零信任原子交换(trustless swap)

  • rzexin
  • 更新于 2024-06-22 21:35
  • 阅读 960

SUI Move官方示例合约实践——NFT类:零信任原子交换(trustless swap)

SUI Move官方示例合约实践——NFT类:零信任原子交换(trustless swap)

1 合约说明

1.1 功能介绍

本合约示例是官方提供的零信任原子交换合约,它类似于托管,但不需要可信的第三方。

合约中使用共享对象来作为两个想要交易对象之间的托管。共享对象是Sui的一个独特概念。任何用户的签名交易都可以对其发起修改,但成功修改的前提是需要满足其中定义的约束。

该合约原子交换的过程分为三个阶段:

  • 阶段一:原子交换的一方可以调用lock接口锁定其交换对象,将会获得一个Locked对象以及用于解锁的Key对象。如果另一方故意拖延(stalls)不配合完成第二阶段的话,第一方可以unlock其锁定对象,使其保持活性。
  • 阶段二:另一方调用create创建一个公开可访问的共享托管对象(Escrow),也将其交换对象进行锁定,并等待第一方完成swap操作。第二方同样也可以取回他们托管的交换对象,使其保持活性。
  • 阶段三:第一方将他的Locked对象及其解锁的Key对象发送到共享的托管对象。只要满足如下所有条件,就能成功完成了交换:
    • 交换交易的发送方是托管的接收方
    • 托管中所需密钥对象(exchange_key)的密钥与交换中提供的解锁密钥(Key)匹配
    • 交换中提供的密钥Key能解锁锁定的Locked对象

1.2 合约代码

1.2.1 合约源码地址

https://github.com/MystenLabs/sui/tree/mainnet-v1.27.2/examples/trading/contracts/escrow

1.2.2 数据结构说明

(1)定义锁对象名称
    /// The `name` of the DOF that holds the Locked object.
    /// Allows better discoverability for the locked object.
    public struct LockedObjectKey has copy, store, drop {}
(2)Locked结构

包装了Key对象

    /// A wrapper that protects access to `obj` by requiring access to a `Key`.
    ///
    /// Used to ensure an object is not modified if it might be involved in a
    /// swap.
    ///
    /// Object is added as a Dynamic Object Field so that it can still be looked-up.
    public struct Locked<phantom T: key + store> has key, store {
        id: UID,
        key: ID,
    }
(3)Key结构
    /// Key to open a locked object (consuming the `Key`)
    public struct Key has key, store { id: UID }
(4)定义托管对象名称
    /// The `name` of the DOF that holds the Escrowed object.
    /// Allows easy discoverability for the escrowed object.
    public struct EscrowedObjectKey has copy, store, drop {}
(5)托管共享对象结构
    /// An object held in escrow
    /// 
    /// The escrowed object is added as a Dynamic Object Field so it can still be looked-up.
    public struct Escrow<phantom T: key + store> has key, store {
        id: UID,

        /// Owner of `escrowed`
        sender: address,

        /// Intended recipient
        recipient: address,

        /// ID of the key that opens the lock on the object sender wants from
        /// recipient.
        exchange_key: ID,
    }

1.2.3 接口说明

(1)锁定托管对象(lock
  • 调用该接口会创建托管对象锁Locked对象及其解锁的Key
  • 将交换对象绑定到锁的动态对象存储中
    /// Lock `obj` and get a key that can be used to unlock it.
    public fun lock<T: key + store>(
        obj: T,
        ctx: &mut TxContext,
    ): (Locked<T>, Key) {
        let key = Key { id: object::new(ctx) };
        let mut lock = Locked {
            id: object::new(ctx),
            key: object::id(&key),
        };

        event::emit(LockCreated {
            lock_id: object::id(&lock),
            key_id: object::id(&key),
            creator: ctx.sender(),
            item_id: object::id(&obj)
        });

        // Adds the `object` as a DOF for the `lock` object
        dof::add(&mut lock.id, LockedObjectKey {}, obj);

        (lock, key)
    }
(2)解锁托管对象(unlock
  • 调用该接口需要传入锁对象及其匹配的解锁Key,否则将报错
  • 成功解锁后,可以获取锁对象中绑定的托管对象
  • 将锁对象和解锁Key对象删除后,返回托管对象
    /// Unlock the object in `locked`, consuming the `key`.  Fails if the wrong
    /// `key` is passed in for the locked object.
    public fun unlock<T: key + store>(mut locked: Locked<T>, key: Key): T {
        assert!(locked.key == object::id(&key), ELockKeyMismatch);
        let Key { id } = key;
        id.delete();

        let obj = dof::remove<LockedObjectKey, T>(&mut locked.id, LockedObjectKey {});

        event::emit(LockDestroyed { lock_id: object::id(&locked) });

        let Locked { id, key: _ } = locked;
        id.delete();
        obj
    }
(3)创建共享托管对象(create
  • 创建的共享托管对象(Escrow)中会包括交换对象的接收方及其交换对象Key ID
  • 将交换对象绑定到共享托管对象(Escrow)的动态对象存储中
    public fun create<T: key + store>(
        escrowed: T,
        exchange_key: ID,
        recipient: address,
        ctx: &mut TxContext
    ) {
        let mut escrow = Escrow<T> {
            id: object::new(ctx),
            sender: ctx.sender(),
            recipient,
            exchange_key,
        };

        event::emit(EscrowCreated {
            escrow_id: object::id(&escrow),
            key_id: exchange_key,
            sender: escrow.sender,
            recipient,
            item_id: object::id(&escrowed),
        });

        dof::add(&mut escrow.id, EscrowedObjectKey {}, escrowed);

        transfer::public_share_object(escrow);
    }
(4)原子交换(swap
  • 参与方1调用该接口,传入参与方2创建共享托管对象,以及参与方1自己创建的托管对象锁Locked对象及其解锁的Key
  • 从共享托管对象中获取到参与方2绑定的交换对象(escrowed
  • 校验共享托管对象中的接受者是否是自己
  • 校验共享托管对象中的交换Key是否跟解锁Key一致
  • 通过参与方1的Key解锁Locked对象获取到参与方1的交换对象,并将其发送给参与方2
  • 最后参与方1获得参与方2的交换对象,完成交换
   /// The `recipient` of the escrow can exchange `obj` with the escrowed item
    public fun swap<T: key + store, U: key + store>(
        mut escrow: Escrow<T>,
        key: Key,
        locked: Locked<U>,
        ctx: &TxContext,
    ): T {
        let escrowed = dof::remove<EscrowedObjectKey, T>(&mut escrow.id, EscrowedObjectKey {});

        let Escrow {
            id,
            sender,
            recipient,
            exchange_key,
        } = escrow;

        assert!(recipient == ctx.sender(), EMismatchedSenderRecipient);
        assert!(exchange_key == object::id(&key), EMismatchedExchangeObject);

        // Do the actual swap
        transfer::public_transfer(locked.unlock(key), sender);

        event::emit(EscrowSwapped {
            escrow_id: id.to_inner(),
        });

        id.delete();

        escrowed
    }
(5)取回托管(return_to_sender
  • 参与方2调用该接口,可以从托管对象中取回交换对象
  • 参与方1可以调用unlock方法取回自己的交换对象
    /// The `creator` can cancel the escrow and get back the escrowed item
    public fun return_to_sender<T: key + store>(
        mut escrow: Escrow<T>,
        ctx: &TxContext
    ): T {

        event::emit(EscrowCancelled {
            escrow_id: object::id(&escrow)
        });

        let escrowed = dof::remove<EscrowedObjectKey, T>(&mut escrow.id, EscrowedObjectKey {});

        let Escrow {
            id,
            sender,
            recipient: _,
            exchange_key: _,
        } = escrow;

        assert!(sender == ctx.sender(), EMismatchedSenderRecipient);
        id.delete();
        escrowed
    }
(6)准备交换对象demo合约
module demo::demo_bear {
    use std::string::{String, utf8};
    use sui::package;
    use sui::display;

    /// our demo struct.
    public struct DemoBear has key, store {
        id: UID,
        name: String
    }

    /// our OTW to create display.
    public struct DEMO_BEAR has drop {}

    // It's recommened to create Display using PTBs instead of
    // directly on the contracts.
    // We are only creating it here for demo purposes (one-step setup).
    fun init(otw: DEMO_BEAR, ctx: &mut TxContext){
        let publisher = package::claim(otw, ctx);
         let keys = vector[
            utf8(b"name"),
            utf8(b"image_url"),
            utf8(b"description"),
        ];

        let values = vector[
            // Let's add a demo name for our `DemoBear`
            utf8(b"{name}"),
            // Adding a happy bear image.
            utf8(b"https://images.unsplash.com/photo-1589656966895-2f33e7653819?q=80&w=1000&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8Mnx8cG9sYXIlMjBiZWFyfGVufDB8fDB8fHww"),
            // Description is static for all bears out there.
            utf8(b"The greatest figure for demos"),
        ];

        // Get a new `Display` object for the `Hero` type.
        let mut display = display::new_with_fields<DemoBear>(
            &publisher, keys, values, ctx
        );

        // Commit first version of `Display` to apply changes.
        display::update_version(&mut display);

        sui::transfer::public_transfer(display, ctx.sender());
        sui::transfer::public_transfer(publisher, ctx.sender())
    }

    public fun new(name: String, ctx: &mut TxContext): DemoBear {
        DemoBear {
            id: object::new(ctx),
            name: name
        }
    }
}

2 合约部署

本文中合约部署和测试采用TypeScript程序,也可以使用CLI方式,但需要额外增加一些entry方法。

以下内容代码实现部分内容参考自:https://github.com/MystenLabs/sui/tree/mainnet-v1.27.2/examples/trading/api/helpers

2.1 部署项目创建

mkdir deployer && cd deployer
pnpm init
pnpm add typescript ts-node  @types/node -D
pnpm add @mysten/sui

2.2 编译并部署合约代码实现

成功部署合约后,会将合约PackageId写到文件中(后续应用调用时会进行读取使用)。

// 部署escrow和demo合约
(async () => {
    await publishPackage({
        packagePath: __dirname + '/../../contracts/escrow',
        network: 'testnet',
        exportFileName: 'escrow-contract',
    });

    await publishPackage({
        packagePath: __dirname + '/../../contracts/demo',
        network: 'testnet',
        exportFileName: 'demo-contract',
    });
})();

/** Publishes a package and saves the package id to a specified json file. */
export const publishPackage = async ({
  packagePath,
  network,
  exportFileName = "contract",
}: {
  packagePath: string;
  network: Network;
  exportFileName: string;
}) => {
  const txb = new Transaction();

  const { modules, dependencies } = JSON.parse(
    execSync(
      `${SUI_BIN} move build --dump-bytecode-as-base64 --path ${packagePath}`,
      {
        encoding: "utf-8",
      }
    )
  );

  const cap = txb.publish({
    modules,
    dependencies,
  });

  // Transfer the upgrade capability to the sender so they can upgrade the package later if they want.
  txb.transferObjects([cap], getActiveAddress());

  const results = await signAndExecute(txb, network);

  // 获取PackageId,以json格式写到文件中
  // @ts-ignore-next-line
  const packageId = results.objectChanges?.find(
    (x) => x.type === "published"
  )?.packageId;

  // save to an env file
  writeFileSync(
    `${exportFileName}.json`,
    JSON.stringify({
      packageId,
    }),
    { encoding: "utf8", flag: "w" }
  );
};

2.3 配套辅助方法代码实现

(1)根据别名获取地址

export const getAddressByAlias = (alias: string): string => {
  const str = execSync(`${SUI_BIN} client  switch --address ${alias}`, {
    encoding: "utf8",
  }).trim();

  const regex = /Active address switched to (\S+)/;
  const match = str.match(regex);

  return match ? match[1] : "";
};

(2)根据指定网络创建客户端对象

/** Get the client for the specified network. */
export const getClient = (network: Network) => {
  return new SuiClient({ url: getFullnodeUrl(network) });
};

(3)获取当前地址的签名者Keypair

/** Returns a signer based on the active address of system's sui. */
export const getSigner = (alias: string) => {
  const sender = getAddressByAlias(alias);

  const keystore = JSON.parse(
    readFileSync(
      path.join(homedir(), ".sui", "sui_config", "sui.keystore"),
      "utf8"
    )
  );

  for (const priv of keystore) {
    const raw = fromB64(priv);
    if (raw[0] !== 0) {
      continue;
    }

    const pair = Ed25519Keypair.fromSecretKey(raw.slice(1));
    if (pair.getPublicKey().toSuiAddress() === sender) {
      return pair;
    }
  }

  throw new Error(`keypair not found for sender: ${sender}`);
};

(4)签名并执行交易

/** A helper to sign & execute a transaction. */
export const signAndExecute = async (
  txb: Transaction,
  network: Network,
  alias: string
) => {
  const client = getClient(network);
  const signer = getSigner(alias);

  return client.signAndExecuteTransaction({
    transaction: txb,
    signer,
    options: {
      showEffects: true,
      showObjectChanges: true,
    },
  });
};

2.4 帐号准备及角色分配

别名 地址 角色
Jason 0x5c5882d73a6e5b6ea1743fb028eff5e0d7cc8b7ae123d27856c5fe666d91569a 部署合约
Alice 0x2d178b9704706393d2630fe6cf9415c2c50b181e9e3c7a977237bb2929f82d19 用户1:创建锁定熊、执行swap操作
Bob 0xf2e6ffef7d0543e258d4c47a53d6fa9872de4630cc186950accbd83415b009f0 用户2:创建托管共享对象
  • 将地址添加到环境变量
export JASON=0x5c5882d73a6e5b6ea1743fb028eff5e0d7cc8b7ae123d27856c5fe666d91569a
export ALICE=0x2d178b9704706393d2630fe6cf9415c2c50b181e9e3c7a977237bb2929f82d19
export BOB=0xf2e6ffef7d0543e258d4c47a53d6fa9872de4630cc186950accbd83415b009f0

2.5 合约部署

切换到Jason账号,进行合约部署

  • 执行部署命令
$ npx ts-node tools/publish-contracts.ts
  • 执行后将会看到生成两个包含PackageID的文件
$ cat escrow-contract.json 
{"packageId":"0x76d7e0976b97567857238b2f2f03b649c7672e81977b8982c0e7894c70f5a95f"}

$ cat demo-contract.json 
{"packageId":"0xdb5c5ae42f826e909463c06882af09d020e51c110b7c0362e9af8eb15ce26a1a"}

3 功能测试

3.1 用户1(Alice)创建锁定熊

type LockedKeyType = {
  lockedObjectId: string;
  keyObjectId: string;
};

const createLockedBear = async (
  alias: string
): Promise<LockedKeyType | undefined> => {
  const txb = new Transaction();

  const bear = txb.moveCall({
    target: `${CONFIG.DEMO_CONTRACT.packageId}::demo_bear::new`,
    arguments: [txb.pure.string(`A happy bear`)],
  });

  const [locked, key] = txb.moveCall({
    target: `${CONFIG.SWAP_CONTRACT.packageId}::lock::lock`,
    arguments: [bear],
    typeArguments: [DEMO_BEAR_TYPE],
  });

  txb.transferObjects(
    [locked, key],
    txb.pure.address(getAddressByAlias(alias))
  );

  const res = await signAndExecute(txb, ACTIVE_NETWORK, alias);

  if (!res.objectChanges || res.objectChanges.length === 0)
    throw new Error("create and locked bears failed");

  const createdBearObjectId = res.objectChanges
    ?.filter((x) => x.type === "created")
    .find((x: { objectType: string }) =>
      x.objectType.endsWith("::demo_bear::DemoBear")
    )?.objectId;

  const keyObjectId = res.objectChanges
    ?.filter((x) => x.type === "created")
    .find((x: { objectType: string }) =>
      x.objectType.endsWith("::lock::Key")
    )?.objectId;

  const lockedObjectId = res.objectChanges
    ?.filter((x) => x.type === "created")
    .find((x: { objectType: string }) =>
      x.objectType.includes("::lock::Locked<")
    )?.objectId;

  if (!lockedObjectId || !keyObjectId) {
    return undefined;
  }

  console.log(
    `${alias}:${getAddressByAlias(
      alias
    )} create and locked escrow bears ${createdBearObjectId}, 
      [LockedObjectId: ${lockedObjectId}]/[KeyObjectId: ${keyObjectId}] success`
  );

  return {
    lockedObjectId,
    keyObjectId,
  };
};

3.2 用户2(Bob)创建待托管熊

创建的托管熊对象,会放到托管共享对象中。

const DEMO_BEAR_TYPE = `${CONFIG.DEMO_CONTRACT.packageId}::demo_bear::DemoBear`;

const createEscrowBear = async (alias: string): Promise<string | undefined> => {
  const txb = new Transaction();

  const bear = txb.moveCall({
    target: `${CONFIG.DEMO_CONTRACT.packageId}::demo_bear::new`,
    arguments: [txb.pure.string(`A happy bear`)],
  });
  txb.transferObjects([bear], txb.pure.address(getAddressByAlias(alias)));

  const res = await signAndExecute(txb, ACTIVE_NETWORK, alias);
  const createdObjectId = res.objectChanges?.find(
    (x) => x.type === "created"
  )?.objectId;

  if (!res.objectChanges || res.objectChanges.length === 0)
    throw new Error("create escrow bears failed, no object changes returned");

  console.log(
    `${alias}:${getAddressByAlias(
      alias
    )} create escrow bears ${createdObjectId} success`
  );

  return createdObjectId;
};

3.3 用户2(Bob)创建待托管共享对象

会使用到3.1创建的待托管熊,以及3.2创建的Key对象ID。最后会创建一个共享的托管对象。

const createEscrow = async (
  escrowBearObjectId: string,
  keyObjectId: string,
  sender_alias: string,
  locker_alias: string
): Promise<string | undefined> => {
  const txb = new Transaction();

  txb.moveCall({
    target: `${CONFIG.SWAP_CONTRACT.packageId}::shared::create`,
    arguments: [
      txb.object(escrowBearObjectId),
      txb.pure.address(keyObjectId),
      txb.pure.address(getAddressByAlias(locker_alias)),
    ],
    typeArguments: [DEMO_BEAR_TYPE],
  });

  const res = await signAndExecute(txb, CONFIG.NETWORK, sender_alias);
  if (!res.objectChanges || res.objectChanges.length === 0)
    throw new Error("create escrow failed");

  console.log(res);

  const sharedEscrowObjectId = res.objectChanges
    ?.filter((x) => x.type === "created")
    .find((x: { objectType: string }) =>
      x.objectType.includes("::shared::Escrow<")
    )?.objectId;

  console.log(`${sender_alias} create escrow ${sharedEscrowObjectId} success`);

  return sharedEscrowObjectId;
};

3.4 用户1(Alice)执行swap操作

const getObjectById = async (objectId: string) => {
  const client = getClient(CONFIG.NETWORK);

  const res = await client.getObject({
    id: objectId,
    options: {
      showType: true,
      showOwner: true,
    },
  });

  return res;
};

const swap = async (
  sharedEscrowObjectId: string,
  keyObjectId: string,
  lockedObjectId: string,
  locker_alias: string
) => {
  const txb = new Transaction();

  const sharedEscrowType = (await getObjectById(sharedEscrowObjectId)).data
    ?.type;
  if (!sharedEscrowType) {
    throw new Error("Failed to get shared escrow object type");
  }

  const lockedType = (await getObjectById(lockedObjectId)).data?.type;
  if (!lockedType) {
    throw new Error("Failed to get locked object type");
  }

  const escrowed = txb.moveCall({
    target: `${CONFIG.SWAP_CONTRACT.packageId}::shared::swap`,
    arguments: [
      txb.object(sharedEscrowObjectId),
      txb.object(keyObjectId),
      txb.object(lockedObjectId),
    ],
    typeArguments: [DEMO_BEAR_TYPE, DEMO_BEAR_TYPE],
  });

  txb.transferObjects(
    [escrowed],
    txb.pure.address(getAddressByAlias(locker_alias))
  );

  txb.setGasBudget(100000000);

  const res = await signAndExecute(txb, CONFIG.NETWORK, locker_alias);
  if (!res.objectChanges || res.objectChanges.length === 0)
    throw new Error("swap failed");

  console.log(res);

  console.log(`${locker_alias} swap ${sharedEscrowObjectId} success`);
};

3.5 main函数

const ALICE = "alice";
const BOB = "bob";

async function main() {
  const lockedKey = await createLockedBear(ALICE);
  if (!lockedKey) {
    throw new Error("Failed to create locked bear");
  }

  const escrowBearObjectId = await createEscrowBear(BOB);
  if (!escrowBearObjectId) {
    throw new Error("Failed to create escrow bear");
  }

  const lockedObjectId = lockedKey?.lockedObjectId;
  const keyObjectId = lockedKey?.keyObjectId;

  // 用户1创建托管对象
  const sharedEscrowObjectId = await createEscrow(
    escrowBearObjectId,
    keyObjectId,
    BOB,
    ALICE
  );
  if (!sharedEscrowObjectId) {
    throw new Error("Failed to create shared escrow");
  }

  // 托管交换
  await swap(sharedEscrowObjectId, keyObjectId, lockedObjectId, ALICE);
}

3.6 执行测试命令

  • 执行命令
$ npx ts-node tools/auto_test.ts
  • 将会看到以下日志
# Alice创建锁定熊:0x7501b6cfaaea9b702d0078708d7b2a6501062b5f0c280e0fb797ad16f4eab816
alice:0x2d178b9704706393d2630fe6cf9415c2c50b181e9e3c7a977237bb2929f82d19 create and locked escrow bears 0x7501b6cfaaea9b702d0078708d7b2a6501062b5f0c280e0fb797ad16f4eab816, 
      [LockedObjectId: 0x79da4f0802adf8f863e39328ca14dd65cf4bbac353ba47071f17b5a2a4a5fd86]/[KeyObjectId: 0xb524ee7c3464460d7b247423bcb0ed762986fe5d1d88647c3dceccbeec2e71a5] success

# Bob创建托管熊:0x4062011a41b15825b32645f7c118e435d9a97e1291bc3b6f126760bf16f021fb
bob:0xf2e6ffef7d0543e258d4c47a53d6fa9872de4630cc186950accbd83415b009f0 create escrow bears 0x4062011a41b15825b32645f7c118e435d9a97e1291bc3b6f126760bf16f021fb success

# Bob闯将托管共享对象
bob create escrow 0xd829028ba9c7d28aa579d60402b02f346734f2534e56873a71e713fd2e5f715f success

# Alice执行swap操作
alice swap 0xd829028ba9c7d28aa579d60402b02f346734f2534e56873a71e713fd2e5f715f success

3.7 检查swap结果

经查看呆萌熊最新归属,已经发生了交换:

image.png

4 参考资料

https://docs.sui.io/guides/developer/app-examples/trustless-swap/backend

5 更多

欢迎关注微信公众号:Move中文,开启你的 Sui Move 之旅!

image.png

点赞 1
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
rzexin
rzexin
0x6Fa5...8165
江湖只有他的大名,没有他的介绍。