我正在参加「掘金·启航计划」!
前言
作为和 Aptos 同期出名的 Move 语言生态双子星的 Sui 网络,在北京时间5月3日晚上8点,也已上线了主网。那么要进入 Sui 生态进行 dapp 的开发,要先记住一句话:万物皆是 Object。
- 为什么这么说呢?
Sui 借鉴了 BTC 的 UTXO 的模式,它将每个交易输出视为一个未使用的交易,使得交易的输入和输出可以更精细地控制和跟踪。
用更为直观的方式来解读,在链上,我的资产,也是一个个 Object 的模式。看下图:
一个账号下可以有多种 Coin,每一种 Coin 都是由一个个的 Object 组成,所有这种 Coin 的 Object 的 balance 相加,就是账户下这种 Coin 的余额。
而 nft 作为非同质化代币,在 Sui 上的表现,也是一个 Object,只不过它是一种更为特殊的 Object。
进入开发
连接钱包
1. 安装依赖
作为 dapp 开发,第一步势必是从连接插件钱包开始,这里我们使用 suiet 团队开发的库,相较于官方的,它对于其他钱包连接的支持更好
yarn add @suiet/wallet-kit
2. 引入 WalletProvider
在项目的根文件中,引入 WalletProvider
(也就是在你 React 项目的根文件中,如果是 nextjs,则在 _app.tsx 中)
<WalletProvider>
<Component {...pageProps} />
</WalletProvider>
3. 在页面中连接钱包
- 可以使用 Suiet 中自带的按钮
ConnectButton
,点击后展开钱包列表
import { ConnectButton } from '@suiet/wallet-kit'
//...
<ConnectButton />
- 也可以根据需求,支持哪些钱包的连接
这里 select 的参数,现在可以使用有 Suiet
、Sui Wallet
、Ethos Wallet
和 Martian Sui Wallet
这4种,分别对应4种钱包。其中 Sui Wallet
是官方钱包
const { select } = useWallet()
<button onClick={() => select('Suiet')}>Connect wallet</button>
4. 获取地址和连接状态
const { address, connected } = useWallet()
5. 签名(signMessage)
import { ethers } from 'ethers';
// 这里借用 ethers 库来帮我们处理 message,将其转为 Uint8Array 类型
const message = ethers.utils.toUtf8Bytes('test 123')
const { signature, messageBytes } = await signMessage({ message })
6. 发送交易
这里要注意一下,sui 的交易和之前的 aptos、evm 都有所区别
- 普通交易,不涉及到自己的资产的
import { TransactionBlock } from '@mysten/sui.js';
const { signAndExecuteTransactionBlock } = useWallet()
const handleTransaction = async () => {
const tx = new TransactionBlock()
tx.moveCall({
target: `${packageId}::${moduleName}::${functionName}`,
arguments: [
tx.pure(参数1),
tx.pure(参数2),
],
typeArguments: [],
})
const result = await signAndExecuteTransactionBlock({
transactionBlock: tx,
options: { showEffects: true },
})
console.log('result', result)
// 通过result?.effects?.status?.status获取交易状态,成功为 'success',失败为'failure'
}
<button onClick={handleTransaction}>Send Transaction</button>
- 涉及到资产交易的。
由于上文提到过 Sui 的核心:万物皆 object,在对资产进行交易时,需要传入作为费用的 objectId。
但这又会出现一个问题:如果我只有一个 object,我既要用这个 object 来作为交易费用,同时,我有需要一个 object 来支付这笔交易的 gasFee,那这交易应该怎么发呢?
这里就会引入一个新的概念:拆币(splitCoin)。也就是说,我需要发一笔交易,把一个 object 拆成了两个,一个足以支付交易费用,一个集合了剩余的 balance 的 object 来作为 gasFee。
例如:我的账户下当前只有一个 balance 为 0.5 的 object。我要发一笔交易,这个合约需要收 0.3 的费用。那么此时,我需要发送一笔拆币交易,将这个 0.5 的 object 拆出一个 0.3 的object,剩余部分作为一个 object 来作为 gasFee。
但如果每次交易都要分开发送两笔:拆币、调用合约,那太复杂了,这里我们将它们合并成一笔交易:
import { TransactionBlock } from '@mysten/sui.js';
const { signAndExecuteTransactionBlock } = useWallet()
const handleTransaction = async () => {
const tx = new TransactionBlock()
const value = '300000000' // 这里是想要拆出的目标值
const [coins] = tx.splitCoins(tx.gas, [
tx.pure(BigInt(value)),
])
tx.moveCall({
target: `${packageId}::${moduleName}::${functionName}`,
arguments: [
tx.pure(参数1),
tx.pure(参数2),
tx.makeMoveVec({ objects: [coins] }),
],
typeArguments: [],
})
const result = await signAndExecuteTransactionBlock({
transactionBlock: tx,
options: { showEffects: true },
})
console.log('result', result)
// 通过result?.effects?.status?.status获取交易状态,成功为 'success',失败为'failure'
}
<button onClick={handleTransaction}>Send Transaction</button>
可以看到,这里的交易合并了 splitCoin
和 moveCall
两笔交易。为了确保所有人都能够正常交易,建议涉及到资产的交易,都加入 splitCoin
。
常用 rpc 方法
1. 创建 rpcProvider
import { Connection, JsonRpcProvider } from '@mysten/sui.js';
const suiProvider = new JsonRpcProvider(new Connection({ fullnode: `${节点rpc地址}` }))
2. 获取余额
const { totalBalance } = await suiProvider.getBalance({
owner: `${你的地址}`,
coinType: `${币的类型地址}` //如果不传,那么返回的就是 SUI 的 balance
})
3. 获取账户下的所有 object,注意:这里的 getOwnedObjects 是分页返回的
const allObjects = await suiProvider.getOwnedObjects({
owner: address,
options: {
showType: true,
showDisplay: true,
showContent: true,
}
});
4. 筛选账户下的 nft
import { Coin } from '@mysten/sui.js';
const objectIDs = (allObjectRefs?.data || [])
.filter((item) => !Coin.isCoin(item))
.map((anObj) => anObj.data.objectId);
5. 模拟交易(调用合约的 public 的方法,也用这个)
import { TransactionBlock, JsonRpcProvider, Connection } from '@mysten/sui.js';
const suiProvider = new JsonRpcProvider(new Connection({ fullnode: `${节点rpc地址}` }))
const tx = new TransactionBlock()
tx.moveCall({
target: `${packageId}::${moduleName}::${functionName}`,
arguments: [
tx.pure(参数1),
tx.pure(参数2),
],
typeArguments: [],
})
const result = await suiProvider.devInspectTransactionBlock({
transactionBlock: tx,
sender: `${地址}`
})
总结
Sui 相对于 evm、aptos 生态,它是一种完全不同的链。不管是内部实现,还是对于 dapp 开发者来说,都是一种非常新奇的体验。