Sui Release .28 Migration Guide
Sui release .28 introduces many Breaking changes that require updates or changes to your app or implementation to continue functioning as expected. This guide provides migration steps to help you make the required updates. It also includes descriptions of some new features included in the release.
Sui Move updates
The steps in this section provide guidance on making updates related to the changes to Sui Move.
Sui Framework split into two Move packages
Updated 3/27/23
The Sui Framework (sui-framework
) Move package contains modules central to Sui Move, including object
, transfer
and dynamic_field
. The framework package is often a dependency of application packages developed on Sui. Previously, sui-framework
included a governance
folder that defined a number of modules related to the operations of Sui’s system, such as validator_set
and staking_pool
. These modules are fundamentally different from the other library modules within the framework, and are not commonly used by developers. To simplify, this release splits sui-framework
into two Sui Move packages to improve modularity, usability and upgrading.
In PR 9618, the sui-framework
crate now contains three Move packages in the packages
directory: move-stdlib
, sui-framework
and sui-system
:
sui-system
contains modules that were in thesui-framework/sources/governance
directory, including all the validator management and staking related functions, published at0x3
with named addresssui_system
.sui-framework
contains all other modules that were not in thegovernance
folder. The framework provides library and utility modules for Sui Move developers. It is still published at0x2
with named addresssui
.move-stdlib
contains a copy of the Move standard library that used to be in thesui-framework/deps
folder. It is still published at0x1
with named addressstd
.
If you develop Sui Move code depending on sui-framework
, the Move.toml
file of your Move package has to change to reflect the path changes:
Prior to this release:
[package]
name = "Example"
version = "0.0.1"
published-at = "0x42"
[dependencies]
Sui = { git = "https://github.com/MystenLabs/sui.git", subdir="crates/sui-framework/", rev = "devnet" }
[addresses]
example = "0x42"
Updated for this release:
[package]
name = "Example"
version = "0.0.1"
published-at = "0x42"
[dependencies]
Sui = { git = "https://github.com/MystenLabs/sui.git", subdir="crates/sui-framework/packages/sui-framework/", rev = "devnet" }
[addresses]
example = "0x42"
If your Sui Move code uses a module in the governance
folder:
genesis.move
, sui_system.move
, validator_cap.move
, voting_power.move
,
stake_subsidy.move
, sui_system_state_inner.move
, validator_set.move
,
staking_pool.move
, validator.move
, or validator_wrapper.move
The modules are now in the sui-system
package. You must list SuiSystem
as a dependency, and access these modules via 0x3
or the sui_system
named address.
Erecover and verify
In this release, ecdsa_k1::ecrecover
and ecdsa_k1::secp256k1_verify
now require you to input the raw message instead of a hashed message.
-
ecdsa_k1::ecrecover(sig, hashed_msg, hash_function)
is updated toecdsa_k1::secp256k1_ecrecover(sig, msg, hash_function)
-
ecdsa_k1::secp256k1_verify(sig, pk, hashed_msg)
is updated toecdsa_k1::secp256k1_verify(sig, pk, msg, hash_function)
When you call these APIs, you must provide the raw message instead of the hashed message for verification or EC recover. You must also provide the hash_function name represented by u8. See the source code for more information:
ID Leak
UID
s must now be statically deemed “fresh” when you construct an object. This means that the UID
must come from object::new
(or test_scenario::new_object
during tests). To migrate an existing project, any function that previously took a UID
as an argument to construct an object now requires the TxContext
to generate new IDs.
For example, prior to release .28, to construct an object:
fun new(id: UID): Counter {
Counter { id, count: 0 }
}
With release .28, to construct an object:
fun new(ctx: &mut TxContext): Counter {
Counter { id: object::new(ctx), count: 0 }
}
Publisher
This is not a breaking change, but Publisher
is an important addition that can be helpful. The Publisher
object requires an OTW and can be claimed in any module within a package via the sui::package::claim
call:
module example::dummy {
use sui::package;
use sui::tx_context::TxContext;
struct DUMMY has drop {}
fun init(otw: DUMMY, ctx: &mut TxContext) {
// creates a Publisher object and sends to the `sender`
package::claim_and_keep(otw, ctx)
}
}
To learn more about Publisher
, see Publisher.
Sui Object Display Standard
This release includes the Sui Object Display standard, a new way to describe objects of a single type using a set of named templates to standardize their display off-chain. The Sui API also supports the new standard.
To read a detailed description and the motivation behind the standard, see the Sui Object Display proposal.
In Sui Move, to claim a Display
object, call display::new<T>(&Publisher)
. As stated in the signature, it requires the Publisher
object. Once acquired, Display
can be modified by adding new fields (templates) to it, and when it’s ready to be published, a display::update_version(&mut Display)
call is required to publish and make it available. All further additions / edits in the Display
should also be applied by calling update_version
again.
Fields that we suggest to use in Display
are:
- name: a displayable name
- link: a link to an object in an application / external link
- description: a broader displayable description
- image_url: an URL or a blob with an image
- project_url: a link to a website
- creator: mentions the creator in any way (text, link, address etc)
See additional information and examples in Display.
API and SDK changes
The steps in this section provide guidance on making updates related to the changes to Sui APIs and SDKs.
Reading objects
The sui_getObject
endpoint now takes an additional configuration parameter of type SuiObjectDataOptions
to control which fields the endpoint retrieves. By default, the endpoint retrieves only object references unless the client request explicitly specifies other data, such as type
, owner
, or bcs
.
TypeScript Migration
import { JsonRpcProvider } from "@mysten/sui.js";
const provider = new JsonRpcProvider();
// Prior to release .28
const txn = await provider.getObject(
"0xcff6ccc8707aa517b4f1b95750a2a8c666012df3"
);
const txns = await provider.getObjectBatch([
"0xcff6ccc8707aa517b4f1b95750a2a8c666012df3",
"0xdff6ccc8707aa517b4f1b95750a2a8c666012df3",
]);
// Updated for release .28
const txn = await provider.getObject({
id: "0xcff6ccc8707aa517b4f1b95750a2a8c666012df3",
// fetch the object content field and display
options: {
showContent: true,
showDisplay: true,
},
});
const txns = await provider.multiGetObjects({
ids: [
"0xcff6ccc8707aa517b4f1b95750a2a8c666012df3",
"0xdff6ccc8707aa517b4f1b95750a2a8c666012df3",
],
// only fetch the object type
options: { showType: true },
});
JSON RPC Migration
# Prior to release .28
curl --location --request POST 'https://fullnode.devnet.sui.io:443' \
--header 'Content-Type: application/json' \
--data-raw '{
"jsonrpc": "2.0",
"id": 1,
"method": "sui_getObject",
"params": {
"object_id": "0x08240661f5504c9bb4a487d9a28e7e9d6822abf692801f2a750d67a44d0b2340",
}
}'
# Updated for release .28
curl --location --request POST 'https://fullnode.devnet.sui.io:443' \
--header 'Content-Type: application/json' \
--data-raw '{
"jsonrpc": "2.0",
"id": 1,
"method": "sui_getObject",
"params": {
"object_id": "0x08240661f5504c9bb4a487d9a28e7e9d6822abf692801f2a750d67a44d0b2340",
"options": {
"showContent": true,
"showOwner": true,
}
}
}'
# If you use sui_getRawObject, enable the showBcs option to retrieve it
curl --location --request POST 'https://fullnode.devnet.sui.io:443' \
--header 'Content-Type: application/json' \
--data-raw '{
"jsonrpc": "2.0",
"id": 1,
"method": "sui_getObject",
"params": {
"object_id": "0x08240661f5504c9bb4a487d9a28e7e9d6822abf692801f2a750d67a44d0b2340",
"options": {
"showBcs": true
}
}
}'
Display
To get a Display
for an object, pass an additional flag to the sui_getObject
call.
{
showDisplay: true;
}
The returned value is the processed template for a type. For example, for Sui Capys it could be:
{
"name": "Capy - one of many",
"description": "Join our Capy adventure",
"link": "https://capy.art/capy/0x00000000....",
"image_url": "https://api.capy.art/capys/0x000adadada..../svg",
"project_url": "https://capy.art/",
"creator": "Capybara Lovers"
}
Reading transactions
The sui_getTransactionBlock
and sui_multiGetTransaction
functions now take an additional optional parameter called options
. Use options
to specify which fields to retrieve, such as transaction, effects, or events. By default, it returns only the transaction digest.
import { JsonRpcProvider } from "@mysten/sui.js";
const provider = new JsonRpcProvider();
// Prior to release .28
const provider = new JsonRpcProvider();
const txn = await provider.getTransactionWithEffects(
"6mn5W1CczLwitHCO9OIUbqirNrQ0cuKdyxaNe16SAME="
);
// You can also fetch multiple transactions in one batch request
const txns = await provider.getTransactionWithEffectsBatch([
"6mn5W1CczLwitHCO9OIUbqirNrQ0cuKdyxaNe16SAME=",
"7mn5W1CczLwitHCO9OIUbqirNrQ0cuKdyxaNe16SAME=",
]);
// Updated for release .28
const provider = new JsonRpcProvider();
const txn = await provider.getTransactionBlock({
digest: "6mn5W1CczLwitHCO9OIUbqirNrQ0cuKdyxaNe16SAME=",
// only fetch the effects field
options: { showEffects: true },
});
// You can also fetch multiple transactions in one batch request
const txns = await provider.multiGetTransactionBlocks({
digests: [
"6mn5W1CczLwitHCO9OIUbqirNrQ0cuKdyxaNe16SAME=",
"7mn5W1CczLwitHCO9OIUbqirNrQ0cuKdyxaNe16SAME=",
],
// fetch both the input transaction data as well as effects
options: { showInput: true, showEffects: true },
});
Reading Events
This release makes the following changes related to reading events:
- Removes System events such as
Publish
,TransferObject
,NewObject
and keeps onlyMoveEvents
. - Adds an
object_changes
andbalance_changes
field inSuiTransactionBlockResponse
import { JsonRpcProvider } from "@mysten/sui.js";
const provider = new JsonRpcProvider();
// Prior to release .28
provider.getEvents({ Sender: toolbox.address() }, null, 2);
// Updated for release .28
const events = provider.queryEvents({
query: { Sender: toolbox.address() },
limit: 2,
});
// Subscribe events
// Prior to release .28
const subscriptionId = await provider.subscribeEvent(
{ SenderAddress: "0xbff6ccc8707aa517b4f1b95750a2a8c666012df3" },
(event: SuiEventEnvelope) => {
// handle subscription notification message here. This function is called once per subscription message.
}
);
// later, to unsubscribe
// calls RPC method 'sui_unsubscribeEvent' with params: [ subscriptionId ]
const subFoundAndRemoved = await provider.unsubscribeEvent(subscriptionId);
// Updated for release .28
// calls RPC method 'sui_subscribeEvent' with params:
// [ { Sender: '0xbff6ccc8707aa517b4f1b95750a2a8c666012df3' } ]
const subscriptionId = await provider.subscribeEvent({
filter: { Sender: "0xbff6ccc8707aa517b4f1b95750a2a8c666012df3" },
onMessage(event: SuiEvent) {
// handle subscription notification message here. This function is called once per subscription message.
},
});
// later, to unsubscribe
// calls RPC method 'sui_unsubscribeEvent' with params: [ subscriptionId ]
const subFoundAndRemoved = await provider.unsubscribeEvent({
id: subscriptionId,
});
Pagination
This release changes the Page
definition.
Prior to release .28
pub struct Page<T, C> {
pub data: Vec<T>,
pub next_cursor: Option<C>,
}
Updated for release .28
pub struct Page<T, C> {
pub data: Vec<T>,
pub next_cursor: Option<C>,
pub has_next_page: bool,
}
Additionally:
next_cursor
is no longer inclusive and now exclusive, it always points to the last item ofdata
if data is not empty;- To check if the current page is the last page or not, instead of doing
next_cursor.is_none()
, now you can simply usehas_next_page
If you use Page
to read pages one by one, now you do not have to manually handle the returned None
value of next_cursor
when the reading process hits the latest page, instead you can always use the returned next_cursor
as the input argument of next read. Before this release, the reading process will start from genesis when it hits latest and the None
value is not properly handled.
Building and executing transaction
The previous transaction builder methods on the Signer
, and the SignableTransaction
interface have been removed, and replaced with a new Transaction
builder class. This new transaction builder takes full advantage of Programmable Transactions.
// Construct a new transaction:
const tx = new Transaction();
// Example replacement for a SUI token transfer:
const [coin] = tx.splitCoins(tx.gas, [tx.pure(1000)]);
tx.transferObjects([coin], tx.pure(keypair.getPublicKey().toSuiAddress()));
// Merge a list of coins into a primary coin:
tx.mergeCoin(tx.object("0xcoinA"), [
tx.object("0xcoinB"),
tx.object("0xcoinC"),
]);
// Make a move call:
tx.moveCall({
target: `${packageObjectId}::nft::mint`,
arguments: [tx.pure("Example NFT")],
});
// Execute a transaction:
const result = await signer.signAndExecuteTransaction({ transaction: tx });
Transaction now support providing a list of gas coins as payment for a transaction. By default, the transaction builder automatically determines the gas budget and coins to use as payment for a transaction. You can also set these values, for example to set your own budget, change the gas price, or do your own gas selection:
// Set an explicit gas price. By default, uses the current reference gas price:
tx.setGasPrice(100);
// Change the gas budget (in SUI). By default, this executes a dry run and uses the gas consumed from that as the budget.
tx.setGasBudget(customBudgetDefined);
// Set the vector of gas objects to use as the gas payment.
tx.setGasPayment([coin1, coin2]);
Staking changes
The steps in this section provide guidance on making updates related to the changes to Sui staking.
Locked coin staking removed
Previously users could stake either their Coin<SUI>
or their LockedCoin<SUI>
with a validator. This release removes support for staking locked coins so stake functions can now take only Coin<SUI>
.
StakedSui object layout changed
Prior to this release, StakedSui struct had the following definition:
struct StakedSui has key {
id: UID,
/// The validator we are staking with.
validator_address: address,
/// The epoch at which the staking pool started operating.
pool_starting_epoch: u64,
/// The epoch at which the delegation is requested.
delegation_request_epoch: u64,
/// The staked SUI tokens.
principal: Balance<SUI>,
/// If the stake comes from a Coin<SUI>, this field is None. If it comes from a LockedCoin<SUI>, this
/// field will record the original lock expiration epoch, to be used when unstaking.
sui_token_lock: Option<EpochTimeLock>,
}
With the removal of locked coin staking and changes to the Sui staking flow, the new struct definition is:
struct StakedSui has key {
id: UID,
/// ID of the staking pool we are staking with.
pool_id: ID,
/// The validator we are staking with.
validator_address: address,
/// The epoch at which the stake becomes active.
stake_activation_epoch: u64,
/// The staked SUI tokens.
principal: Balance<SUI>,
}
Changes to stake deposit / withdraw APIs
This release includes the following changes related to stake deposit and withdrawal requests:
- Removes the
request_switch_delegation
function - Renames all delegation functions to use staking instead of delegation.
Prior to release .28, the function names were:
/// Add delegated stake to a validator's staking pool using multiple coins and amount.
#[method(name = "requestAddDelegation")]
async fn request_add_delegation(
&self,
/// the transaction signer's Sui address
signer: SuiAddress,
/// Coin<SUI> or LockedCoin<SUI> object to delegate
coins: Vec<ObjectID>,
/// delegation amount
amount: Option<u64>,
/// the validator's Sui address
validator: SuiAddress,
/// gas object to be used in this transaction, node will pick one from the signer's possession if not provided
gas: Option<ObjectID>,
/// the gas budget, the transaction will fail if the gas cost exceed the budget
gas_budget: u64,
) -> RpcResult<TransactionBlockBytes>;
/// Withdraw a delegation from a validator's staking pool.
#[method(name = "requestWithdrawDelegation")]
async fn request_withdraw_delegation(
&self,
/// the transaction signer's Sui address
signer: SuiAddress,
/// Delegation object ID
delegation: ObjectID,
/// StakedSui object ID
staked_sui: ObjectID,
/// gas object to be used in this transaction, node will pick one from the signer's possession if not provided
gas: Option<ObjectID>,
/// the gas budget, the transaction will fail if the gas cost exceed the budget
gas_budget: u64,
) -> RpcResult<TransactionBlockBytes>;
/// Switch delegation from the current validator to a new one.
#[method(name = "requestSwitchDelegation")]
async fn request_switch_delegation(
&self,
/// the transaction signer's Sui address
signer: SuiAddress,
/// Delegation object ID
delegation: ObjectID,
/// StakedSui object ID
staked_sui: ObjectID,
/// Validator to switch to
new_validator_address: SuiAddress,
/// gas object to be used in this transaction, node will pick one from the signer's possession if not provided
gas: Option<ObjectID>,
/// the gas budget, the transaction will fail if the gas cost exceed the budget
gas_budget: u64,
) -> RpcResult<TransactionBlockBytes>;
Effective with release .28, the function names are:
/// Add stake to a validator's staking pool using multiple coins and amount.
#[method(name = "requestAddStake")]
async fn request_add_stake(
&self,
/// the transaction signer's Sui address
signer: SuiAddress,
/// Coin<SUI> object to stake
coins: Vec<ObjectID>,
/// stake amount
amount: Option<u64>,
/// the validator's Sui address
validator: SuiAddress,
/// gas object to be used in this transaction, node will pick one from the signer's possession if not provided
gas: Option<ObjectID>,
/// the gas budget, the transaction will fail if the gas cost exceed the budget
gas_budget: u64,
) -> RpcResult<TransactionBlockBytes>;
/// Withdraw stake from a validator's staking pool.
#[method(name = "requestWithdrawStake")]
async fn request_withdraw_stake(
&self,
/// the transaction signer's Sui address
signer: SuiAddress,
/// Delegation object ID
delegation: ObjectID,
/// StakedSui object ID
staked_sui: ObjectID,
/// gas object to be used in this transaction, node will pick one from the signer's possession if not provided
gas: Option<ObjectID>,
/// the gas budget, the transaction will fail if the gas cost exceed the budget
gas_budget: u64,
) -> RpcResult<TransactionBlockBytes>;
Changes to getDelegatedStakes
The getDelegatedStakes
function has been renamed to getStakes
. The getStakes
function returns all of the stakes for an address grouped by validator staking pools, as well as the estimated staking rewards earned so far:
{
"jsonrpc": "2.0",
"result": [
{
"validatorAddress": "0x8760b337dcb641811414daff8f98e6824caf7e5ca28530c4248557057ddc9004",
"stakingPool": "0x628ffd0e51e9a6ea32c13c2739a31a8f344b557d3429e057b377a9c499b9bb13",
"stakes": [
{
"stakedSuiId": "0xa3cc3319d355dc92afee3669cd8f545de98c5ee380b6e1275b891bebdd82ad28",
"stakeRequestEpoch": 3,
"stakeActiveEpoch": 4,
"principal": 99999999998977,
"status": "Active",
"estimatedReward": 998
}
]
},
{
"validatorAddress": "0x6b34f8d6d70676db526017b03ec35f8f74ec67ee10426e2a3049a42045c90913",
"stakingPool": "0xd5d9aa879b78dc1f516d71ab979189086eff752f65e4b0dea15829e3157962e1",
"stakes": [
{
"stakedSuiId": "0x5ee438610276e8fcfe0c0615caf4a2ca7c408569f47c5a622be58863c35b357b",
"stakeRequestEpoch": 2,
"stakeActiveEpoch": 3,
"principal": 100000000000000,
"status": "Active",
"estimatedReward": 1998
},
{
"stakedSuiId": "0x9eac8bd615977f8d635dc2054d13c463829161be41425d701ba8f9a444ca69e9",
"stakeRequestEpoch": 5,
"stakeActiveEpoch": 6,
"principal": 100000000000000,
"status": "Pending"
}
]
}
],
"id": 1
}
Add getStakesByIds function
With the new getStakesByIds
it's possible to query the delegated stakes using a vector of staked SUI IDs. The function returns all of the stakes queried, grouped by validator staking pools, as well as the estimated staking rewards earned so far.
Secp256k1 derive keypair
Match Secp256k1.deriveKeypair
with Ed25519 on a function signature takes in a mnemonics string and an optional path string instead of a required path string and a mnemonics string. See PR 8542 for details.