SUI Move官方示例合约实践——NFT类:零信任原子交换(trustless swap)
本合约示例是官方提供的零信任原子交换合约,它类似于托管,但不需要可信的第三方。
合约中使用共享对象来作为两个想要交易对象之间的托管。共享对象是Sui
的一个独特概念。任何用户的签名交易都可以对其发起修改,但成功修改的前提是需要满足其中定义的约束。
该合约原子交换的过程分为三个阶段:
lock
接口锁定其交换对象,将会获得一个Locked
对象以及用于解锁的Key
对象。如果另一方故意拖延(stalls
)不配合完成第二阶段的话,第一方可以unlock
其锁定对象,使其保持活性。create
创建一个公开可访问的共享托管对象(Escrow
),也将其交换对象进行锁定,并等待第一方完成swap
操作。第二方同样也可以取回他们托管的交换对象,使其保持活性。Locked
对象及其解锁的Key
对象发送到共享的托管对象。只要满足如下所有条件,就能成功完成了交换:
exchange_key
)的密钥与交换中提供的解锁密钥(Key
)匹配Key
能解锁锁定的Locked
对象https://github.com/MystenLabs/sui/tree/mainnet-v1.27.2/examples/trading/contracts/escrow
/// The `name` of the DOF that holds the Locked object.
/// Allows better discoverability for the locked object.
public struct LockedObjectKey has copy, store, drop {}
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,
}
Key
结构 /// Key to open a locked object (consuming the `Key`)
public struct Key has key, store { id: UID }
/// The `name` of the DOF that holds the Escrowed object.
/// Allows easy discoverability for the escrowed object.
public struct EscrowedObjectKey has copy, store, drop {}
/// 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,
}
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)
}
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
}
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);
}
swap
)Locked
对象及其解锁的Key
escrowed
)Key
是否跟解锁Key
一致Key
解锁Locked
对象获取到参与方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
}
return_to_sender
)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
}
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
}
}
}
本文中合约部署和测试采用TypeScript
程序,也可以使用CLI
方式,但需要额外增加一些entry
方法。
以下内容代码实现部分内容参考自:https://github.com/MystenLabs/sui/tree/mainnet-v1.27.2/examples/trading/api/helpers
mkdir deployer && cd deployer
pnpm init
pnpm add typescript ts-node @types/node -D
pnpm add @mysten/sui
成功部署合约后,会将合约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" }
);
};
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] : "";
};
/** Get the client for the specified network. */
export const getClient = (network: Network) => {
return new SuiClient({ url: getFullnodeUrl(network) });
};
/** 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}`);
};
/** 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,
},
});
};
别名 | 地址 | 角色 |
---|---|---|
Jason | 0x5c5882d73a6e5b6ea1743fb028eff5e0d7cc8b7ae123d27856c5fe666d91569a |
部署合约 |
Alice | 0x2d178b9704706393d2630fe6cf9415c2c50b181e9e3c7a977237bb2929f82d19 |
用户1:创建锁定熊、执行swap 操作 |
Bob | 0xf2e6ffef7d0543e258d4c47a53d6fa9872de4630cc186950accbd83415b009f0 |
用户2:创建托管共享对象 |
export JASON=0x5c5882d73a6e5b6ea1743fb028eff5e0d7cc8b7ae123d27856c5fe666d91569a
export ALICE=0x2d178b9704706393d2630fe6cf9415c2c50b181e9e3c7a977237bb2929f82d19
export BOB=0xf2e6ffef7d0543e258d4c47a53d6fa9872de4630cc186950accbd83415b009f0
切换到
Jason
账号,进行合约部署
$ npx ts-node tools/publish-contracts.ts
$ cat escrow-contract.json
{"packageId":"0x76d7e0976b97567857238b2f2f03b649c7672e81977b8982c0e7894c70f5a95f"}
$ cat demo-contract.json
{"packageId":"0xdb5c5ae42f826e909463c06882af09d020e51c110b7c0362e9af8eb15ce26a1a"}
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,
};
};
创建的托管熊对象,会放到托管共享对象中。
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.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;
};
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`);
};
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);
}
$ 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
经查看呆萌熊最新归属,已经发生了交换:
https://docs.sui.io/guides/developer/app-examples/trustless-swap/backend
欢迎关注微信公众号:Move中文,开启你的 Sui Move 之旅!
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!