From 7d1ce8641ecbd17505f31e44b3bd1d6aaacd8dcb Mon Sep 17 00:00:00 2001 From: Sebastian Wolfram Date: Fri, 3 May 2024 17:00:07 +0200 Subject: [PATCH 01/62] add initial sui handling for tests --- Cargo.toml | 1 + identity_sui_name_tbd/Cargo.toml | 26 +++ identity_sui_name_tbd/README.md | 15 ++ .../packages/stardust-test/.gitignore | 1 + .../packages/stardust-test/Move.lock | 28 ++++ .../packages/stardust-test/Move.toml | 12 ++ .../packages/stardust-test/README.md | 10 ++ .../stardust-test/basic_migration_graph.svg | 4 + .../packages/stardust-test/design.md | 26 +++ .../stardust-test/sources/alias/alias.move | 116 ++++++++++++++ .../sources/alias/alias_output.move | 84 ++++++++++ .../sources/basic/basic_output.move | 134 ++++++++++++++++ .../stardust-test/sources/capped_coin.move | 124 ++++++++++++++ .../stardust-test/sources/nft/irc27.move | 151 ++++++++++++++++++ .../stardust-test/sources/nft/nft.move | 148 +++++++++++++++++ .../stardust-test/sources/nft/nft_output.move | 119 ++++++++++++++ .../address_unlock_condition.move | 73 +++++++++ .../expiration_unlock_condition.move | 67 ++++++++ ...orage_deposit_return_unlock_condition.move | 50 ++++++ .../timelock_unlock_condition.move | 38 +++++ .../stardust-test/sources/utilities.move | 45 ++++++ .../stardust-test/tests/alias_tests.move | 106 ++++++++++++ .../stardust-test/tests/basic_tests.move | 110 +++++++++++++ .../tests/capped_coin_tests.move | 134 ++++++++++++++++ .../stardust-test/tests/nft_tests.move | 148 +++++++++++++++++ .../address_unlock_condition_tests.move | 132 +++++++++++++++ .../scripts/create_test_alias_output.sh | 36 +++++ .../scripts/publish_stardust_test.sh | 11 ++ identity_sui_name_tbd/src/error.rs | 23 +++ identity_sui_name_tbd/src/lib.rs | 8 + identity_sui_name_tbd/src/resolution/mod.rs | 6 + .../src/resolution/unmigrated_resolver.rs | 73 +++++++++ identity_sui_name_tbd/src/utils.rs | 16 ++ identity_sui_name_tbd/tests/test.rs | 27 ++++ 34 files changed, 2102 insertions(+) create mode 100644 identity_sui_name_tbd/Cargo.toml create mode 100644 identity_sui_name_tbd/README.md create mode 100644 identity_sui_name_tbd/packages/stardust-test/.gitignore create mode 100644 identity_sui_name_tbd/packages/stardust-test/Move.lock create mode 100644 identity_sui_name_tbd/packages/stardust-test/Move.toml create mode 100644 identity_sui_name_tbd/packages/stardust-test/README.md create mode 100644 identity_sui_name_tbd/packages/stardust-test/basic_migration_graph.svg create mode 100644 identity_sui_name_tbd/packages/stardust-test/design.md create mode 100644 identity_sui_name_tbd/packages/stardust-test/sources/alias/alias.move create mode 100644 identity_sui_name_tbd/packages/stardust-test/sources/alias/alias_output.move create mode 100644 identity_sui_name_tbd/packages/stardust-test/sources/basic/basic_output.move create mode 100644 identity_sui_name_tbd/packages/stardust-test/sources/capped_coin.move create mode 100644 identity_sui_name_tbd/packages/stardust-test/sources/nft/irc27.move create mode 100644 identity_sui_name_tbd/packages/stardust-test/sources/nft/nft.move create mode 100644 identity_sui_name_tbd/packages/stardust-test/sources/nft/nft_output.move create mode 100644 identity_sui_name_tbd/packages/stardust-test/sources/unlock_condition/address_unlock_condition.move create mode 100644 identity_sui_name_tbd/packages/stardust-test/sources/unlock_condition/expiration_unlock_condition.move create mode 100644 identity_sui_name_tbd/packages/stardust-test/sources/unlock_condition/storage_deposit_return_unlock_condition.move create mode 100644 identity_sui_name_tbd/packages/stardust-test/sources/unlock_condition/timelock_unlock_condition.move create mode 100644 identity_sui_name_tbd/packages/stardust-test/sources/utilities.move create mode 100644 identity_sui_name_tbd/packages/stardust-test/tests/alias_tests.move create mode 100644 identity_sui_name_tbd/packages/stardust-test/tests/basic_tests.move create mode 100644 identity_sui_name_tbd/packages/stardust-test/tests/capped_coin_tests.move create mode 100644 identity_sui_name_tbd/packages/stardust-test/tests/nft_tests.move create mode 100644 identity_sui_name_tbd/packages/stardust-test/tests/unlock_condition/address_unlock_condition_tests.move create mode 100755 identity_sui_name_tbd/scripts/create_test_alias_output.sh create mode 100755 identity_sui_name_tbd/scripts/publish_stardust_test.sh create mode 100644 identity_sui_name_tbd/src/error.rs create mode 100644 identity_sui_name_tbd/src/lib.rs create mode 100644 identity_sui_name_tbd/src/resolution/mod.rs create mode 100644 identity_sui_name_tbd/src/resolution/unmigrated_resolver.rs create mode 100644 identity_sui_name_tbd/src/utils.rs create mode 100644 identity_sui_name_tbd/tests/test.rs diff --git a/Cargo.toml b/Cargo.toml index 0799f08ca1..7524e8fa54 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,6 +13,7 @@ members = [ "identity_stronghold", "identity_jose", "identity_eddsa_verifier", + "identity_sui_name_tbd", "examples", ] diff --git a/identity_sui_name_tbd/Cargo.toml b/identity_sui_name_tbd/Cargo.toml new file mode 100644 index 0000000000..9d5a205456 --- /dev/null +++ b/identity_sui_name_tbd/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "identity_sui_name_tbd" +version = "0.0.1" +authors.workspace = true +edition.workspace = true +homepage.workspace = true +keywords = ["iota", "identity", "sui"] +license.workspace = true +readme = "./README.md" +repository.workspace = true +rust-version.workspace = true +description = "SUI related tooling for identity-rs" + +[dependencies] +serde.workspace = true +serde_json.workspace = true +strum.workspace = true +sui-sdk = { git = "https://github.com/mystenlabs/sui", package = "sui-sdk"} +thiserror.workspace = true + +[dev-dependencies] +anyhow = "1.0.75" +tokio = { version = "1.29.0", default-features = false, features = ["macros", "sync", "rt"] } + +[lints] +workspace = true diff --git a/identity_sui_name_tbd/README.md b/identity_sui_name_tbd/README.md new file mode 100644 index 0000000000..c02d210a18 --- /dev/null +++ b/identity_sui_name_tbd/README.md @@ -0,0 +1,15 @@ +IOTA Identity - SUI tooling +=== + +## To clarify + +### DID resolving part + +- [ ] use as handler for actual `identity_resolver`? +- [ ] use builder pattern for initialization? + - would wrap up optional params (e.g. for network and stardust package ID) more easily +- [ ] maybe move util functions, network into helper / client struct + +### Signing of transactions + +- [ ] does everyone build their own \ No newline at end of file diff --git a/identity_sui_name_tbd/packages/stardust-test/.gitignore b/identity_sui_name_tbd/packages/stardust-test/.gitignore new file mode 100644 index 0000000000..c795b054e5 --- /dev/null +++ b/identity_sui_name_tbd/packages/stardust-test/.gitignore @@ -0,0 +1 @@ +build \ No newline at end of file diff --git a/identity_sui_name_tbd/packages/stardust-test/Move.lock b/identity_sui_name_tbd/packages/stardust-test/Move.lock new file mode 100644 index 0000000000..2acba99761 --- /dev/null +++ b/identity_sui_name_tbd/packages/stardust-test/Move.lock @@ -0,0 +1,28 @@ +# @generated by Move, please check-in and do not edit manually. + +[move] +version = 0 +manifest_digest = "76F60CE48787CB754F6A10443A5C5FFA5E4C07364A3E6CAEDECFA81065621CEE" +deps_digest = "3C4103934B1E040BB6B23F1D610B4EF9F2F1166A50A104EADCF77467C004C600" + +dependencies = [ + { name = "MoveStdlib" }, + { name = "Sui" }, +] + +[[move.package]] +name = "MoveStdlib" +source = { git = "https://github.com/iotaledger/kinesis.git", rev = "7899dc9ce682c3d0a97f249ce7eaa27b9473b920", subdir = "crates/sui-framework/packages/move-stdlib" } + +[[move.package]] +name = "Sui" +source = { git = "https://github.com/iotaledger/kinesis.git", rev = "7899dc9ce682c3d0a97f249ce7eaa27b9473b920", subdir = "crates/sui-framework/packages/sui-framework" } + +dependencies = [ + { name = "MoveStdlib" }, +] + +[move.toolchain-version] +compiler-version = "1.24.0" +edition = "2024.beta" +flavor = "sui" diff --git a/identity_sui_name_tbd/packages/stardust-test/Move.toml b/identity_sui_name_tbd/packages/stardust-test/Move.toml new file mode 100644 index 0000000000..c88f31fae0 --- /dev/null +++ b/identity_sui_name_tbd/packages/stardust-test/Move.toml @@ -0,0 +1,12 @@ +[package] +name = "Stardust Test" +version = "0.0.1" +edition = "2024.beta" + +[dependencies] +# as we don't have the tag, reference to the respective commit hash +MoveStdlib = { git = "https://github.com/iotaledger/kinesis.git", subdir = "crates/sui-framework/packages/move-stdlib", rev = "7899dc9ce682c3d0a97f249ce7eaa27b9473b920" } +Sui = { git = "https://github.com/iotaledger/kinesis.git", subdir = "crates/sui-framework/packages/sui-framework", rev = "7899dc9ce682c3d0a97f249ce7eaa27b9473b920" } + +[addresses] +stardust = "0x0" diff --git a/identity_sui_name_tbd/packages/stardust-test/README.md b/identity_sui_name_tbd/packages/stardust-test/README.md new file mode 100644 index 0000000000..7b58f9508b --- /dev/null +++ b/identity_sui_name_tbd/packages/stardust-test/README.md @@ -0,0 +1,10 @@ +# stardust test package + +This package is a copy of the in development [stardust package](https://github.com/iotaledger/kinesis/tree/develop/crates/sui-framework/packages/stardust) from [a commit]([7899dc9ce682c3d0a97f249ce7eaa27b9473b920](https://github.com/iotaledger/kinesis/commit/7899dc9ce682c3d0a97f249ce7eaa27b9473b920)) from Wed May 1 12:52:25 2024 +0000. + +The changes introduced in the local copy are for testing purposes only. Depending on how we will be able to access test data, we might be able to drop this folder in the future. + +The local changes are: + +- remove `#[test_only]` from `create_for_testing` in `./sources/alias/alias.move` to be able to create test data +- update dependencies `MoveStdlib` and `Sui` to use kinesis project version (same commit hash as mentioned above) \ No newline at end of file diff --git a/identity_sui_name_tbd/packages/stardust-test/basic_migration_graph.svg b/identity_sui_name_tbd/packages/stardust-test/basic_migration_graph.svg new file mode 100644 index 0000000000..7e9d71dcb1 --- /dev/null +++ b/identity_sui_name_tbd/packages/stardust-test/basic_migration_graph.svg @@ -0,0 +1,4 @@ + + + +
Basic Output
Expiration?
Native Tokens?
Native Tokens?
SDRUC?
SDRUC?
SDRUC?
SDRUC?
Timelock?
Timelock?
Timelock?
Timelock?
Timelock?
Timelock?
Timelock?
Timelock?
Yes
Yes
Yes
Yes
Yes
Yes
Yes
No
No
No
No
No
No
No
Owned stardust::basic::BasicOutput
O
I
T
Owned 0x2::coin::Coin<IOTA>
O
I
Owned stardust::basic::BasicOutput
SDR
O
I
Owned stardust::basic::BasicOutput
SDR
O
I
T
Shared stardust::basic::BasicOutput
NT
SDR
E
I
Shared stardust::basic::BasicOutput
NT
E
I
T
Shared stardust::basic::BasicOutput
NT
E
I
Shared stardust::basic::BasicOutput
SDR
E
I
T
Shared stardust::basic::BasicOutput
SDR
E
I
Shared stardust::basic::BasicOutput
E
I
T
Shared stardust::basic::BasicOutput
E
I
Owned stardust::basic::BasicOutput
NT
SDR
O
I
T
Owned stardust::basic::BasicOutput
NT
SDR
O
I
Owned stardust::basic::BasicOutput
NT
O
I
T
Owned 0x2::coin::Coin<IOTA>
O
I
Yes
No
Yes
Yes
Yes
Yes
Yes
Yes
Yes
No
No
No
No
No
+
No
No
tokens: Bag<String, Balance<TokenType>
sdruc: Option<StorageDepositReturn>::Some(
StorageDepositReturn {
return_address: <RETURN ADDRESS>,
return_amount: <RETURN_AMOUNT>
}
)
timelock: Option<timelock>::Some(
Timelock {
unix_time: <UNIX_TIME>
}
)
expiration: Option<Expiration>::Some(
Expiration {
owner: <ADDRESS-UNLOCK>,
return_address: <RETURN ADDRESS>,
unix_time: <UNIX_TIME>
}
)
owner(obj metadata): hash(<ADDRESS UNLOCK>)
balance: Balance<IOTA>
O
NT
SDR
E
I
T
Shared stardust::basic::BasicOutput
NT
SDR
E
I
T
Owned 0x2::coin::Coin<TokenType>
O
NT
balance: Balance<TokenType>
NT
Move Field Types
\ No newline at end of file diff --git a/identity_sui_name_tbd/packages/stardust-test/design.md b/identity_sui_name_tbd/packages/stardust-test/design.md new file mode 100644 index 0000000000..8605b793a9 --- /dev/null +++ b/identity_sui_name_tbd/packages/stardust-test/design.md @@ -0,0 +1,26 @@ +# Stardust on Move + +## Migrating Basic Outputs + +Every Basic Output has an `Address Unlock` and an `IOTA` balance (u64). Depending on what other fields we have, we will create different objects. + +[Decision graph on what to do with a basic output during migration](./basic_migration_graph.svg) + +![](./basic_migration_graph.svg) + +## User Flow + +Majority of user funds are sitting in Basic Outputs without unlock conditions. Such tokens will be migrated to `0x2::coin::Coin` which one can directly use as a gas payment object. +If a user does not end up with such coin objects at migration, we will have to sponsor their transaction to extract assets. + +- We can directly ask back the gas fee from the migrated object +- Take a look at the `test` function inside [`stardust::basic_output`](./sources/basic/basic_output.move) on how to construct a PTB for a user to claim all assets and fuflill unlock conditions. + +## Alias Object + +- Alias ID must be kept between the migration from Stardust to Move (for applications like Identity). During the migration, any Alias Output with a zeroed ID must have its corresponding computed Alias ID set. +- The Foundry Counter in Stardust is used to give foundries a unique ID. The Foundry ID is the concatenation of `Address || Serial Number || Token Scheme Type`. In Move the foundries are represented by unique packages that define the corresponding Coin Type (a one time witness) of the Native Token. Because the foundry counter can no longer be enforced to be incremented when a new package is deployed, which defines a native token and is owned by that Alias, the Foundry Counter becomes meaningless. Hence, we should remove it. The same count can be determined (off-chain) by counting the number of `TreasuryCap`s the Alias owns. +- State Controller is represented as a `StateCap` that can be updated by the governor. This happens by increasing the `StateCap` version. +- The Governor Capability contains the ID of the alias which it controls. This is needed such that not _any_ `GovernorCap` can be used to update _any_ Alias. +- No way for user to create a new Alias in Move by not providing a constructor. These are only created during the migration. +- We would most likely want to receive Alias, Basic and NFT Output objects or Treasury Caps objects and return them in a receiving function (`unlock_alias_address_owned_output`), so we do not have to store them in the Alias itself. Then they can be transferred somewhere else in the calling PTB. diff --git a/identity_sui_name_tbd/packages/stardust-test/sources/alias/alias.move b/identity_sui_name_tbd/packages/stardust-test/sources/alias/alias.move new file mode 100644 index 0000000000..79c85428fe --- /dev/null +++ b/identity_sui_name_tbd/packages/stardust-test/sources/alias/alias.move @@ -0,0 +1,116 @@ +module stardust::alias { + + /// The persisted Alias object from Stardust, without tokens and assets. + /// Outputs owned the AliasID/Address in Stardust will be sent to this object and + /// have to be received via this object once extracted from `AliasOutput`. + public struct Alias has key, store { + /// The ID of the Alias = hash of the Output ID that created the Alias Output in Stardust. + /// This is the AliasID from Stardust. + id: UID, + + /// The last State Controller address assigned before the migration. + legacy_state_controller: Option
, + /// A counter increased by 1 every time the alias was state transitioned. + state_index: u32, + /// State metadata that can be used to store additional information. + state_metadata: Option>, + + /// The sender feature. + sender: Option
, + /// The metadata feature. + metadata: Option>, + + /// The immutable issuer feature. + immutable_issuer: Option
, + /// The immutable metadata feature. + immutable_metadata: Option>, + } + + // === Public-Mutative Functions === + + /// Destroy the `Alias` object, equivalent to `burning` an Alias Output in Stardust. + public fun destroy(self: Alias) { + let Alias { + id, + legacy_state_controller: _, + state_index: _, + state_metadata: _, + sender: _, + metadata: _, + immutable_issuer: _, + immutable_metadata: _, + } = self; + + object::delete(id); + } + + // === Public-Mutative Functions === + + /// Get the Alias's `legacy_state_controller`. + public fun legacy_state_controller(self: &Alias): &Option
{ + &self.legacy_state_controller + } + + /// Get the Alias's `state_index`. + public fun state_index(self: &Alias): u32 { + self.state_index + } + + /// Get the Alias's `state_metadata`. + public fun state_metadata(self: &Alias): &Option> { + &self.state_metadata + } + + /// Get the Alias's `sender`. + public fun sender(self: &Alias): &Option
{ + &self.sender + } + + /// Get the Alias's `metadata`. + public fun metadata(self: &Alias): &Option> { + &self.metadata + } + + /// Get the Alias's `immutable_sender`. + public fun immutable_issuer(self: &Alias): &Option
{ + &self.immutable_issuer + } + + /// Get the Alias's `immutable_metadata`. + public fun immutable_metadata(self: &Alias): &Option> { + &self.immutable_metadata + } + + // === Public-Package Functions === + + /// Get the Alias's id. + public(package) fun id(self: &mut Alias): &mut UID { + &mut self.id + } + + // === Test Functions === + + // #[test_only] + public fun create_for_testing( + legacy_state_controller: Option
, + state_index: u32, + state_metadata: Option>, + sender: Option
, + metadata: Option>, + immutable_issuer: Option
, + immutable_metadata: Option>, + ctx: &mut TxContext + ) { + let alias = Alias { + id: object::new(ctx), + legacy_state_controller, + state_index, + state_metadata, + sender, + metadata, + immutable_issuer, + immutable_metadata, + }; + transfer::transfer(alias, ctx.sender()); + } +} diff --git a/identity_sui_name_tbd/packages/stardust-test/sources/alias/alias_output.move b/identity_sui_name_tbd/packages/stardust-test/sources/alias/alias_output.move new file mode 100644 index 0000000000..d7ab2b26bb --- /dev/null +++ b/identity_sui_name_tbd/packages/stardust-test/sources/alias/alias_output.move @@ -0,0 +1,84 @@ +module stardust::alias_output { + + use sui::bag::Bag; + use sui::balance::Balance; + use sui::dynamic_object_field; + use sui::sui::SUI; + use sui::transfer::Receiving; + + use stardust::alias::Alias; + + /// The Alias dynamic object field name. + const ALIAS_NAME: vector = b"alias"; + + /// Owned Object controlled by the Governor Address. + public struct AliasOutput has key { + /// This is a "random" UID, not the AliasID from Stardust. + id: UID, + + /// The amount of IOTA coins held by the output. + iota: Balance, + + /// The `Bag` holds native tokens, key-ed by the stringified type of the asset. + /// Example: key: "0xabcded::soon::SOON", value: Balance<0xabcded::soon::SOON>. + native_tokens: Bag, + } + + // === Public-Mutative Functions === + + /// The function extracts assets from a legacy `AliasOutput`. + /// - returns the IOTA Balance, + /// - the native tokens Bag, + /// - and the `Alias` object that persists the AliasID=ObjectID from Stardust. + public fun extract_assets(mut output: AliasOutput): (Balance, Bag, Alias) { + // Load the related alias object. + let alias = load_alias(&mut output); + + // Unpack the output into its basic part. + let AliasOutput { + id, + iota, + native_tokens + } = output; + + // Delete the output. + object::delete(id); + + (iota, native_tokens, alias) + } + + // === Public-Package Functions === + + /// Utility function to receive an `AliasOutput` object in other Stardust modules. + /// Other modules in the Stardust package can call this function to receive an `AliasOutput` object (nft). + public(package) fun receive(parent: &mut UID, output: Receiving) : AliasOutput { + transfer::receive(parent, output) + } + + // === Private Functions === + + /// Loads the `Alias` object from the dynamic object field. + fun load_alias(output: &mut AliasOutput): Alias { + dynamic_object_field::remove(&mut output.id, ALIAS_NAME) + } + + // === Test Functions === + + #[test_only] + public fun create_for_testing( + iota: Balance, + native_tokens: Bag, + ctx: &mut TxContext + ): AliasOutput { + AliasOutput { + id: object::new(ctx), + iota, + native_tokens, + } + } + + #[test_only] + public fun attach_alias(output: &mut AliasOutput, alias: Alias) { + dynamic_object_field::add(&mut output.id, ALIAS_NAME, alias) + } +} diff --git a/identity_sui_name_tbd/packages/stardust-test/sources/basic/basic_output.move b/identity_sui_name_tbd/packages/stardust-test/sources/basic/basic_output.move new file mode 100644 index 0000000000..d01b8029c3 --- /dev/null +++ b/identity_sui_name_tbd/packages/stardust-test/sources/basic/basic_output.move @@ -0,0 +1,134 @@ +module stardust::basic_output { + // === Imports === + + // Sui imports. + use sui::bag::Bag; + use sui::balance::Balance; + use sui::sui::SUI; + use sui::transfer::Receiving; + + // Package imports. + use stardust::expiration_unlock_condition::ExpirationUnlockCondition; + use stardust::storage_deposit_return_unlock_condition::StorageDepositReturnUnlockCondition; + use stardust::timelock_unlock_condition::TimelockUnlockCondition; + + // === Structs === + + /// A basic output that has unlock conditions/features. + /// - basic outputs with expiration unlock condition must be a shared object, since that's the only + /// way to handle the two possible addresses that can unlock the output. + /// - notice that there is no `store` ability and there is no custom transfer function: + /// - you can call `extract_assets`, + /// - or you can call `receive` in other models to receive a `BasicOutput`. + public struct BasicOutput has key { + /// Hash of the `outputId` that was migrated. + id: UID, + + /// The amount of IOTA coins held by the output. + iota: Balance, + + /// The `Bag` holds native tokens, key-ed by the stringified type of the asset. + /// Example: key: "0xabcded::soon::SOON", value: Balance<0xabcded::soon::SOON>. + native_tokens: Bag, + + /// The storage deposit return unlock condition. + storage_deposit_return: Option, + /// The timelock unlock condition. + timelock: Option, + /// The expiration unlock condition. + expiration: Option, + + // Possible features, they have no effect and only here to hold data until the object is deleted. + + /// The metadata feature. + metadata: Option>, + /// The tag feature. + tag: Option>, + /// The sender feature. + sender: Option
+ } + + // === Public-Mutative Functions === + + /// Extract the assets stored inside the output, respecting the unlock conditions. + /// - The object will be deleted. + /// - The `StorageDepositReturnUnlockCondition` will return the deposit. + /// - Remaining assets (IOTA coins and native tokens) will be returned. + public fun extract_assets(output: BasicOutput, ctx: &mut TxContext) : (Balance, Bag) { + // Unpack the output into its basic part. + let BasicOutput { + id, + iota: mut iota, + native_tokens, + storage_deposit_return: mut storage_deposit_return, + timelock: mut timelock, + expiration: mut expiration, + sender: _, + metadata: _, + tag: _ + } = output; + + // If the output has a timelock, then we need to check if the timelock has expired. + if (timelock.is_some()) { + timelock.extract().unlock(ctx); + }; + + // If the output has an expiration, then we need to check who can unlock the output. + if (expiration.is_some()) { + expiration.extract().unlock(ctx); + }; + + // If the output has an storage deposit return, then we need to return the deposit. + if (storage_deposit_return.is_some()) { + storage_deposit_return.extract().unlock(&mut iota, ctx); + }; + + // Destroy the unlock conditions. + option::destroy_none(timelock); + option::destroy_none(expiration); + option::destroy_none(storage_deposit_return); + + // Delete the output. + object::delete(id); + + return (iota, native_tokens) + } + + // === Public-Package Functions === + + /// Utility function to receive a basic output in other stardust modules. + /// Since `BasicOutput` only has `key`, it can not be received via `public_receive`. + /// The private receiver must be implemented in its defining module (here). + /// Other modules in the Stardust package can call this function to receive a basic output (alias, NFT). + public(package) fun receive(parent: &mut UID, output: Receiving) : BasicOutput { + transfer::receive(parent, output) + } + + // === Test Functions === + + // test only function to create a basic output + #[test_only] + public fun create_for_testing( + iota: Balance, + native_tokens: Bag, + storage_deposit_return: Option, + timelock: Option, + expiration: Option, + metadata: Option>, + tag: Option>, + sender: Option
, + ctx: &mut TxContext + ): BasicOutput { + BasicOutput { + id: object::new(ctx), + iota, + native_tokens, + storage_deposit_return, + timelock, + expiration, + metadata, + tag, + sender + } + } +} diff --git a/identity_sui_name_tbd/packages/stardust-test/sources/capped_coin.move b/identity_sui_name_tbd/packages/stardust-test/sources/capped_coin.move new file mode 100644 index 0000000000..7b2df72d36 --- /dev/null +++ b/identity_sui_name_tbd/packages/stardust-test/sources/capped_coin.move @@ -0,0 +1,124 @@ +// Copyright (c) 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +module stardust::capped_coin { + + use std::ascii; + use std::string; + + use sui::balance::{Supply, Balance}; + use sui::coin::{Self, Coin, TreasuryCap, CoinMetadata}; + + /// The error returned when the maximum supply reached. + const EMaximumSupplyReached: u64 = 0; + + /// The policy wrapper that ensures the supply of a `Coin` never exceeds the maximum supply. + public struct MaxSupplyPolicy has key, store { + id: UID, + + /// The maximum supply. + maximum_supply: u64, + + /// The wrapped `TreasuryCap`. + treasury_cap: TreasuryCap, + } + + /// Wrap a `TreasuryCap` into a `MaxSupplyPolicy` to prevent minting of tokens > max supply. + /// Be careful, once you add a maximum supply you will not be able to change it or get rid of it! + /// This gives coin holders a guarantee that the maximum supply of that specific coin will never change. + public fun create_max_supply_policy( + treasury_cap: TreasuryCap, + maximum_supply: u64, + ctx: &mut TxContext + ): MaxSupplyPolicy { + MaxSupplyPolicy { + id: object::new(ctx), + maximum_supply, + treasury_cap + } + } + + /// Return the total number of `T`'s in circulation. + public fun total_supply(policy: &MaxSupplyPolicy): u64 { + coin::total_supply(&policy.treasury_cap) + } + + /// Get immutable reference to the treasury's `Supply`. + public fun supply_immut(policy: &MaxSupplyPolicy): &Supply { + coin::supply_immut(&policy.treasury_cap) + } + + /// Create a `Coin` worth `value` and increase the total supply in `cap` accordingly. + public fun mint( + policy: &mut MaxSupplyPolicy, + value: u64, + ctx: &mut TxContext + ): Coin { + assert!(total_supply(policy) + value <= policy.maximum_supply, EMaximumSupplyReached); + coin::mint(&mut policy.treasury_cap, value, ctx) + } + + /// Mint some amount of `T` as a `Balance` and increase the total supply in `cap` accordingly. + /// Aborts if `value` + `cap.total_supply` >= `U64_MAX`. + public fun mint_balance( + policy: &mut MaxSupplyPolicy, + value: u64 + ): Balance { + assert!(total_supply(policy) + value <= policy.maximum_supply, EMaximumSupplyReached); + coin::mint_balance(&mut policy.treasury_cap, value) + } + + /// Destroy the coin `c` and decrease the total supply in `cap` accordingly. + public entry fun burn(policy: &mut MaxSupplyPolicy, c: Coin): u64 { + coin::burn(&mut policy.treasury_cap, c) + } + + /// Mint `amount` of `Coin` and send it to the `recipient`. Invokes `mint()`. + public fun mint_and_transfer( + policy: &mut MaxSupplyPolicy, + amount: u64, + recipient: address, + ctx: &mut TxContext + ) { + assert!(total_supply(policy) + amount <= policy.maximum_supply, EMaximumSupplyReached); + coin::mint_and_transfer(&mut policy.treasury_cap, amount, recipient, ctx) + } + + // === Update coin metadata === + + /// Update the `name` of the coin in the `CoinMetadata`. + public fun update_name( + policy: &mut MaxSupplyPolicy, + metadata: &mut CoinMetadata, + name: string::String + ) { + coin::update_name(&policy.treasury_cap, metadata, name) + } + + /// Update the `symbol` of the coin in the `CoinMetadata`. + public fun update_symbol( + policy: &mut MaxSupplyPolicy, + metadata: &mut CoinMetadata, + symbol: ascii::String + ) { + coin::update_symbol(&policy.treasury_cap, metadata, symbol) + } + + /// Update the `description` of the coin in the `CoinMetadata`. + public fun update_description( + policy: &mut MaxSupplyPolicy, + metadata: &mut CoinMetadata, + description: string::String + ) { + coin::update_description(&policy.treasury_cap, metadata, description) + } + + /// Update the `url` of the coin in the `CoinMetadata` + public fun update_icon_url( + policy: &mut MaxSupplyPolicy, + metadata: &mut CoinMetadata, + url: ascii::String + ) { + coin::update_icon_url(&policy.treasury_cap, metadata, url) + } +} diff --git a/identity_sui_name_tbd/packages/stardust-test/sources/nft/irc27.move b/identity_sui_name_tbd/packages/stardust-test/sources/nft/irc27.move new file mode 100644 index 0000000000..0a5b35ea3b --- /dev/null +++ b/identity_sui_name_tbd/packages/stardust-test/sources/nft/irc27.move @@ -0,0 +1,151 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +module stardust::irc27 { + + use std::fixed_point32::FixedPoint32; + use std::string::String; + + use sui::table::Table; + use sui::url::Url; + use sui::vec_set::VecSet; + + /// The IRC27 NFT metadata standard schema. + public struct Irc27Metadata has store { + /// Version of the metadata standard. + version: String, + + /// The media type (MIME) of the asset. + /// + /// ## Examples + /// - Image files: `image/jpeg`, `image/png`, `image/gif`, etc. + /// - Video files: `video/x-msvideo` (avi), `video/mp4`, `video/mpeg`, etc. + /// - Audio files: `audio/mpeg`, `audio/wav`, etc. + /// - 3D Assets: `model/obj`, `model/u3d`, etc. + /// - Documents: `application/pdf`, `text/plain`, etc. + media_type: String, + + /// URL pointing to the NFT file location. + uri: Url, + + /// The human-readable name of the native token. + name: String, + + /// The human-readable collection name of the native token. + collection_name: Option, + + /// Royalty payment addresses mapped to the payout percentage. + /// Contains a hash of the 32 bytes parsed from the BECH32 encoded IOTA address in the metadata, it is a legacy address. + /// Royalties are not supported by the protocol and needed to be processed by an integrator. + royalties: Table, + + /// The human-readable name of the native token creator. + issuer_name: Option, + + /// The human-readable description of the token. + description: Option, + + /// Additional attributes which follow [OpenSea Metadata standards](https://docs.opensea.io/docs/metadata-standards). + attributes: VecSet, + + /// Legacy non-standard metadata fields. + non_standard_fields: Table, + } + + /// Permanently destroy a `Irc27Metadata` object. + public fun destroy(irc27: Irc27Metadata) { + let Irc27Metadata { + version: _, + media_type: _, + uri: _, + name: _, + collection_name: _, + royalties, + issuer_name: _, + description: _, + attributes: _, + non_standard_fields, + } = irc27; + + royalties.drop(); + + non_standard_fields.drop(); + } + + /// Get the metadata's `version`. + public fun version(irc27: &Irc27Metadata): &String { + &irc27.version + } + + /// Get the metadata's `media_type`. + public fun media_type(irc27: &Irc27Metadata): &String { + &irc27.media_type + } + + /// Get the metadata's `uri`. + public fun uri(irc27: &Irc27Metadata): &Url { + &irc27.uri + } + + /// Get the metadata's `name`. + public fun name(irc27: &Irc27Metadata): &String { + &irc27.name + } + + /// Get the metadata's `collection_name`. + public fun collection_name(irc27: &Irc27Metadata): &Option { + &irc27.collection_name + } + + /// Get the metadata's `royalties`. + public fun royalties(irc27: &Irc27Metadata): &Table { + &irc27.royalties + } + + /// Get the metadata's `issuer_name`. + public fun issuer_name(irc27: &Irc27Metadata): &Option { + &irc27.issuer_name + } + + /// Get the metadata's `description`. + public fun description(irc27: &Irc27Metadata): &Option { + &irc27.description + } + + /// Get the metadata's `attributes`. + public fun attributes(irc27: &Irc27Metadata): &VecSet { + &irc27.attributes + } + + /// Get the metadata's `non_standard_fields`. + public fun non_standard_fields(irc27: &Irc27Metadata): &Table { + &irc27.non_standard_fields + } + + #[test_only] + public fun create_for_testing( + version: String, + media_type: String, + uri: Url, + name: String, + collection_name: Option, + royalties: Table, + issuer_name: Option, + description: Option, + attributes: VecSet, + non_standard_fields: Table, + ): Irc27Metadata { + Irc27Metadata { + version, + media_type, + uri, + name, + collection_name, + royalties, + issuer_name, + description, + attributes, + non_standard_fields + } + } +} diff --git a/identity_sui_name_tbd/packages/stardust-test/sources/nft/nft.move b/identity_sui_name_tbd/packages/stardust-test/sources/nft/nft.move new file mode 100644 index 0000000000..d5c17921d5 --- /dev/null +++ b/identity_sui_name_tbd/packages/stardust-test/sources/nft/nft.move @@ -0,0 +1,148 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +module stardust::nft { + + use std::string; + + use sui::display; + use sui::package; + + use stardust::irc27::{Self, Irc27Metadata}; + + /// One Time Witness. + public struct NFT has drop {} + + /// The Stardust NFT representation. + public struct Nft has key, store { + /// The Nft's ID is nested from Stardust. + id: UID, + + /// The sender feature holds the last sender address assigned before the migration and + /// is not supported by the protocol after it. + legacy_sender: Option
, + /// The metadata feature. + metadata: Option>, + /// The tag feature. + tag: Option>, + + /// The immutable issuer feature. + immutable_issuer: Option
, + /// The immutable metadata feature. + immutable_metadata: Irc27Metadata, + } + + /// The `Nft` module initializer. + fun init(otw: NFT, ctx: &mut TxContext) { + // Claim the module publisher. + let publisher = package::claim(otw, ctx); + + // Build a `Display` object. + let keys = vector[ + // The Sui standard fields. + string::utf8(b"name"), + string::utf8(b"image_url"), + string::utf8(b"description"), + string::utf8(b"creator"), + + // The extra IRC27-nested fileds. + string::utf8(b"version"), + string::utf8(b"media_type"), + string::utf8(b"collection_name"), + ]; + + let values = vector[ + // The Sui standard fields. + string::utf8(b"{immutable_metadata.name}"), + string::utf8(b"{immutable_metadata.uri}"), + string::utf8(b"{immutable_metadata.description}"), + string::utf8(b"{immutable_metadata.issuer_name}"), + + // The extra IRC27-nested fileds. + string::utf8(b"{immutable_metadata.version}"), + string::utf8(b"{immutable_metadata.media_type}"), + string::utf8(b"{immutable_metadata.collection_name}"), + ]; + + let mut display = display::new_with_fields( + &publisher, + keys, + values, + ctx + ); + + // Commit the first version of `Display` to apply changes. + display.update_version(); + + // Burn the publisher object. + package::burn_publisher(publisher); + + // Freeze the display object. + sui::transfer::public_freeze_object(display); + } + + /// Permanently destroy an `Nft` object. + public fun destroy(nft: Nft) { + let Nft { + id, + legacy_sender: _, + metadata: _, + tag: _, + immutable_issuer: _, + immutable_metadata, + } = nft; + + irc27::destroy(immutable_metadata); + + object::delete(id); + } + + /// Get the Nft's `legacy_sender`. + public fun legacy_sender(nft: &Nft): &Option
{ + &nft.legacy_sender + } + + /// Get the Nft's `metadata`. + public fun metadata(nft: &Nft): &Option> { + &nft.metadata + } + + /// Get the Nft's `tag`. + public fun tag(nft: &Nft): &Option> { + &nft.tag + } + + /// Get the Nft's `immutable_sender`. + public fun immutable_issuer(nft: &Nft): &Option
{ + &nft.immutable_issuer + } + + /// Get the Nft's `immutable_metadata`. + public fun immutable_metadata(nft: &Nft): &Irc27Metadata { + &nft.immutable_metadata + } + + /// Get the Nft's id. + public(package) fun id(self: &mut Nft): &mut UID { + &mut self.id + } + + #[test_only] + public fun create_for_testing( + legacy_sender: Option
, + metadata: Option>, + tag: Option>, + immutable_issuer: Option
, + immutable_metadata: Irc27Metadata, + ctx: &mut TxContext, + ): Nft { + Nft { + id: object::new(ctx), + legacy_sender, + metadata, + tag, + immutable_issuer, + immutable_metadata, + } + } +} diff --git a/identity_sui_name_tbd/packages/stardust-test/sources/nft/nft_output.move b/identity_sui_name_tbd/packages/stardust-test/sources/nft/nft_output.move new file mode 100644 index 0000000000..395fed6e94 --- /dev/null +++ b/identity_sui_name_tbd/packages/stardust-test/sources/nft/nft_output.move @@ -0,0 +1,119 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +module stardust::nft_output { + + use sui::bag::Bag; + use sui::balance::Balance; + use sui::dynamic_object_field; + use sui::sui::SUI; + use sui::transfer::Receiving; + + use stardust::nft::Nft; + + use stardust::expiration_unlock_condition::ExpirationUnlockCondition; + use stardust::storage_deposit_return_unlock_condition::StorageDepositReturnUnlockCondition; + use stardust::timelock_unlock_condition::TimelockUnlockCondition; + + /// The NFT dynamic field name. + const NFT_NAME: vector = b"nft"; + + /// The Stardust NFT output representation. + public struct NftOutput has key { + /// This is a "random" UID, not the NFTID from Stardust. + id: UID, + + /// The amount of IOTA tokens held by the output. + iota: Balance, + + /// The `Bag` holds native tokens, key-ed by the stringified type of the asset. + /// Example: key: "0xabcded::soon::SOON", value: Balance<0xabcded::soon::SOON>. + native_tokens: Bag, + + /// The storage deposit return unlock condition. + storage_deposit_return: Option, + /// The timelock unlock condition. + timelock: Option, + /// The expiration unlock condition. + expiration: Option, + } + + /// The function extracts assets from a legacy NFT output. + public fun extract_assets(mut output: NftOutput, ctx: &mut TxContext): (Balance, Bag, Nft) { + // Load the related Nft object. + let nft = load_nft(&mut output); + + // Unpuck the output. + let NftOutput { + id, + iota: mut iota, + native_tokens, + storage_deposit_return: mut storage_deposit_return, + timelock: mut timelock, + expiration: mut expiration + } = output; + + // If the output has a timelock, then we need to check if the timelock has expired. + if (timelock.is_some()) { + timelock.extract().unlock(ctx); + }; + + // If the output has an expiration, then we need to check who can unlock the output. + if (expiration.is_some()) { + expiration.extract().unlock(ctx); + }; + + // If the output has an SDRUC, then we need to return the deposit. + if (storage_deposit_return.is_some()) { + storage_deposit_return.extract().unlock(&mut iota, ctx); + }; + + // Destroy the output. + option::destroy_none(timelock); + option::destroy_none(expiration); + option::destroy_none(storage_deposit_return); + + object::delete(id); + + return (iota, native_tokens, nft) + } + + /// Loads the related `Nft` object. + fun load_nft(output: &mut NftOutput): Nft { + dynamic_object_field::remove(&mut output.id, NFT_NAME) + } + + // === Public-Package Functions === + + /// Utility function to receive an `NftOutput` in other Stardust modules. + /// Other modules in the stardust package can call this function to receive an `NftOutput` (alias). + public(package) fun receive(parent: &mut UID, nft: Receiving) : NftOutput { + transfer::receive(parent, nft) + } + + // === Test Functions === + + #[test_only] + public fun attach_nft(output: &mut NftOutput, nft: Nft) { + dynamic_object_field::add(&mut output.id, NFT_NAME, nft) + } + + #[test_only] + public fun create_for_testing( + iota: Balance, + native_tokens: Bag, + storage_deposit_return: Option, + timelock: Option, + expiration: Option, + ctx: &mut TxContext, + ): NftOutput { + NftOutput { + id: object::new(ctx), + iota, + native_tokens, + storage_deposit_return, + timelock, + expiration, + } + } +} diff --git a/identity_sui_name_tbd/packages/stardust-test/sources/unlock_condition/address_unlock_condition.move b/identity_sui_name_tbd/packages/stardust-test/sources/unlock_condition/address_unlock_condition.move new file mode 100644 index 0000000000..ba70b83252 --- /dev/null +++ b/identity_sui_name_tbd/packages/stardust-test/sources/unlock_condition/address_unlock_condition.move @@ -0,0 +1,73 @@ +module stardust::address_unlock_condition { + + use sui::coin::TreasuryCap; + use sui::transfer::Receiving; + + use stardust::alias::Alias; + use stardust::alias_output::{Self, AliasOutput}; + use stardust::basic_output::{Self, BasicOutput}; + use stardust::nft::Nft; + use stardust::nft_output::{Self, NftOutput}; + + // === Receiving on Alias Address/AliasID as ObjectID === + + /// Unlock a `BasicOutput` locked to the alias address. + public fun unlock_alias_address_owned_basic( + self: &mut Alias, + output_to_unlock: Receiving + ): BasicOutput { + basic_output::receive(self.id(), output_to_unlock) + } + + /// Unlock an `NftOutput` locked to the alias address. + public fun unlock_alias_address_owned_nft( + self: &mut Alias, + output_to_unlock: Receiving, + ): NftOutput { + nft_output::receive(self.id(), output_to_unlock) + } + + /// Unlock an `AliasOutput` locked to the alias address. + public fun unlock_alias_address_owned_alias( + self: &mut Alias, + output_to_unlock: Receiving, + ): AliasOutput { + alias_output::receive(self.id(), output_to_unlock) + } + + /// Unlock a `TreasuryCap` locked to the alias address. + public fun unlock_alias_address_owned_treasury( + self: &mut Alias, + treasury_to_unlock: Receiving>, + ): TreasuryCap { + transfer::public_receive(self.id(), treasury_to_unlock) + } + + // TODO: be able to receive MaxSupplyPolicy from https://github.com/iotaledger/kinesis/pull/145 + + // === Receiving on NFT Address/NFTID as ObjectID === + + /// Unlock a `BasicOutput` locked to the `Nft` address. + public fun unlock_nft_address_owned_basic( + self: &mut Nft, + output_to_unlock: Receiving, + ): BasicOutput { + basic_output::receive(self.id(), output_to_unlock) + } + + /// Unlock an `NftOutput` locked to the `Nft` address. + public fun unlock_nft_address_owned_nft( + self: &mut Nft, + output_to_unlock: Receiving, + ): NftOutput { + nft_output::receive(self.id(), output_to_unlock) + } + + /// Unlock an `AliasOutput` locked to the `Nft` address. + public fun unlock_nft_address_owned_alias( + self: &mut Nft, + output_to_unlock: Receiving, + ): AliasOutput { + alias_output::receive(self.id(), output_to_unlock) + } +} diff --git a/identity_sui_name_tbd/packages/stardust-test/sources/unlock_condition/expiration_unlock_condition.move b/identity_sui_name_tbd/packages/stardust-test/sources/unlock_condition/expiration_unlock_condition.move new file mode 100644 index 0000000000..d3746fdbe6 --- /dev/null +++ b/identity_sui_name_tbd/packages/stardust-test/sources/unlock_condition/expiration_unlock_condition.move @@ -0,0 +1,67 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +module stardust::expiration_unlock_condition { + + /// The output can not be unlocked by the sender error. + const EWrongSender: u64 = 0; + + /// The Stardust expiration unlock condition. + public struct ExpirationUnlockCondition has store { + /// The address who owns the output before the timestamp has passed. + owner: address, + /// The address that is allowed to spend the locked funds after the timestamp has passed. + return_address: address, + /// Before this unix time, Address Unlock Condition is allowed to unlock the output, after that only the address defined in Return Address. + unix_time: u32, + } + + /// Check the unlock condition. + public fun unlock(condition: ExpirationUnlockCondition, ctx: &mut TxContext) { + let unlock_address = condition.can_be_unlocked_by(ctx); + + assert!(unlock_address == ctx.sender(), EWrongSender); + + let ExpirationUnlockCondition { + owner: _, + return_address: _, + unix_time: _, + } = condition; + } + + /// Return the address that can unlock the related output. + public fun can_be_unlocked_by(condition: &ExpirationUnlockCondition, ctx: &TxContext): address { + // Unix time in seconds. + let current_time = ((tx_context::epoch_timestamp_ms(ctx) / 1000) as u32); + + if (condition.unix_time() < current_time) { + condition.return_address() + } else { + condition.owner() + } + } + + /// Get the unlock condition's `owner`. + public fun owner(condition: &ExpirationUnlockCondition): address { + condition.owner + } + + /// Get the unlock condition's `return_address`. + public fun return_address(condition: &ExpirationUnlockCondition): address { + condition.return_address + } + + /// Get the unlock condition's `unix_time`. + public fun unix_time(condition: &ExpirationUnlockCondition): u32 { + condition.unix_time + } + + #[test_only] + public fun create_for_testing(owner: address, return_address: address, unix_time: u32): ExpirationUnlockCondition { + ExpirationUnlockCondition { + owner, + return_address, + unix_time, + } + } +} diff --git a/identity_sui_name_tbd/packages/stardust-test/sources/unlock_condition/storage_deposit_return_unlock_condition.move b/identity_sui_name_tbd/packages/stardust-test/sources/unlock_condition/storage_deposit_return_unlock_condition.move new file mode 100644 index 0000000000..e21373eb3b --- /dev/null +++ b/identity_sui_name_tbd/packages/stardust-test/sources/unlock_condition/storage_deposit_return_unlock_condition.move @@ -0,0 +1,50 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +module stardust::storage_deposit_return_unlock_condition { + + use sui::balance::{Balance, split}; + use sui::coin::from_balance; + use sui::sui::SUI; + use sui::transfer::public_transfer; + + /// The Stardust storage deposit return unlock condition. + public struct StorageDepositReturnUnlockCondition has store { + /// The address to which the consuming transaction should deposit the amount defined in Return Amount. + return_address: address, + /// The amount of IOTA coins the consuming transaction should deposit to the address defined in Return Address. + return_amount: u64, + } + + /// Check the unlock condition. + public fun unlock(condition: StorageDepositReturnUnlockCondition, funding: &mut Balance, ctx: &mut TxContext) { + // Aborts if `funding` is not enough. + let return_balance = funding.split(condition.return_amount()); + + // Recipient will need to transfer the coin to a normal ed25519 address instead of legacy. + public_transfer(from_balance(return_balance, ctx), condition.return_address()); + + let StorageDepositReturnUnlockCondition { + return_address: _, + return_amount: _, + } = condition; + } + + /// Get the unlock condition's `return_address`. + public fun return_address(condition: &StorageDepositReturnUnlockCondition): address { + condition.return_address + } + + /// Get the unlock condition's `return_amount`. + public fun return_amount(condition: &StorageDepositReturnUnlockCondition): u64 { + condition.return_amount + } + + #[test_only] + public fun create_for_testing(return_address: address, return_amount: u64): StorageDepositReturnUnlockCondition { + StorageDepositReturnUnlockCondition { + return_address, + return_amount, + } + } +} diff --git a/identity_sui_name_tbd/packages/stardust-test/sources/unlock_condition/timelock_unlock_condition.move b/identity_sui_name_tbd/packages/stardust-test/sources/unlock_condition/timelock_unlock_condition.move new file mode 100644 index 0000000000..aa1a7bfba9 --- /dev/null +++ b/identity_sui_name_tbd/packages/stardust-test/sources/unlock_condition/timelock_unlock_condition.move @@ -0,0 +1,38 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +module stardust::timelock_unlock_condition { + + /// The timelock is not expired error. + const ETimelockNotExpired: u64 = 0; + + /// The Stardust timelock unlock condition. + public struct TimelockUnlockCondition has store { + /// The unix time (seconds since Unix epoch) starting from which the output can be consumed. + unix_time: u32 + } + + /// Check the unlock condition. + public fun unlock(condition: TimelockUnlockCondition, ctx: &TxContext) { + assert!(!is_timelocked(&condition, ctx), ETimelockNotExpired); + + let TimelockUnlockCondition { + unix_time: _, + } = condition; + } + + /// Check if the output is locked by the `Timelock` condition. + public fun is_timelocked(condition: &TimelockUnlockCondition, ctx: &TxContext): bool { + condition.unix_time() > ((tx_context::epoch_timestamp_ms(ctx) / 1000) as u32) + } + + /// Get the unlock condition's `unix_time`. + public fun unix_time(condition: &TimelockUnlockCondition): u32 { + condition.unix_time + } + + #[test_only] + public fun create_for_testing(unix_time: u32): TimelockUnlockCondition { + TimelockUnlockCondition { unix_time } + } +} diff --git a/identity_sui_name_tbd/packages/stardust-test/sources/utilities.move b/identity_sui_name_tbd/packages/stardust-test/sources/utilities.move new file mode 100644 index 0000000000..b2bcff68ed --- /dev/null +++ b/identity_sui_name_tbd/packages/stardust-test/sources/utilities.move @@ -0,0 +1,45 @@ +module stardust::utilities { + // === Imports === + use std::type_name; + + use sui::bag::Bag; + use sui::balance::Balance; + use sui::coin; + + // === Errors === + + /// Returned when trying to extract a `Balance` from a `Bag` and the balance is zero. + const EZeroNativeTokenBalance: u64 = 0; + + // === Public-Mutative Functions === + + /// Extract a `Balance` from a `Bag`, create a `Coin` out of it and send it to the address. + /// NOTE: We return the `Bag` by value so the function can be called repeatedly in a PTB. + public fun extract_and_send_to(mut bag: Bag, to: address, ctx: &mut TxContext): Bag { + let coin = coin::from_balance(extract_(&mut bag), ctx); + transfer::public_transfer(coin, to); + bag + } + + /// Extract a `Balance` from a `Bag` and return it. Caller can decide what to do with it. + /// NOTE: We return the `Bag` by value so the function can be called repeatedly in a PTB. + public fun extract(mut bag: Bag): (Bag, Balance) { + let balance = extract_(&mut bag); + (bag, balance) + } + + // === Private Functions === + + /// Get a `Balance` from a `Bag`. + /// Aborts if the balance is zero or if there is no balance for the type `T`. + fun extract_(bag: &mut Bag): Balance { + let key = type_name::get().into_string(); + + // This call aborts if the key doesn't exist. + let balance : Balance = bag.remove(key); + + assert!(balance.value() != 0, EZeroNativeTokenBalance); + + balance + } +} diff --git a/identity_sui_name_tbd/packages/stardust-test/tests/alias_tests.move b/identity_sui_name_tbd/packages/stardust-test/tests/alias_tests.move new file mode 100644 index 0000000000..e48f1f10c7 --- /dev/null +++ b/identity_sui_name_tbd/packages/stardust-test/tests/alias_tests.move @@ -0,0 +1,106 @@ +module stardust::alias_tests { + + use std::type_name; + + use sui::bag; + use sui::balance; + use sui::coin; + use sui::sui::SUI; + + use stardust::alias; + use stardust::alias_output; + use stardust::utilities; + + const ENativeTokenBagNonEmpty: u64 = 1; + + // One Time Witness for coins used in the tests. + public struct TEST_A has drop {} + public struct TEST_B has drop {} + + // Demonstration on how to claim the assets from an `AliasOutput` with all unlock conditions inside one PTB. + #[test] + fun demonstrate_claiming_ptb() { + let initial_iota_in_output = 10000; + let initial_testA_in_output = 100; + let initial_testB_in_output = 100; + + let owner = @0xA; + let migrate_to = @0xD; + + // Create a new tx context. + let mut ctx = tx_context::new( + // sender + @0xA, + // tx_hash + x"3a985da74fe225b2045c172d6bd390bd855f086e3e9d525b46bfe24511431532", + // epoch + 1, + // epoch ts in ms (10 in seconds) + 10000, + // ids created + 0, + ); + + // Mint some tokens. + let iota = balance::create_for_testing(initial_iota_in_output); + + let test_a_balance = balance::create_for_testing(initial_testA_in_output); + let test_b_balance = balance::create_for_testing(initial_testB_in_output); + + // Add the native token balances to the bag. + let mut native_tokens = bag::new(&mut ctx); + + native_tokens.add(type_name::get().into_string(), test_a_balance); + native_tokens.add(type_name::get().into_string(), test_b_balance); + + // Create an `AliasOutput`. + let mut alias_output = alias_output::create_for_testing( + iota, + native_tokens, + &mut ctx, + ); + + let alias = alias::create_for_testing( + // legacy state controller + option::some(owner), + // state index + 0, + // state metadata + option::some(b"state metadata content"), + // sender feature + option::some(owner), + // metadata feature + option::some(b"metadata content"), + // issuer feature + option::some(owner), + // immutable metadata + option::some(b"immutable metadata content"), + &mut ctx, + ); + alias_output.attach_alias(alias); + + // Command 1: extract the base token and native tokens bag. + let (extracted_base_token, mut extracted_native_tokens, extracted_alias) = alias_output.extract_assets(); + + // Command 2: extract the asset A and send to the user. + extracted_native_tokens = utilities::extract_and_send_to(extracted_native_tokens, migrate_to, &mut ctx); + + // Command 3: extract the asset B and send to the user + extracted_native_tokens = utilities::extract_and_send_to(extracted_native_tokens, migrate_to, &mut ctx); + assert!(extracted_native_tokens.is_empty(), ENativeTokenBagNonEmpty); + + // Command 4: delete the bag. + bag::destroy_empty(extracted_native_tokens); + + // Command 5: create a coin from the extracted IOTA balance. + let iota_coin = coin::from_balance(extracted_base_token, &mut ctx); + + // Command 6: send back the base token coin to the user. + transfer::public_transfer(iota_coin, migrate_to); + + // Command 7: destroy the alias. + alias::destroy(extracted_alias); + + // !!! migration complete !!! + } +} diff --git a/identity_sui_name_tbd/packages/stardust-test/tests/basic_tests.move b/identity_sui_name_tbd/packages/stardust-test/tests/basic_tests.move new file mode 100644 index 0000000000..c54ea8a0e2 --- /dev/null +++ b/identity_sui_name_tbd/packages/stardust-test/tests/basic_tests.move @@ -0,0 +1,110 @@ +module stardust::basic_tests { + + use std::type_name; + + use sui::bag; + use sui::balance; + use sui::coin; + use sui::sui::SUI; + + use stardust::basic_output; + use stardust::expiration_unlock_condition; + use stardust::storage_deposit_return_unlock_condition; + use stardust::timelock_unlock_condition; + use stardust::utilities; + + const ENoBaseTokenBalance: u64 = 1; + const ENativeTokenBagNonEmpty: u64 = 2; + const EIotaBalanceMismatch: u64 = 3; + + // One Time Witness for coins used in the tests. + public struct TEST_A has drop {} + public struct TEST_B has drop {} + + // Demonstration on how to claim the assets from a `BasicOutput` with all unlock conditions inside one PTB. + #[test] + fun demonstrate_claiming_ptb() { + let initial_iota_in_output = 10000; + let initial_testA_in_output = 100; + let initial_testB_in_output = 100; + let sdruc_return_amount = 1000; + + let timelocked_until = 5; + let expiration_after = 20; + let owner = @0xA; + let sdruc_return_address = @0xB; + let expiration_return_address = @0xC; + let migrate_to = @0xD; + + // Create a new tx context. + let mut ctx = tx_context::new( + // sender + @0xA, + // tx_hash + x"3a985da74fe225b2045c172d6bd390bd855f086e3e9d525b46bfe24511431532", + // epoch + 1, + // epoch ts in ms (10 in seconds) + 10000, + // ids created + 0, + ); + + // Mint some tokens. + let iota = balance::create_for_testing(initial_iota_in_output); + + let test_a_balance = balance::create_for_testing(initial_testA_in_output); + let test_b_balance = balance::create_for_testing(initial_testB_in_output); + + // Add the native token balances to the bag. + let mut native_tokens = bag::new(&mut ctx); + + native_tokens.add(type_name::get().into_string(), test_a_balance); + native_tokens.add(type_name::get().into_string(), test_b_balance); + + let output = basic_output::create_for_testing( + iota, + native_tokens, + option::some(storage_deposit_return_unlock_condition::create_for_testing(sdruc_return_address, sdruc_return_amount)), + option::some(timelock_unlock_condition::create_for_testing(timelocked_until)), + option::some(expiration_unlock_condition::create_for_testing(owner, expiration_return_address, expiration_after)), + // metadata feature + option::some(b"metadata content"), + // tag feature + option::some(b"tag content"), + // sender feature + option::some(owner), + &mut ctx, + ); + + // Ready with the basic output, now we can claim the assets. + // The task is to assemble a PTB-like transaction in move that demonstrates how to claim. + // PTB inputs: basic output ID (`basic`) and address to migrate to (`migrate_to`) + + // Command 1: extract the base token and native tokens bag. + let (extracted_base_token, mut extracted_native_tokens) = output.extract_assets(&mut ctx); + assert!(extracted_base_token.value() == 9000, ENoBaseTokenBalance); + + // Command 2: extract the asset A and send to the user. + extracted_native_tokens = utilities::extract_and_send_to(extracted_native_tokens, migrate_to, &mut ctx); + + // Command 3: extract the asset B and send to the user. + extracted_native_tokens = utilities::extract_and_send_to(extracted_native_tokens, migrate_to, &mut ctx); + assert!(extracted_native_tokens.is_empty(), ENativeTokenBagNonEmpty); + + // Command 4: delete the bag. + extracted_native_tokens.destroy_empty(); + + // Command 5: create a coin from the extracted IOTA balance. + let iota_coin = coin::from_balance(extracted_base_token, &mut ctx); + // We should have `initial_iota_in_output` - `sdruc_return_amount` left in the coin. + assert!(iota_coin.value() == (initial_iota_in_output - sdruc_return_amount), EIotaBalanceMismatch); + + // Command 6: send back the base token coin to the user. + // If we sponsored the transaction with our own coins, now is the time to detuct it from the user by taking from `iota_coin` and merging it into the gas token + // since we can dry run the tx before submission, we know how much to charge the user, or we charge the whole gas budget. + transfer::public_transfer(iota_coin, migrate_to); + + // !!! migration complete !!! + } +} diff --git a/identity_sui_name_tbd/packages/stardust-test/tests/capped_coin_tests.move b/identity_sui_name_tbd/packages/stardust-test/tests/capped_coin_tests.move new file mode 100644 index 0000000000..f31b4009ba --- /dev/null +++ b/identity_sui_name_tbd/packages/stardust-test/tests/capped_coin_tests.move @@ -0,0 +1,134 @@ +// Copyright (c) 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +module stardust::capped_coin_tests { + + use sui::coin::{Self, Coin}; + use sui::test_scenario; + + use stardust::capped_coin; + + public struct CAPPED_COIN_TESTS has drop {} + + // Show how Capped Coin works and test if it acts according to specs. + #[test] + fun create_and_mint_capped_coin() { + // Set up a test environment. + let sender = @0xA; + let mut scenario = test_scenario::begin(sender); + let witness = CAPPED_COIN_TESTS{}; + + // Create a Coin. + let (cap, meta) = coin::create_currency( + witness, + 0, + b"TEST", + b"TEST", + b"TEST", + option::none(), + scenario.ctx(), + ); + + // Create the policy and consume the Cap, only 100 allowed of this coin after this! + let mut policy = capped_coin::create_max_supply_policy(cap, 100, scenario.ctx()); + + // We should start out with a Supply of 0. + assert!(capped_coin::total_supply(&policy) == 0, 0); + + capped_coin::mint_and_transfer(&mut policy, 10, sender, scenario.ctx()); + + scenario.next_tx(sender); + + // We should start out with a Supply of 0. + assert!(capped_coin::total_supply(&policy) == 10, 0); + + transfer::public_transfer(policy, scenario.ctx().sender()); + transfer::public_freeze_object(meta); + + scenario.end(); + } + + // Show how burning works. + #[test] + fun create_and_burn_capped_coin() { + // Set up a test environment. + let sender = @0xA; + let mut scenario = test_scenario::begin(sender); + let witness = CAPPED_COIN_TESTS{}; + + // Create a Coin. + let (cap, meta) = coin::create_currency( + witness, + 0, + b"TEST", + b"TEST", + b"TEST", + option::none(), + scenario.ctx(), + ); + + // Create the policy and consume the Cap, only 100 allowed of this coin after this! + let mut policy = capped_coin::create_max_supply_policy(cap, 100, scenario.ctx()); + + // We should start out with a Supply of 0. + assert!(capped_coin::total_supply(&policy) == 0, 0); + + capped_coin::mint_and_transfer(&mut policy, 10, sender, scenario.ctx()); + + scenario.next_tx(sender); + + // We should start out with a Supply of 0. + assert!(capped_coin::total_supply(&policy) == 10, 0); + + let coin = scenario.take_from_address>(sender); + capped_coin::burn(&mut policy, coin); + + assert!(capped_coin::total_supply(&policy) == 0, 0); + + transfer::public_transfer(policy, scenario.ctx().sender()); + transfer::public_freeze_object(meta); + + scenario.end(); + } + + // Demonstrate cap limitations. + #[test] + #[expected_failure(abort_code = capped_coin::EMaximumSupplyReached)] + fun create_and_mint_too_many_capped_coins() { + // Set up a test environment. + let sender = @0xA; + let mut scenario = test_scenario::begin(sender); + let witness = CAPPED_COIN_TESTS{}; + + // Create a Coin. + let (cap, meta) = coin::create_currency( + witness, + 0, + b"TEST", + b"TEST", + b"TEST", + option::none(), + scenario.ctx(), + ); + + // Create the policy and consume the Cap, only 100 allowed of this coin after this! + let mut policy = capped_coin::create_max_supply_policy(cap, 100, scenario.ctx()); + + // We should start out with a Supply of 0. + assert!(capped_coin::total_supply(&policy) == 0, 0); + + capped_coin::mint_and_transfer(&mut policy, 10, sender, scenario.ctx()); + + scenario.next_tx(sender); + + // We should start out with a Supply of 0. + assert!(capped_coin::total_supply(&policy) == 10, 0); + + capped_coin::mint_and_transfer(&mut policy, 1000, sender, scenario.ctx()); + + transfer::public_transfer(policy, scenario.ctx().sender()); + transfer::public_freeze_object(meta); + + scenario.end(); + } +} diff --git a/identity_sui_name_tbd/packages/stardust-test/tests/nft_tests.move b/identity_sui_name_tbd/packages/stardust-test/tests/nft_tests.move new file mode 100644 index 0000000000..0a5f28877e --- /dev/null +++ b/identity_sui_name_tbd/packages/stardust-test/tests/nft_tests.move @@ -0,0 +1,148 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +module stardust::nft_tests { + + use std::ascii; + use std::fixed_point32; + use std::string; + use std::type_name; + + use sui::bag; + use sui::balance::{Self, Balance}; + use sui::coin::{Self, Coin}; + use sui::sui::SUI; + use sui::table; + use sui::test_scenario; + use sui::url; + use sui::vec_set; + + use stardust::irc27; + use stardust::nft_output; + use stardust::nft; + + use stardust::expiration_unlock_condition; + use stardust::storage_deposit_return_unlock_condition; + use stardust::timelock_unlock_condition; + + // One time witness for coins used in the tests. + public struct TEST_A has drop {} + public struct TEST_B has drop {} + + // Demonstration on how to claim the assets from an NFT output with all unlock conditions inside one PTB. + #[test] + fun nft_assets_extraction() { + // Set up a test enviroment. + let sender = @0xA; + let mut scenario = test_scenario::begin(sender); + + // Create an NftOutput object. + let test_a_balance = balance::create_for_testing(100); + let test_b_balance = balance::create_for_testing(200); + + let mut native_tokens = bag::new(scenario.ctx()); + native_tokens.add(type_name::get().into_string(), test_a_balance); + native_tokens.add(type_name::get().into_string(), test_b_balance); + + let mut nft_output = nft_output::create_for_testing( + balance::create_for_testing(10000), + native_tokens, + option::some(storage_deposit_return_unlock_condition::create_for_testing(@0xB, 1000)), + option::some(timelock_unlock_condition::create_for_testing(5)), + option::some(expiration_unlock_condition::create_for_testing(sender, @0xB, 20)), + scenario.ctx(), + ); + + // Create an Nft object. + let mut royalties = table::new(scenario.ctx()); + royalties.add(sender, fixed_point32::create_from_rational(1, 2)); + + let mut attributes = vec_set::empty(); + attributes.insert(string::utf8(b"attribute")); + + let mut non_standard_fields = table::new(scenario.ctx()); + non_standard_fields.add(string::utf8(b"field"), string::utf8(b"value")); + + let nft = nft::create_for_testing( + option::some(sender), + option::some(b"metadata"), + option::some(b"tag"), + option::some(sender), + irc27::create_for_testing( + string::utf8(b"0.0.1"), + string::utf8(b"image/png"), + url::new_unsafe(ascii::string(b"www.best-nft.com/nft.png")), + string::utf8(b"nft"), + option::some(string::utf8(b"collection")), + royalties, + option::some(string::utf8(b"issuer")), + option::some(string::utf8(b"description")), + attributes, + non_standard_fields, + ), + scenario.ctx(), + ); + + // Add the Nft as a dynamic field to the output. + nft_output.attach_nft(nft); + + // Increment epoch timestamp. + scenario.ctx().increment_epoch_timestamp(10000); + + // Extract assets. + let (iota, mut native_tokens, nft) = nft_output.extract_assets(scenario.ctx()); + + // Check the extracted IOTA balance. + assert!(iota.value() == 9000, 0); + + // Check the extracted native tokens. + assert!(native_tokens.borrow>(type_name::get().into_string()).value() == 100, 1); + assert!(native_tokens.borrow>(type_name::get().into_string()).value() == 200, 2); + + // Check the extracted NFT. + assert!(nft.legacy_sender().contains(&sender), 3); + assert!(nft.metadata().contains(&b"metadata"), 4); + assert!(nft.tag().contains(&b"tag"), 5); + assert!(nft.immutable_issuer().contains(&sender), 6); + + assert!(nft.immutable_metadata().version() == string::utf8(b"0.0.1"), 7); + assert!(nft.immutable_metadata().media_type() == string::utf8(b"image/png"), 8); + assert!(nft.immutable_metadata().uri() == url::new_unsafe(ascii::string(b"www.best-nft.com/nft.png")), 9); + assert!(nft.immutable_metadata().name() == string::utf8(b"nft"), 10); + assert!(nft.immutable_metadata().collection_name().contains(&string::utf8(b"collection")), 11); + assert!(nft.immutable_metadata().royalties().length() == 1, 12); + assert!(nft.immutable_metadata().royalties()[sender] == fixed_point32::create_from_rational(1, 2), 13); + assert!(nft.immutable_metadata().issuer_name().contains(&string::utf8(b"issuer")), 14); + assert!(nft.immutable_metadata().description().contains(&string::utf8(b"description")), 15); + assert!(nft.immutable_metadata().attributes().size() == 1, 16); + assert!(nft.immutable_metadata().attributes().contains(&string::utf8(b"attribute")), 17); + + assert!(nft.immutable_metadata().non_standard_fields().length() == 1, 18); + assert!(nft.immutable_metadata().non_standard_fields()[string::utf8(b"field")] == string::utf8(b"value"), 19); + + // Check the storage deposit return. + scenario.next_tx(sender); + + let returned_storage_deposit = scenario.take_from_address>(@0xB); + + assert!(returned_storage_deposit.value() == 1000, 18); + + test_scenario::return_to_address(@0xB, returned_storage_deposit); + + // Transfer the extracted assets. + transfer::public_transfer(coin::from_balance(iota, scenario.ctx()), @0xC); + + let coin_a = coin::from_balance(native_tokens.remove>(type_name::get().into_string()), scenario.ctx()); + let coin_b = coin::from_balance(native_tokens.remove>(type_name::get().into_string()), scenario.ctx()); + + transfer::public_transfer(coin_a, @0xC); + transfer::public_transfer(coin_b, @0xC); + + transfer::public_transfer(nft, @0xC); + + // Cleanup. + bag::destroy_empty(native_tokens); + + scenario.end(); + } +} diff --git a/identity_sui_name_tbd/packages/stardust-test/tests/unlock_condition/address_unlock_condition_tests.move b/identity_sui_name_tbd/packages/stardust-test/tests/unlock_condition/address_unlock_condition_tests.move new file mode 100644 index 0000000000..4b455716bd --- /dev/null +++ b/identity_sui_name_tbd/packages/stardust-test/tests/unlock_condition/address_unlock_condition_tests.move @@ -0,0 +1,132 @@ +module stardust::address_unlock_condition_tests { + + use sui::bag; + use sui::balance; + use sui::coin; + use sui::sui::SUI; + + use stardust::alias; + use stardust::alias_output; + use stardust::basic_output; + use stardust::expiration_unlock_condition; + use stardust::storage_deposit_return_unlock_condition; + use stardust::timelock_unlock_condition; + + const ENativeTokenBagNonEmpty: u64 = 1; + const EIotaBlanceMismatch: u64 = 3; + + // One Time Witness for coins used in the tests. + public struct TEST_A has drop {} + public struct TEST_B has drop {} + + // Demonstration on how to claim the assets from a basic alias_output with all unlock conditions inside one PTB. + #[test] + fun demonstrate_alias_address_unlocking() { + let initial_iota_in_output = 10000; + + let owner = @0xA; + let migrate_to = @0xD; + + // Create a new tx context. + let mut ctx = tx_context::new( + // sender + @0xA, + // tx)hash + x"3a985da74fe225b2045c172d6bd390bd855f086e3e9d525b46bfe24511431532", + // epoch + 1, + // epoch ts in ms (10 in seconds) + 10000, + // ids created + 0, + ); + + let mut alias_output = alias_output::create_for_testing( + // iota + balance::create_for_testing(initial_iota_in_output), + // tokens + bag::new(&mut ctx), + &mut ctx, + ); + + let alias = alias::create_for_testing( + // legacy state controller + option::some(owner), + // state index + 0, + // state metadata + option::some(b"state metadata content"), + // sender feature + option::some(owner), + // metadata feature + option::some(b"metadata content"), + // issuer feature + option::some(owner), + // immutable metadata + option::some(b"immutable metadata content"), + &mut ctx, + ); + + alias_output.attach_alias(alias); + + // `BasicOutput` owned by the alias. + let basic_sui_balance = balance::create_for_testing(initial_iota_in_output); + let timelocked_until = 5; + let expiration_after = 20; + let sdruc_return_address = @0xB; + let sdruc_return_amount = 1000; + let expiration_return_address = @0xC; + + let basic_output = basic_output::create_for_testing( + basic_sui_balance, + bag::new(&mut ctx), + option::some(storage_deposit_return_unlock_condition::create_for_testing(sdruc_return_address, sdruc_return_amount)), + option::some(timelock_unlock_condition::create_for_testing(timelocked_until)), + option::some(expiration_unlock_condition::create_for_testing(owner, expiration_return_address, expiration_after)), + // metadata feature + option::some(b"metadata content"), + // tag feature + option::some(b"tag content"), + // sender feature + option::some(owner), + &mut ctx, + ); + + // Command 1: unlock the basic token. + // TODO: is it possible to create a Receiving object? + // transfer::transfer(basic_output, alias.id().uid_to_address()); + // let basic_output = unlock_alias_address_owned_basic(&mut alias, basic_output); + + // Command 2: extract the base token and native token bag. + let (extracted_base_token, extracted_native_tokens) = basic_output.extract_assets(&mut ctx); + + // Command 3: delete the bag. + extracted_native_tokens.destroy_empty(); + + // Command 4: create a coin from the extracted IOTA balance. + let iota_coin = coin::from_balance(extracted_base_token, &mut ctx); + // We should have `initial_iota_in_output` - `sdruc_return_amount` left in the coin. + assert!(iota_coin.value() == (initial_iota_in_output - sdruc_return_amount), EIotaBlanceMismatch); + + // Command 6: send back the base token coin to the user. + transfer::public_transfer(iota_coin, migrate_to); + + // Command 7: extract the base token and native tokens bag. + let (extracted_base_token, native_token_bag, extracted_alias) = alias_output.extract_assets(); + + // Command 8: delete the bag. + assert!(native_token_bag.is_empty(), ENativeTokenBagNonEmpty); + bag::destroy_empty(native_token_bag); + + // Command 9: create a coin from the extracted iota balance. + let iota_coin = coin::from_balance(extracted_base_token, &mut ctx); + + // Command 11: send back the base token coin to the user. + transfer::public_transfer(iota_coin, migrate_to); + + // Command 12: destroy the alias. + alias::destroy(extracted_alias); + + // !!! migration complete !!! + } +} diff --git a/identity_sui_name_tbd/scripts/create_test_alias_output.sh b/identity_sui_name_tbd/scripts/create_test_alias_output.sh new file mode 100755 index 0000000000..e9054db754 --- /dev/null +++ b/identity_sui_name_tbd/scripts/create_test_alias_output.sh @@ -0,0 +1,36 @@ +#!/bin/bash + +# Copyright 2020-2024 IOTA Stiftung +# SPDX-License-Identifier: Apache-2.0 + +if [ -z "$1" ] + then + echo "No arguments supplied, please pass package id as hex string" + exit 1 +fi + + +# create_for_testing args: +# legacy_state_controller: Option
, +# state_index: u32, +# state_metadata: Option>, +# sender: Option
, +# metadata: Option>, +# immutable_issuer: Option
, +# immutable_metadata: Option>, +# ctx: &mut TxContext +sui \ + client \ + call \ + --package $1 \ + --module alias \ + --function create_for_testing \ + --args \ + [] \ + 123 \ + [] \ + [] \ + [] \ + [] \ + [] \ + --gas-budget 10000000 diff --git a/identity_sui_name_tbd/scripts/publish_stardust_test.sh b/identity_sui_name_tbd/scripts/publish_stardust_test.sh new file mode 100755 index 0000000000..9eb0fb6bcb --- /dev/null +++ b/identity_sui_name_tbd/scripts/publish_stardust_test.sh @@ -0,0 +1,11 @@ +#!/bin/bash + +# Copyright 2020-2024 IOTA Stiftung +# SPDX-License-Identifier: Apache-2.0 + +script_dir=$(dirname $0) +package_dir=$script_dir/../packages/stardust-test + +echo "publishing package from $package_dir" +cd $package_dir +sui client publish --gas-budget 100000000 . diff --git a/identity_sui_name_tbd/src/error.rs b/identity_sui_name_tbd/src/error.rs new file mode 100644 index 0000000000..57c9305d3b --- /dev/null +++ b/identity_sui_name_tbd/src/error.rs @@ -0,0 +1,23 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +//! Errors that may occur in the identity_sui_name_tbd crate. + +/// This type represents all possible errors that can occur in the library. +#[derive(Debug, thiserror::Error, strum::IntoStaticStr)] +#[non_exhaustive] +pub enum Error { + // because we'll most probably need them later anyway + // /// Caused by a failure to encode Rust types as JSON. + // #[error("failed to encode JSON")] + // EncodeJSON(#[source] serde_json::Error), + // /// Caused by a failure to decode Rust types from JSON. + // #[error("failed to decode JSON")] + // DecodeJSON(#[source] serde_json::Error), + /// failed to connect to network + #[error("failed to connect to sui network node; {0:?}")] + Network(String, #[source] sui_sdk::error::Error), + /// could not lookup an object ID + #[error("failed to lookup an object; {0}")] + ObjectLookup(String), +} diff --git a/identity_sui_name_tbd/src/lib.rs b/identity_sui_name_tbd/src/lib.rs new file mode 100644 index 0000000000..86e595ec73 --- /dev/null +++ b/identity_sui_name_tbd/src/lib.rs @@ -0,0 +1,8 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +mod error; +pub mod resolution; +mod utils; + +pub use error::Error; diff --git a/identity_sui_name_tbd/src/resolution/mod.rs b/identity_sui_name_tbd/src/resolution/mod.rs new file mode 100644 index 0000000000..5262837fc8 --- /dev/null +++ b/identity_sui_name_tbd/src/resolution/mod.rs @@ -0,0 +1,6 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +mod unmigrated_resolver; + +pub use unmigrated_resolver::*; diff --git a/identity_sui_name_tbd/src/resolution/unmigrated_resolver.rs b/identity_sui_name_tbd/src/resolution/unmigrated_resolver.rs new file mode 100644 index 0000000000..88fef16a25 --- /dev/null +++ b/identity_sui_name_tbd/src/resolution/unmigrated_resolver.rs @@ -0,0 +1,73 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use std::str::FromStr; + +use serde_json::Value; +use sui_sdk::rpc_types::SuiObjectDataOptions; +use sui_sdk::rpc_types::SuiParsedData; +use sui_sdk::types::base_types::ObjectID; +use sui_sdk::SuiClient; + +use crate::utils::get_client; +use crate::Error; + +pub const LOCAL_NETWORK: &str = "http://127.0.0.1:9000"; + +pub struct UnmigratedResolver { + client: SuiClient, +} + +impl UnmigratedResolver { + pub async fn new(network: &str) -> Result { + let client = get_client(network).await?; + + Ok(Self { client }) + } + + pub async fn get_alias_output(&self, object_id: &str) -> Result { + let object_id = ObjectID::from_str(object_id) + .map_err(|err| Error::ObjectLookup(format!("Could not parse given object id {object_id}; {err}")))?; + let options = SuiObjectDataOptions { + show_type: true, + show_owner: true, + show_previous_transaction: true, + show_display: true, + show_content: true, + show_bcs: true, + show_storage_rebate: true, + }; + let response = self + .client + .read_api() + .get_object_with_options(object_id, options) + .await + .map_err(|err| { + Error::ObjectLookup(format!( + "Could not get object with options for this object_id {object_id}; {err}" + )) + })?; + + let data = response.data.ok_or_else(|| { + Error::ObjectLookup(format!( + "Call succeeded but could not get data for object id {object_id}" + )) + })?; + let content = data + .content + .ok_or_else(|| Error::ObjectLookup(format!("no content in retrieved object in object id {object_id}")))?; + + let object = match content { + SuiParsedData::MoveObject(value) => value, + _ => { + return Err(Error::ObjectLookup(format!( + "found data at object id {object_id} is not an object" + ))); + } + }; + + let alias_output = object.fields.to_json_value(); + + Ok(alias_output) + } +} diff --git a/identity_sui_name_tbd/src/utils.rs b/identity_sui_name_tbd/src/utils.rs new file mode 100644 index 0000000000..f30bc2eaf3 --- /dev/null +++ b/identity_sui_name_tbd/src/utils.rs @@ -0,0 +1,16 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use sui_sdk::SuiClient; +use sui_sdk::SuiClientBuilder; + +use crate::Error; + +pub async fn get_client(network: &str) -> Result { + let client = SuiClientBuilder::default() + .build(network) + .await + .map_err(|err| Error::Network(format!("failed to connect to {network}"), err))?; + + Ok(client) +} diff --git a/identity_sui_name_tbd/tests/test.rs b/identity_sui_name_tbd/tests/test.rs new file mode 100644 index 0000000000..d00dcf9b46 --- /dev/null +++ b/identity_sui_name_tbd/tests/test.rs @@ -0,0 +1,27 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use identity_sui_name_tbd::resolution::UnmigratedResolver; +use identity_sui_name_tbd::resolution::LOCAL_NETWORK; + +#[tokio::test] +async fn can_initialize_resolver_for_unmigrated_alias_outputs() -> anyhow::Result<()> { + let result = UnmigratedResolver::new(LOCAL_NETWORK).await; + + assert!(result.is_ok()); + + Ok(()) +} + +#[tokio::test] +async fn can_fetch_alias_output_by_object_id() -> anyhow::Result<()> { + let resolver = UnmigratedResolver::new(LOCAL_NETWORK).await?; + let result = resolver + .get_alias_output("0x669c70a008a5e226813927a7b62a5029306d8f7e7366b0634ef6027b3dbda850") + .await; + + dbg!(&result); + assert!(result.is_ok()); + + Ok(()) +} From 7c896c2e2ca88648a222dd1a63f6d670a4145e19 Mon Sep 17 00:00:00 2001 From: Enrico Marconi Date: Tue, 7 May 2024 10:19:58 +0200 Subject: [PATCH 02/62] identity-iota move package, DID Document Obj, MigrationRegistry Obj - migration registry in move code - migration registry integration --- .gitignore | 3 + identity_sui_name_tbd/Cargo.toml | 2 +- .../packages/identity_iota/Move.lock | 38 ++++++++ .../packages/identity_iota/Move.toml | 16 ++++ .../identity_iota/sources/controller.move | 19 ++++ .../identity_iota/sources/document.move | 77 ++++++++++++++++ .../sources/migration_registry.move | 44 ++++++++++ .../packages/stardust-test/Move.lock | 6 +- .../packages/stardust-test/Move.toml | 2 +- .../stardust-test/sources/alias/alias.move | 22 ++++- .../sources/alias/alias_output.move | 17 +++- .../scripts/create_test_alias_output.sh | 42 ++++----- .../scripts/migrate_alias_output.sh | 30 +++++++ identity_sui_name_tbd/src/error.rs | 3 + identity_sui_name_tbd/src/lib.rs | 1 + identity_sui_name_tbd/src/migration/mod.rs | 6 ++ .../src/migration/registry.rs | 88 +++++++++++++++++++ .../src/resolution/unmigrated_resolver.rs | 13 ++- 18 files changed, 384 insertions(+), 45 deletions(-) create mode 100644 identity_sui_name_tbd/packages/identity_iota/Move.lock create mode 100644 identity_sui_name_tbd/packages/identity_iota/Move.toml create mode 100644 identity_sui_name_tbd/packages/identity_iota/sources/controller.move create mode 100644 identity_sui_name_tbd/packages/identity_iota/sources/document.move create mode 100644 identity_sui_name_tbd/packages/identity_iota/sources/migration_registry.move create mode 100755 identity_sui_name_tbd/scripts/migrate_alias_output.sh create mode 100644 identity_sui_name_tbd/src/migration/mod.rs create mode 100644 identity_sui_name_tbd/src/migration/registry.rs diff --git a/.gitignore b/.gitignore index 5a33dbab25..40cc69fcac 100644 --- a/.gitignore +++ b/.gitignore @@ -28,3 +28,6 @@ index.html *.hodl.* !/bindings/wasm/static/index.html + +# ignore SUI build artifacts +build diff --git a/identity_sui_name_tbd/Cargo.toml b/identity_sui_name_tbd/Cargo.toml index 9d5a205456..791d21a494 100644 --- a/identity_sui_name_tbd/Cargo.toml +++ b/identity_sui_name_tbd/Cargo.toml @@ -17,9 +17,9 @@ serde_json.workspace = true strum.workspace = true sui-sdk = { git = "https://github.com/mystenlabs/sui", package = "sui-sdk"} thiserror.workspace = true +anyhow = "1.0.75" [dev-dependencies] -anyhow = "1.0.75" tokio = { version = "1.29.0", default-features = false, features = ["macros", "sync", "rt"] } [lints] diff --git a/identity_sui_name_tbd/packages/identity_iota/Move.lock b/identity_sui_name_tbd/packages/identity_iota/Move.lock new file mode 100644 index 0000000000..d182f7c848 --- /dev/null +++ b/identity_sui_name_tbd/packages/identity_iota/Move.lock @@ -0,0 +1,38 @@ +# @generated by Move, please check-in and do not edit manually. + +[move] +version = 0 +manifest_digest = "5302BCFBECBC30D467D03B2D18F53E3EE90CFEB854C9E73696E0BE70EF064980" +deps_digest = "060AD7E57DFB13104F21BE5F5C3759D03F0553FC3229247D9A7A6B45F50D03A3" + +dependencies = [ + { name = "MoveStdlib" }, + { name = "StardustTest" }, + { name = "Sui" }, +] + +[[move.package]] +name = "MoveStdlib" +source = { git = "https://github.com/iotaledger/kinesis.git", rev = "7899dc9ce682c3d0a97f249ce7eaa27b9473b920", subdir = "crates/sui-framework/packages/move-stdlib" } + +[[move.package]] +name = "StardustTest" +source = { local = "../stardust-test" } + +dependencies = [ + { name = "MoveStdlib" }, + { name = "Sui" }, +] + +[[move.package]] +name = "Sui" +source = { git = "https://github.com/iotaledger/kinesis.git", rev = "7899dc9ce682c3d0a97f249ce7eaa27b9473b920", subdir = "crates/sui-framework/packages/sui-framework" } + +dependencies = [ + { name = "MoveStdlib" }, +] + +[move.toolchain-version] +compiler-version = "1.22.0" +edition = "legacy" +flavor = "sui" diff --git a/identity_sui_name_tbd/packages/identity_iota/Move.toml b/identity_sui_name_tbd/packages/identity_iota/Move.toml new file mode 100644 index 0000000000..a5bd3cb7e7 --- /dev/null +++ b/identity_sui_name_tbd/packages/identity_iota/Move.toml @@ -0,0 +1,16 @@ +[package] +name = "IdentityIota" +edition = "2024.beta" + +[dependencies] +MoveStdlib = { git = "https://github.com/iotaledger/kinesis.git", subdir = "crates/sui-framework/packages/move-stdlib", rev = "7899dc9ce682c3d0a97f249ce7eaa27b9473b920" } +Sui = { git = "https://github.com/iotaledger/kinesis.git", subdir = "crates/sui-framework/packages/sui-framework", rev = "7899dc9ce682c3d0a97f249ce7eaa27b9473b920" } +StardustTest = { local = "../stardust-test" } + +[addresses] +identity_iota = "0x0" + +[dev-dependencies] + +[dev-addresses] + diff --git a/identity_sui_name_tbd/packages/identity_iota/sources/controller.move b/identity_sui_name_tbd/packages/identity_iota/sources/controller.move new file mode 100644 index 0000000000..e3d14df55f --- /dev/null +++ b/identity_sui_name_tbd/packages/identity_iota/sources/controller.move @@ -0,0 +1,19 @@ +module identity_iota::controller { + + public struct ControllerCap has key, store { + id: UID, + /// The DID this capability has control over. + did: ID, + } + + public(package) fun new(did: ID, ctx: &mut TxContext): ControllerCap { + ControllerCap { + id: object::new(ctx), + did, + } + } + + public fun did(self: &ControllerCap): ID { + self.did + } +} \ No newline at end of file diff --git a/identity_sui_name_tbd/packages/identity_iota/sources/document.move b/identity_sui_name_tbd/packages/identity_iota/sources/document.move new file mode 100644 index 0000000000..be18587215 --- /dev/null +++ b/identity_sui_name_tbd/packages/identity_iota/sources/document.move @@ -0,0 +1,77 @@ +module identity_iota::document { + use sui::{balance::Balance, bag::Bag, sui::SUI, transfer::share_object}; + use stardust::alias_output::{AliasOutput, extract_assets}; + use identity_iota::{controller, controller::ControllerCap, migration_registry::MigrationRegistry}; + + const ENotADidOutput: u64 = 1; + const EInvalidCapability: u64 = 2; + + /// DID document. + public struct Document has key { + id: UID, + doc: vector, + iota: Balance, + native_tokens: Bag, + } + + /// Creates a new `Document` from an Iota 1.0 legacy `AliasOutput`. + public fun from_legacy_alias_output( + alias_output: AliasOutput, + migration_registry: &mut MigrationRegistry, + ctx: &mut TxContext + ): ControllerCap { + // Extract required data from output. + let (iota, native_tokens, alias_data) = extract_assets(alias_output); + let ( + alias_id, + _, + _, + mut state_metadata, + _, + _, + _, + _, + ) = alias_data.destructure(); + // Check if `state_metadata` contains a DID document. + assert!(is_did_output(state_metadata.borrow()), ENotADidOutput); + let legacy_id = alias_id.to_inner(); + // Destroy alias. + object::delete(alias_id); + + let id = object::new(ctx); + let doc_id = id.to_inner(); + // Create a capability for the governor. + let controller_capability = controller::new(doc_id, ctx); + // Create and share the new DID document. + let document = Document { + id, + iota, + native_tokens, + doc: state_metadata.extract() + }; + share_object(document); + + // Add a migration record. + migration_registry.add(legacy_id, doc_id); + + // Transfer the capability to the governor. + controller_capability + } + + /// Updates the DID document. + public fun update(self: &mut Document, data: vector, controller_capability: &ControllerCap) { + // Check the provided capability is for this document. + assert!(self.id.to_inner() == controller_capability.did(), EInvalidCapability); + // Check `data` is a DID document. + assert!(is_did_output(&data), ENotADidOutput); + self.doc = data; + } + + /// Checks if `data` is a state matadata representing a DID. + /// i.e. starts with the bytes b"DID". + fun is_did_output(data: &vector): bool { + data[0] == 0x44 && // b'D' + data[1] == 0x49 && // b'I' + data[2] == 0x44 // b'D' + } +} \ No newline at end of file diff --git a/identity_sui_name_tbd/packages/identity_iota/sources/migration_registry.move b/identity_sui_name_tbd/packages/identity_iota/sources/migration_registry.move new file mode 100644 index 0000000000..83f31cf933 --- /dev/null +++ b/identity_sui_name_tbd/packages/identity_iota/sources/migration_registry.move @@ -0,0 +1,44 @@ +module identity_iota::migration_registry { + use sui::{dynamic_field as field, transfer::share_object, event}; + + /// One time witness needed to construct a singleton migration registry. + public struct MIGRATION_REGISTRY has drop {} + + + /// Event type that is fired upon creation of a `MigrationRegistry`. + public struct MigrationRegistryCreated has copy, drop { + id: ID + } + + /// Object that tracks migrated alias outputs to their corresponding object IDs. + public struct MigrationRegistry has key { + id: UID, + } + + /// Creates a singleton instance of `MigrationRegistry` when publishing this package. + fun init(_otw: MIGRATION_REGISTRY, ctx: &mut TxContext) { + let id = object::new(ctx); + let registry_id = id.to_inner(); + let registry = MigrationRegistry { + id + }; + share_object(registry); + // Signal the creation of a migration registry. + event::emit(MigrationRegistryCreated { id: registry_id }); + } + + /// Lookup an alias ID into the migration registry. + public fun lookup(self: &MigrationRegistry, alias_id: ID): Option { + if (field::exists_(&self.id, alias_id)) { + let entry = field::borrow(&self.id, alias_id); + option::some(*entry) + } else { + option::none() + } + } + + /// Adds a new Alias ID -> Object ID binding to the regitry. + public(package) fun add(self: &mut MigrationRegistry, alias_id: ID, object_id: ID) { + field::add(&mut self.id, alias_id, object_id); + } +} \ No newline at end of file diff --git a/identity_sui_name_tbd/packages/stardust-test/Move.lock b/identity_sui_name_tbd/packages/stardust-test/Move.lock index 2acba99761..dd3fb3aedc 100644 --- a/identity_sui_name_tbd/packages/stardust-test/Move.lock +++ b/identity_sui_name_tbd/packages/stardust-test/Move.lock @@ -2,7 +2,7 @@ [move] version = 0 -manifest_digest = "76F60CE48787CB754F6A10443A5C5FFA5E4C07364A3E6CAEDECFA81065621CEE" +manifest_digest = "5D20B4D5C9D91B3D962F447225225C662ADD77CA8DB9994269E198B802862C93" deps_digest = "3C4103934B1E040BB6B23F1D610B4EF9F2F1166A50A104EADCF77467C004C600" dependencies = [ @@ -23,6 +23,6 @@ dependencies = [ ] [move.toolchain-version] -compiler-version = "1.24.0" -edition = "2024.beta" +compiler-version = "1.22.0" +edition = "legacy" flavor = "sui" diff --git a/identity_sui_name_tbd/packages/stardust-test/Move.toml b/identity_sui_name_tbd/packages/stardust-test/Move.toml index c88f31fae0..2510fc6f04 100644 --- a/identity_sui_name_tbd/packages/stardust-test/Move.toml +++ b/identity_sui_name_tbd/packages/stardust-test/Move.toml @@ -1,5 +1,5 @@ [package] -name = "Stardust Test" +name = "StardustTest" version = "0.0.1" edition = "2024.beta" diff --git a/identity_sui_name_tbd/packages/stardust-test/sources/alias/alias.move b/identity_sui_name_tbd/packages/stardust-test/sources/alias/alias.move index 79c85428fe..84809c13a0 100644 --- a/identity_sui_name_tbd/packages/stardust-test/sources/alias/alias.move +++ b/identity_sui_name_tbd/packages/stardust-test/sources/alias/alias.move @@ -27,6 +27,20 @@ module stardust::alias { } // === Public-Mutative Functions === + public fun destructure(self: Alias): + (UID, Option
, u32, Option>, Option
, Option>, Option
, Option>) { + let Alias { + id, + legacy_state_controller, + state_index, + state_metadata, + sender, + metadata, + immutable_issuer, + immutable_metadata, + } = self; + (id, legacy_state_controller, state_index, state_metadata, sender, metadata, immutable_issuer, immutable_metadata) + } /// Destroy the `Alias` object, equivalent to `burning` an Alias Output in Stardust. public fun destroy(self: Alias) { @@ -100,8 +114,9 @@ module stardust::alias { immutable_issuer: Option
, immutable_metadata: Option>, ctx: &mut TxContext - ) { - let alias = Alias { + ): Alias + { + Alias { id: object::new(ctx), legacy_state_controller, state_index, @@ -110,7 +125,6 @@ module stardust::alias { metadata, immutable_issuer, immutable_metadata, - }; - transfer::transfer(alias, ctx.sender()); + } } } diff --git a/identity_sui_name_tbd/packages/stardust-test/sources/alias/alias_output.move b/identity_sui_name_tbd/packages/stardust-test/sources/alias/alias_output.move index d7ab2b26bb..2a612592c3 100644 --- a/identity_sui_name_tbd/packages/stardust-test/sources/alias/alias_output.move +++ b/identity_sui_name_tbd/packages/stardust-test/sources/alias/alias_output.move @@ -1,6 +1,8 @@ module stardust::alias_output { + use sui::bag; use sui::bag::Bag; + use sui::balance; use sui::balance::Balance; use sui::dynamic_object_field; use sui::sui::SUI; @@ -12,7 +14,7 @@ module stardust::alias_output { const ALIAS_NAME: vector = b"alias"; /// Owned Object controlled by the Governor Address. - public struct AliasOutput has key { + public struct AliasOutput has key, store { /// This is a "random" UID, not the AliasID from Stardust. id: UID, @@ -64,7 +66,7 @@ module stardust::alias_output { // === Test Functions === - #[test_only] + // #[test_only] public fun create_for_testing( iota: Balance, native_tokens: Bag, @@ -77,7 +79,16 @@ module stardust::alias_output { } } - #[test_only] + // #[test_only] + public fun create_empty_for_testing(ctx: &mut TxContext): AliasOutput { + AliasOutput { + id: object::new(ctx), + iota: balance::zero(), + native_tokens: bag::new(ctx), + } + } + + // #[test_only] public fun attach_alias(output: &mut AliasOutput, alias: Alias) { dynamic_object_field::add(&mut output.id, ALIAS_NAME, alias) } diff --git a/identity_sui_name_tbd/scripts/create_test_alias_output.sh b/identity_sui_name_tbd/scripts/create_test_alias_output.sh index e9054db754..7c95778d0d 100755 --- a/identity_sui_name_tbd/scripts/create_test_alias_output.sh +++ b/identity_sui_name_tbd/scripts/create_test_alias_output.sh @@ -9,28 +9,20 @@ if [ -z "$1" ] exit 1 fi - -# create_for_testing args: -# legacy_state_controller: Option
, -# state_index: u32, -# state_metadata: Option>, -# sender: Option
, -# metadata: Option>, -# immutable_issuer: Option
, -# immutable_metadata: Option>, -# ctx: &mut TxContext -sui \ - client \ - call \ - --package $1 \ - --module alias \ - --function create_for_testing \ - --args \ - [] \ - 123 \ - [] \ - [] \ - [] \ - [] \ - [] \ - --gas-budget 10000000 +sui client ptb \ + --gas-budget 50000000 \ + --move-call sui::tx_context::sender \ + --assign sender \ + --move-call $1::alias::create_for_testing \ + none \ + 123u32 \ + 'some("DIDwhatever")' \ + none \ + none \ + none \ + none \ + --assign "alias" \ + --move-call $1::alias_output::create_empty_for_testing \ + --assign alias_output \ + --move-call $1::alias_output::attach_alias alias_output "alias" \ + --transfer-objects "[alias_output]" sender diff --git a/identity_sui_name_tbd/scripts/migrate_alias_output.sh b/identity_sui_name_tbd/scripts/migrate_alias_output.sh new file mode 100755 index 0000000000..9ab614a5ca --- /dev/null +++ b/identity_sui_name_tbd/scripts/migrate_alias_output.sh @@ -0,0 +1,30 @@ +#!/bin/bash + +# Copyright 2020-2024 IOTA Stiftung +# SPDX-License-Identifier: Apache-2.0 + +if [ -z "$1" ] + then + echo "No arguments supplied, please pass package id as hex string" + exit 1 +fi + +if [ -z "$2" ] + then + echo "pass the address of the alias output you want to migrate" + exit 1 +fi + +if [ -z "$3" ] + then + echo "pass the address of the MigrationRegistry shared object" + exit 1 +fi + +sui client ptb \ + --gas-budget 50000000 \ + --move-call sui::tx_context::sender \ + --assign sender \ + --move-call $1::document::from_legacy_alias_output @$2 @$3 \ + --assign controller_cap \ + --transfer-objects "[controller_cap]" sender diff --git a/identity_sui_name_tbd/src/error.rs b/identity_sui_name_tbd/src/error.rs index 57c9305d3b..33ca1a0126 100644 --- a/identity_sui_name_tbd/src/error.rs +++ b/identity_sui_name_tbd/src/error.rs @@ -20,4 +20,7 @@ pub enum Error { /// could not lookup an object ID #[error("failed to lookup an object; {0}")] ObjectLookup(String), + /// MigrationRegistry error. + #[error(transparent)] + MigrationRegistryNotFound(crate::migration::Error), } diff --git a/identity_sui_name_tbd/src/lib.rs b/identity_sui_name_tbd/src/lib.rs index 86e595ec73..e3f8a12b1f 100644 --- a/identity_sui_name_tbd/src/lib.rs +++ b/identity_sui_name_tbd/src/lib.rs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 mod error; +pub mod migration; pub mod resolution; mod utils; diff --git a/identity_sui_name_tbd/src/migration/mod.rs b/identity_sui_name_tbd/src/migration/mod.rs new file mode 100644 index 0000000000..e65a32d805 --- /dev/null +++ b/identity_sui_name_tbd/src/migration/mod.rs @@ -0,0 +1,6 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +mod registry; + +pub use registry::*; diff --git a/identity_sui_name_tbd/src/migration/registry.rs b/identity_sui_name_tbd/src/migration/registry.rs new file mode 100644 index 0000000000..9800d1fc90 --- /dev/null +++ b/identity_sui_name_tbd/src/migration/registry.rs @@ -0,0 +1,88 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use std::sync::OnceLock; + +use serde::Deserialize; +use sui_sdk::rpc_types::EventFilter; +use sui_sdk::rpc_types::SuiData; +use sui_sdk::types::base_types::ObjectID; +use sui_sdk::SuiClient; + +static MIGRATION_REGISTRY_ID: OnceLock = OnceLock::new(); + +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error(transparent)] + ClientError(anyhow::Error), + #[error("could not locate MigrationRegistry object: {0}")] + NotFound(String), + #[error("malformed MigrationRegistry's entry: {0}")] + Malformed(String), +} + +#[derive(Deserialize)] +struct MigrationRegistryCreatedEvent { + #[allow(dead_code)] + id: ObjectID, +} + +async fn migration_registry_id(sui_client: &SuiClient) -> Result { + if let Some(registry_id) = MIGRATION_REGISTRY_ID.get() { + return Ok(*registry_id); + } + + let event_tag = { + let package_id = std::env::var("IDENTITY_IOTA_PKG_ID").expect("set IDENTITY_IOTA_PKG_ID"); + let tag = format!("{package_id}::migration_registry::MigrationRegistryCreated"); + sui_sdk::types::parse_sui_struct_tag(&tag) + .map_err(|e| Error::NotFound(format!("invalid event tag \"{tag}\": {}", e)))? + }; + + let mut returned_events = sui_client + .event_api() + .query_events(EventFilter::MoveEventType(event_tag), None, Some(1), false) + .await + .map_err(|e| Error::ClientError(e.into()))? + .data; + let event = if !returned_events.is_empty() { + returned_events.swap_remove(0) + } else { + return Err(Error::NotFound(String::from("no \"MigrationRegistryCreated\" event"))); + }; + + let registry_id = serde_json::from_value::(event.parsed_json) + .map(|e| e.id) + .map_err(|e| Error::NotFound(format!("Malformed \"MigrationRegistryEvent\": {}", e)))?; + + let _ = MIGRATION_REGISTRY_ID.set(registry_id); + Ok(registry_id) +} + +/// Lookup a legacy `alias_id` into the migration registry +/// to get the UID of the corresponding migrated DID document if any. +pub async fn lookup(sui_client: &SuiClient, alias_id: ObjectID) -> Result, Error> { + let dynamic_field_name = serde_json::from_value(serde_json::json!({ + "type": "0x2::object::ID", + "value": alias_id.to_string() + })) + .expect("valid move value"); + + sui_client + .read_api() + .get_dynamic_field_object(migration_registry_id(sui_client).await?, dynamic_field_name) + .await + .map_err(|e| Error::ClientError(e.into()))? + .data + .map(|data| { + data + .content + .and_then(|content| content.try_into_move()) + .and_then(|move_object| move_object.read_dynamic_field_value("value")) + .and_then(|address_value| serde_json::from_value(address_value.to_json_value()).ok()) + .ok_or(Error::Malformed( + "invalid MigrationRegistry's Entry encoding".to_string(), + )) + }) + .transpose() +} diff --git a/identity_sui_name_tbd/src/resolution/unmigrated_resolver.rs b/identity_sui_name_tbd/src/resolution/unmigrated_resolver.rs index 88fef16a25..4999b5e542 100644 --- a/identity_sui_name_tbd/src/resolution/unmigrated_resolver.rs +++ b/identity_sui_name_tbd/src/resolution/unmigrated_resolver.rs @@ -57,16 +57,13 @@ impl UnmigratedResolver { .content .ok_or_else(|| Error::ObjectLookup(format!("no content in retrieved object in object id {object_id}")))?; - let object = match content { - SuiParsedData::MoveObject(value) => value, - _ => { - return Err(Error::ObjectLookup(format!( - "found data at object id {object_id} is not an object" - ))); - } + let SuiParsedData::MoveObject(value) = content else { + return Err(Error::ObjectLookup(format!( + "found data at object id {object_id} is not an object" + ))); }; - let alias_output = object.fields.to_json_value(); + let alias_output = value.fields.to_json_value(); Ok(alias_output) } From e49661d14ea5f0ceca74ae25b60c08c12cbc3451 Mon Sep 17 00:00:00 2001 From: Sebastian Wolfram Date: Thu, 16 May 2024 09:55:22 +0200 Subject: [PATCH 03/62] add signing and tx tests --- identity_sui_name_tbd/Cargo.toml | 8 +- identity_sui_name_tbd/src/lib.rs | 2 +- identity_sui_name_tbd/tests/test.rs | 170 ++++++++++++++++++++++++++++ 3 files changed, 178 insertions(+), 2 deletions(-) diff --git a/identity_sui_name_tbd/Cargo.toml b/identity_sui_name_tbd/Cargo.toml index 791d21a494..a1fdabb753 100644 --- a/identity_sui_name_tbd/Cargo.toml +++ b/identity_sui_name_tbd/Cargo.toml @@ -12,14 +12,20 @@ rust-version.workspace = true description = "SUI related tooling for identity-rs" [dependencies] +identity_stronghold = { path = "../identity_stronghold", default-features = false } +iota-sdk = { version = "1.0", default-features = false, features = ["tls", "client", "stronghold"] } serde.workspace = true serde_json.workspace = true strum.workspace = true -sui-sdk = { git = "https://github.com/mystenlabs/sui", package = "sui-sdk"} +sui-sdk = { git = "https://github.com/iotaledger/kinesis.git", package = "sui-sdk" } thiserror.workspace = true anyhow = "1.0.75" [dev-dependencies] +anyhow = "1.0.75" +bcs = "0.1.4" +fastcrypto = { git = "https://github.com/MystenLabs/fastcrypto", rev = "d7a33a9f79271bfc19fc4c8816ea5467e4205e17" } +shared-crypto = { git = "https://github.com/iotaledger/kinesis.git", package = "shared-crypto" } tokio = { version = "1.29.0", default-features = false, features = ["macros", "sync", "rt"] } [lints] diff --git a/identity_sui_name_tbd/src/lib.rs b/identity_sui_name_tbd/src/lib.rs index e3f8a12b1f..1e4c849da7 100644 --- a/identity_sui_name_tbd/src/lib.rs +++ b/identity_sui_name_tbd/src/lib.rs @@ -4,6 +4,6 @@ mod error; pub mod migration; pub mod resolution; -mod utils; +pub mod utils; pub use error::Error; diff --git a/identity_sui_name_tbd/tests/test.rs b/identity_sui_name_tbd/tests/test.rs index d00dcf9b46..ce4e6db7ab 100644 --- a/identity_sui_name_tbd/tests/test.rs +++ b/identity_sui_name_tbd/tests/test.rs @@ -1,8 +1,60 @@ // Copyright 2020-2024 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 +use std::path::PathBuf; +use std::str::FromStr; + +use fastcrypto::hash::HashFunction; +use fastcrypto::traits::ToFromBytes; use identity_sui_name_tbd::resolution::UnmigratedResolver; use identity_sui_name_tbd::resolution::LOCAL_NETWORK; +use identity_sui_name_tbd::utils::get_client; +use iota_sdk::client::secret::stronghold::StrongholdSecretManager; +use iota_sdk::client::secret::SecretManage; +use iota_sdk::crypto::keys::bip39::Mnemonic; +use iota_sdk::crypto::keys::bip44::Bip44; +use shared_crypto::intent::Intent; +use shared_crypto::intent::IntentMessage; +use sui_sdk::rpc_types::SuiTransactionBlockResponseOptions; +use sui_sdk::types::base_types::ObjectID; +use sui_sdk::types::base_types::SuiAddress; +use sui_sdk::types::crypto::DefaultHash; +use sui_sdk::types::crypto::Signature; +use sui_sdk::types::crypto::SignatureScheme; +use sui_sdk::types::quorum_driver_types::ExecuteTransactionRequestType; +use sui_sdk::types::transaction::Transaction; +use sui_sdk::types::transaction::TransactionData; + +const ACCOUNT_INDEX: u32 = 0; +const INTERNAL_ADDRESS: bool = false; +const ADDRESS_INDEX: u32 = 0; +const TEST_MNEMONIC: &str = + "result crisp session latin must fruit genuine question prevent start coconut brave speak student dismiss"; + +/// Creates a stronghold path in the temporary directory, whose exact location is OS-dependent. +pub fn stronghold_path() -> PathBuf { + let mut file = std::env::temp_dir(); + file.push("test_strongholds"); + file.push("001"); + file.set_extension("stronghold"); + file.to_owned() +} + +// must be done and can only be done once to import test mnemonic +#[tokio::test] +#[ignore] +async fn can_import_the_test_mnemonic() -> anyhow::Result<()> { + let stronghold_secret_manager = StrongholdSecretManager::builder() + .password("secure_password".to_string()) + .build("test.stronghold") + .expect("Failed to create temporary stronghold"); + + stronghold_secret_manager + .store_mnemonic(Mnemonic::from(TEST_MNEMONIC)) + .await?; + + Ok(()) +} #[tokio::test] async fn can_initialize_resolver_for_unmigrated_alias_outputs() -> anyhow::Result<()> { @@ -14,6 +66,7 @@ async fn can_initialize_resolver_for_unmigrated_alias_outputs() -> anyhow::Resul } #[tokio::test] +#[ignore] async fn can_fetch_alias_output_by_object_id() -> anyhow::Result<()> { let resolver = UnmigratedResolver::new(LOCAL_NETWORK).await?; let result = resolver @@ -25,3 +78,120 @@ async fn can_fetch_alias_output_by_object_id() -> anyhow::Result<()> { Ok(()) } + +#[tokio::test] +async fn can_sign_a_message() -> anyhow::Result<()> { + let stronghold_secret_manager = StrongholdSecretManager::builder() + .password("secure_password".to_string()) + .build("test.stronghold") + .expect("Failed to create temporary stronghold"); + + // stronghold_secret_manager + // .store_mnemonic(Mnemonic::from(TEST_MNEMONIC)) + // .await?; + + // serialized tx + let data: &[u8] = &[ + 0, 0, 2, 0, 8, 16, 39, 0, 0, 0, 0, 0, 0, 0, 32, 99, 128, 244, 235, 93, 122, 247, 240, 204, 187, 233, 12, 112, 87, + 11, 181, 255, 12, 156, 255, 214, 241, 218, 171, 221, 98, 11, 202, 210, 215, 253, 16, 2, 2, 0, 1, 1, 0, 0, 1, 1, 3, + 0, 0, 0, 0, 1, 1, 0, 115, 166, 179, 195, 62, 45, 99, 56, 61, 229, 198, 120, 108, 186, 202, 35, 31, 247, 137, 244, + 200, 83, 175, 109, 84, 203, 136, 61, 135, 128, 173, 192, 2, 86, 67, 123, 247, 153, 252, 197, 226, 203, 38, 161, 86, + 142, 90, 239, 112, 39, 9, 77, 143, 4, 87, 199, 140, 127, 174, 79, 156, 223, 83, 73, 240, 0, 0, 0, 0, 0, 0, 0, 0, + 32, 180, 177, 115, 103, 130, 89, 100, 56, 193, 53, 145, 165, 23, 70, 130, 185, 100, 51, 64, 35, 67, 10, 247, 136, + 16, 45, 57, 47, 146, 205, 41, 253, 86, 67, 123, 247, 153, 252, 197, 226, 203, 38, 161, 86, 142, 90, 239, 112, 39, + 9, 77, 143, 4, 87, 199, 140, 127, 174, 79, 156, 223, 83, 73, 240, 0, 0, 0, 0, 0, 0, 0, 0, 32, 180, 177, 115, 103, + 130, 89, 100, 56, 193, 53, 145, 165, 23, 70, 130, 185, 100, 51, 64, 35, 67, 10, 247, 136, 16, 45, 57, 47, 146, 205, + 41, 253, 115, 166, 179, 195, 62, 45, 99, 56, 61, 229, 198, 120, 108, 186, 202, 35, 31, 247, 137, 244, 200, 83, 175, + 109, 84, 203, 136, 61, 135, 128, 173, 192, 1, 0, 0, 0, 0, 0, 0, 0, 16, 39, 0, 0, 0, 0, 0, 0, 0, + ]; + + // build intent message to sign + let msg: TransactionData = bcs::from_bytes(data)?; + let intent = Intent::sui_transaction(); + let intent_msg = IntentMessage::new(intent, msg); + let mut hasher = DefaultHash::default(); + hasher.update(bcs::to_bytes(&intent_msg)?); + let digest = hasher.finalize().digest; + + // sign with sui m/44'/784'/0'/0'/0' + let bip44_chain = Bip44::new(784) + .with_account(ACCOUNT_INDEX) + .with_change(INTERNAL_ADDRESS as _) + .with_address_index(ADDRESS_INDEX); + let signed = stronghold_secret_manager.sign_ed25519(&digest, bip44_chain).await?; + println!("signed - sui config: {:?}", &signed); + + dbg!(&signed); + + Ok(()) +} + +#[tokio::test] +async fn can_submit_a_tx() -> anyhow::Result<()> { + let client = get_client(LOCAL_NETWORK).await?; + + let stronghold_secret_manager = StrongholdSecretManager::builder() + .password("secure_password".to_string()) + .build("test.stronghold") + .expect("Failed to create temporary stronghold"); + + let sender = SuiAddress::from_str("0x936accb491f0facaac668baaedcf4d0cfc6da1120b66f77fa6a43af718669973")?; + let get_flag_call = client + .transaction_builder() + .move_call( + sender, // account + ObjectID::from_str("0xfc5a7684cb42742fc0d88b4224b02ece1f971fe9fbac4ab620df831ff928e1ad")?, // p id + "checkin", // module + "get_flag", // fn + vec![], + vec![], + None, // The node will pick a gas object belong to the signer if not provided. + 10000000, + None, + ) + .await?; + dbg!(&get_flag_call); + + // build intent message to sign + let intent = Intent::sui_transaction(); + let intent_msg = IntentMessage::new(intent, &get_flag_call); + let mut hasher = DefaultHash::default(); + hasher.update(bcs::to_bytes(&intent_msg)?); + let digest = hasher.finalize().digest; + + // sign with sui m/44'/784'/0'/0'/0' + let bip44_chain = Bip44::new(784) + .with_account(ACCOUNT_INDEX) + .with_change(INTERNAL_ADDRESS as _) + .with_address_index(ADDRESS_INDEX); + let signed = stronghold_secret_manager.sign_ed25519(&digest, bip44_chain).await?; + println!("signed - sui config: {:?}", &signed); + + dbg!(&signed); + + // convert to sui signature object + let binding = [ + [SignatureScheme::ED25519.flag()].as_slice(), + signed.signature().to_bytes().as_slice(), + signed.public_key_bytes().to_bytes().as_slice(), + ] + .concat(); + let signature_bytes: &[u8] = binding.as_slice(); + + let signature = Signature::from_bytes(signature_bytes)?; + + let response = client + .quorum_driver_api() + .execute_transaction_block( + Transaction::from_data(get_flag_call, vec![signature]), + SuiTransactionBlockResponseOptions::full_content(), + Some(ExecuteTransactionRequestType::WaitForLocalExecution), + ) + .await?; + + dbg!(&response); + + dbg!(&response.events); + + Ok(()) +} From 9feb1e189c399606b50d9a86521dfe4eafbaa88c Mon Sep 17 00:00:00 2001 From: wulfraem Date: Mon, 27 May 2024 11:09:20 +0200 Subject: [PATCH 04/62] Feat/add did resolving * add initial resolving for migrated and undmigrated alias did docs * fix issues after merge conflicts --- .../0_basic/2b_resolve_did_via_kinesis.rs | 35 ++++++++ examples/Cargo.toml | 5 ++ identity_iota_core/Cargo.toml | 3 + .../src/client/kinesis_identity_client.rs | 75 +++++++++++++++++ identity_iota_core/src/client/mod.rs | 4 + identity_iota_core/src/error.rs | 3 + identity_resolver/src/resolution/resolver.rs | 18 ++++ identity_sui_name_tbd/Cargo.toml | 1 - .../packages/identity_iota/Move.lock | 2 +- .../packages/identity_iota/Move.toml | 1 + .../packages/stardust-test/Move.lock | 2 +- .../packages/stardust-test/Move.toml | 3 +- .../stardust-test/sources/alias/alias.move | 23 +++++ .../scripts/create_test_alias_output.sh | 6 +- .../scripts/migrate_alias_output.sh | 2 +- .../scripts/publish_identity_package.sh | 11 +++ identity_sui_name_tbd/src/lib.rs | 1 - identity_sui_name_tbd/src/migration/alias.rs | 84 +++++++++++++++++++ .../src/migration/document.rs | 65 ++++++++++++++ identity_sui_name_tbd/src/migration/mod.rs | 4 + .../src/migration/registry.rs | 1 + identity_sui_name_tbd/src/resolution/mod.rs | 6 -- .../src/resolution/unmigrated_resolver.rs | 70 ---------------- identity_sui_name_tbd/src/signing/mod.rs | 52 ++++++++++++ identity_sui_name_tbd/src/utils.rs | 2 + identity_sui_name_tbd/tests/test.rs | 19 +++-- 26 files changed, 406 insertions(+), 92 deletions(-) create mode 100644 examples/0_basic/2b_resolve_did_via_kinesis.rs create mode 100644 identity_iota_core/src/client/kinesis_identity_client.rs create mode 100755 identity_sui_name_tbd/scripts/publish_identity_package.sh create mode 100644 identity_sui_name_tbd/src/migration/alias.rs create mode 100644 identity_sui_name_tbd/src/migration/document.rs delete mode 100644 identity_sui_name_tbd/src/resolution/mod.rs delete mode 100644 identity_sui_name_tbd/src/resolution/unmigrated_resolver.rs create mode 100644 identity_sui_name_tbd/src/signing/mod.rs diff --git a/examples/0_basic/2b_resolve_did_via_kinesis.rs b/examples/0_basic/2b_resolve_did_via_kinesis.rs new file mode 100644 index 0000000000..d58248f12c --- /dev/null +++ b/examples/0_basic/2b_resolve_did_via_kinesis.rs @@ -0,0 +1,35 @@ +// Copyright 2020-2023 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use anyhow::anyhow; +use identity_iota::iota::IotaDID; +use identity_iota::iota::IotaDocument; +use identity_iota::prelude::Resolver; +use sui_sdk::SuiClientBuilder; + +/// Demonstrates how to resolve an existing DID in an Alias Output. +#[tokio::main] +async fn main() -> anyhow::Result<()> { + let client = SuiClientBuilder::default() + .build("http://127.0.0.1:9000") + .await + .map_err(|err| anyhow!(format!("failed to connect to network; {}", err)))?; + + // We can also create a `Resolver` that has additional convenience methods, + // for example to resolve presentation issuers or to verify presentations. + let mut resolver = Resolver::::new(); + + // We need to register a handler that can resolve IOTA DIDs. + // This convenience method only requires us to provide a client. + resolver.attach_kinesis_iota_handler(client.clone()); + + // let did = IotaDID::parse("did:iota:0x737794842572ee0a98ff46b2aadf9219de707998bd9f767a9b24b82ff9d437c8")?; // a + let did = IotaDID::parse("did:iota:0x439308c50ba8ac17972dd595c4cb6866e5721ddcc63d6ab0e9749d4c3a777cb2")?; // a (mig) + let result = resolver.resolve(&did).await; + + dbg!(&result); + + assert!(result.is_ok()); + + Ok(()) +} diff --git a/examples/Cargo.toml b/examples/Cargo.toml index e91dad0eca..a7e550bfa5 100644 --- a/examples/Cargo.toml +++ b/examples/Cargo.toml @@ -15,6 +15,7 @@ primitive-types = "0.12.1" rand = "0.8.5" sd-jwt-payload = { version = "0.2.1", default-features = false, features = ["sha"] } serde_json = { version = "1.0", default-features = false } +sui-sdk = { git = "https://github.com/iotaledger/kinesis.git", package = "sui-sdk" } tokio = { version = "1.29", default-features = false, features = ["rt"] } [lib] @@ -32,6 +33,10 @@ name = "1_update_did" path = "0_basic/2_resolve_did.rs" name = "2_resolve_did" +[[example]] +path = "0_basic/2b_resolve_did_via_kinesis.rs" +name = "2b_resolve_did_via_kinesis" + [[example]] path = "0_basic/3_deactivate_did.rs" name = "3_deactivate_did" diff --git a/identity_iota_core/Cargo.toml b/identity_iota_core/Cargo.toml index 4e55ae36f4..d38562fcc8 100644 --- a/identity_iota_core/Cargo.toml +++ b/identity_iota_core/Cargo.toml @@ -18,6 +18,7 @@ identity_core = { version = "=1.2.0", path = "../identity_core", default-feature identity_credential = { version = "=1.2.0", path = "../identity_credential", default-features = false, features = ["validator"] } identity_did = { version = "=1.2.0", path = "../identity_did", default-features = false } identity_document = { version = "=1.2.0", path = "../identity_document", default-features = false } +identity_sui_name_tbd = { path = "../identity_sui_name_tbd" } identity_verification = { version = "=1.2.0", path = "../identity_verification", default-features = false } iota-sdk = { version = "1.0.2", default-features = false, features = ["serde", "std"], optional = true } num-derive = { version = "0.4", default-features = false } @@ -26,6 +27,8 @@ once_cell = { version = "1.18", default-features = false, features = ["std"] } prefix-hex = { version = "0.7", default-features = false } ref-cast = { version = "1.0.14", default-features = false } serde.workspace = true +serde_json.workspace = true +sui-sdk = { git = "https://github.com/iotaledger/kinesis.git", package = "sui-sdk" } strum.workspace = true thiserror.workspace = true diff --git a/identity_iota_core/src/client/kinesis_identity_client.rs b/identity_iota_core/src/client/kinesis_identity_client.rs new file mode 100644 index 0000000000..6cdc9a1234 --- /dev/null +++ b/identity_iota_core/src/client/kinesis_identity_client.rs @@ -0,0 +1,75 @@ +// Copyright 2020-2023 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use std::str::FromStr; + +use identity_sui_name_tbd::migration::get_alias; +use identity_sui_name_tbd::migration::get_identity_document; +use identity_sui_name_tbd::migration::lookup; +use iota_sdk::types::block::output::AliasId; +use sui_sdk::types::base_types::ObjectID; +use sui_sdk::SuiClient; + +use crate::Error; +use crate::IotaDID; +use crate::IotaDocument; +use crate::Result; +use crate::StateMetadataDocument; + +/// An extension trait that provides helper functions for resolution of DID documents in unmigrated Alias Outputs and +/// migrated identity document. +#[cfg_attr(feature = "send-sync-client-ext", async_trait::async_trait)] +#[cfg_attr(not(feature = "send-sync-client-ext"), async_trait::async_trait(?Send))] +pub trait KinesisIotaIdentityClientExt { + /// Resolve a [`IotaDocument`]. + async fn resolve_did(&self, did: &IotaDID) -> Result; +} + +#[cfg_attr(feature = "send-sync-client-ext", async_trait::async_trait)] +#[cfg_attr(not(feature = "send-sync-client-ext"), async_trait::async_trait(?Send))] +impl KinesisIotaIdentityClientExt for SuiClient { + async fn resolve_did(&self, did: &IotaDID) -> Result { + // get alias id from did (starting with 0x) + let alias_id: AliasId = AliasId::from(did); + let alias_id_string = alias_id.to_string(); + + // try to resolve unmigrated alias (stardust `Alias` object) + let unmigrated_alias = get_alias(self, &alias_id_string).await.map_err(|err| { + Error::DIDResolutionErrorKinesis(format!("could no query for alias output {alias_id_string}; {err}")) + })?; + let state_metadata = if let Some(unmigrated_alias_value) = unmigrated_alias { + // if we found an unmigrated alias, fetch state metadata / serialized document from it + unmigrated_alias_value + .state_metadata + .ok_or_else(|| Error::DIDResolutionErrorKinesis("alias state metadata must not be empty".to_string()))? + } else { + // otherwise check registry for a migrated alias + let object_id = ObjectID::from_str(&alias_id_string) + .map_err(|_| Error::DIDSyntaxError(identity_did::Error::InvalidMethodId))?; + let mapped_id = lookup(self, object_id).await.map_err(|err| { + Error::DIDResolutionErrorKinesis(format!("failed to look up alias id in migration registry; {err}")) + })?; + // if we found a mapping, resolve to identity package `Document` object + let document = if let Some(mapped_id_value) = mapped_id { + let mapped_id_value_string = mapped_id_value.to_string(); + get_identity_document(self, &mapped_id_value_string) + .await + .map_err(|err| Error::DIDResolutionErrorKinesis(format!("failed to resolve identity document; {err}")))? + .ok_or_else(|| { + Error::DIDResolutionErrorKinesis(format!( + "no identity document found for mapped id value {mapped_id_value_string}" + )) + })? + } else { + return Err(Error::DIDResolutionErrorKinesis(format!( + "could not find alias id {alias_id_string} in migration registry" + ))); + }; + // and get state metadata / serialized document from it + document.doc + }; + + // unpack and return document + return StateMetadataDocument::unpack(&state_metadata).and_then(|doc| doc.into_iota_document(did)); + } +} diff --git a/identity_iota_core/src/client/mod.rs b/identity_iota_core/src/client/mod.rs index b1cfb4b17f..5a4cabe9e9 100644 --- a/identity_iota_core/src/client/mod.rs +++ b/identity_iota_core/src/client/mod.rs @@ -6,7 +6,11 @@ pub use identity_client::IotaIdentityClientExt; #[cfg(feature = "iota-client")] pub use self::iota_client::IotaClientExt; +#[cfg(feature = "iota-client")] +pub use self::kinesis_identity_client::KinesisIotaIdentityClientExt; mod identity_client; #[cfg(feature = "iota-client")] mod iota_client; +#[cfg(feature = "iota-client")] +mod kinesis_identity_client; diff --git a/identity_iota_core/src/error.rs b/identity_iota_core/src/error.rs index 2a5e16ef34..c9065f8f4d 100644 --- a/identity_iota_core/src/error.rs +++ b/identity_iota_core/src/error.rs @@ -25,6 +25,9 @@ pub enum Error { /// Caused by a client failure during resolution. #[error("DID resolution failed")] DIDResolutionError(#[source] iota_sdk::client::error::Error), + /// Caused by a look failures during resolution. + #[error("DID resolution failed: {0}")] + DIDResolutionErrorKinesis(String), #[cfg(feature = "iota-client")] /// Caused by an error when building a basic output. #[error("basic output build error")] diff --git a/identity_resolver/src/resolution/resolver.rs b/identity_resolver/src/resolution/resolver.rs index b8ceffbc7f..538563198b 100644 --- a/identity_resolver/src/resolution/resolver.rs +++ b/identity_resolver/src/resolution/resolver.rs @@ -256,6 +256,7 @@ mod iota_handler { use identity_iota_core::IotaDID; use identity_iota_core::IotaDocument; use identity_iota_core::IotaIdentityClientExt; + use identity_iota_core::KinesisIotaIdentityClientExt; use std::collections::HashMap; use std::sync::Arc; @@ -280,6 +281,23 @@ mod iota_handler { self.attach_handler(IotaDID::METHOD.to_owned(), handler); } + /// Convenience method for attaching a new handler responsible for resolving IOTA DIDs via kinesis. + /// + /// See also [`attach_handler`](Self::attach_handler) + pub fn attach_kinesis_iota_handler(&mut self, client: CLI) + where + CLI: KinesisIotaIdentityClientExt + Send + Sync + 'static, + { + let arc_client: Arc = Arc::new(client); + + let handler = move |did: IotaDID| { + let future_client = arc_client.clone(); + async move { future_client.resolve_did(&did).await } + }; + + self.attach_handler(IotaDID::METHOD.to_owned(), handler); + } + /// Convenience method for attaching multiple handlers responsible for resolving IOTA DIDs /// on multiple networks. /// diff --git a/identity_sui_name_tbd/Cargo.toml b/identity_sui_name_tbd/Cargo.toml index a1fdabb753..57aad95b71 100644 --- a/identity_sui_name_tbd/Cargo.toml +++ b/identity_sui_name_tbd/Cargo.toml @@ -22,7 +22,6 @@ thiserror.workspace = true anyhow = "1.0.75" [dev-dependencies] -anyhow = "1.0.75" bcs = "0.1.4" fastcrypto = { git = "https://github.com/MystenLabs/fastcrypto", rev = "d7a33a9f79271bfc19fc4c8816ea5467e4205e17" } shared-crypto = { git = "https://github.com/iotaledger/kinesis.git", package = "shared-crypto" } diff --git a/identity_sui_name_tbd/packages/identity_iota/Move.lock b/identity_sui_name_tbd/packages/identity_iota/Move.lock index d182f7c848..19a8ecb42b 100644 --- a/identity_sui_name_tbd/packages/identity_iota/Move.lock +++ b/identity_sui_name_tbd/packages/identity_iota/Move.lock @@ -2,7 +2,7 @@ [move] version = 0 -manifest_digest = "5302BCFBECBC30D467D03B2D18F53E3EE90CFEB854C9E73696E0BE70EF064980" +manifest_digest = "074E59DE585EC1DC8A5252DBFB83A9505160E22B3A46DB9A4A43698C6A5A8818" deps_digest = "060AD7E57DFB13104F21BE5F5C3759D03F0553FC3229247D9A7A6B45F50D03A3" dependencies = [ diff --git a/identity_sui_name_tbd/packages/identity_iota/Move.toml b/identity_sui_name_tbd/packages/identity_iota/Move.toml index a5bd3cb7e7..2caecdd12b 100644 --- a/identity_sui_name_tbd/packages/identity_iota/Move.toml +++ b/identity_sui_name_tbd/packages/identity_iota/Move.toml @@ -9,6 +9,7 @@ StardustTest = { local = "../stardust-test" } [addresses] identity_iota = "0x0" +stardust = "0xd2ee1f1775868aed0e8375a7469624e3c39a559b62ab92dddad99544c1d90565" [dev-dependencies] diff --git a/identity_sui_name_tbd/packages/stardust-test/Move.lock b/identity_sui_name_tbd/packages/stardust-test/Move.lock index dd3fb3aedc..4289e66f9e 100644 --- a/identity_sui_name_tbd/packages/stardust-test/Move.lock +++ b/identity_sui_name_tbd/packages/stardust-test/Move.lock @@ -2,7 +2,7 @@ [move] version = 0 -manifest_digest = "5D20B4D5C9D91B3D962F447225225C662ADD77CA8DB9994269E198B802862C93" +manifest_digest = "8E14974C460B81B94E56B319A03957D7126B98F7AAD0364C36333DB04C09DCD8" deps_digest = "3C4103934B1E040BB6B23F1D610B4EF9F2F1166A50A104EADCF77467C004C600" dependencies = [ diff --git a/identity_sui_name_tbd/packages/stardust-test/Move.toml b/identity_sui_name_tbd/packages/stardust-test/Move.toml index 2510fc6f04..da068ef782 100644 --- a/identity_sui_name_tbd/packages/stardust-test/Move.toml +++ b/identity_sui_name_tbd/packages/stardust-test/Move.toml @@ -2,6 +2,7 @@ name = "StardustTest" version = "0.0.1" edition = "2024.beta" +published-at = "0xd2ee1f1775868aed0e8375a7469624e3c39a559b62ab92dddad99544c1d90565" [dependencies] # as we don't have the tag, reference to the respective commit hash @@ -9,4 +10,4 @@ MoveStdlib = { git = "https://github.com/iotaledger/kinesis.git", subdir = "crat Sui = { git = "https://github.com/iotaledger/kinesis.git", subdir = "crates/sui-framework/packages/sui-framework", rev = "7899dc9ce682c3d0a97f249ce7eaa27b9473b920" } [addresses] -stardust = "0x0" +stardust = "0xd2ee1f1775868aed0e8375a7469624e3c39a559b62ab92dddad99544c1d90565" \ No newline at end of file diff --git a/identity_sui_name_tbd/packages/stardust-test/sources/alias/alias.move b/identity_sui_name_tbd/packages/stardust-test/sources/alias/alias.move index 84809c13a0..ac2deef92a 100644 --- a/identity_sui_name_tbd/packages/stardust-test/sources/alias/alias.move +++ b/identity_sui_name_tbd/packages/stardust-test/sources/alias/alias.move @@ -127,4 +127,27 @@ module stardust::alias { immutable_metadata, } } + + public fun create_with_state_metadata_for_testing( + legacy_state_controller: Option
, + state_index: u32, + state_metadata: vector, + sender: Option
, + metadata: Option>, + immutable_issuer: Option
, + immutable_metadata: Option>, + ctx: &mut TxContext + ): Alias + { + Alias { + id: object::new(ctx), + legacy_state_controller, + state_index, + state_metadata: option::some(state_metadata), + sender, + metadata, + immutable_issuer, + immutable_metadata, + } + } } diff --git a/identity_sui_name_tbd/scripts/create_test_alias_output.sh b/identity_sui_name_tbd/scripts/create_test_alias_output.sh index 7c95778d0d..eb766493aa 100755 --- a/identity_sui_name_tbd/scripts/create_test_alias_output.sh +++ b/identity_sui_name_tbd/scripts/create_test_alias_output.sh @@ -13,10 +13,14 @@ sui client ptb \ --gas-budget 50000000 \ --move-call sui::tx_context::sender \ --assign sender \ + --make-move-vec "" "[68u8, 73u8, 68u8, 1u8, 0u8, 131u8, 1u8, 123u8, 34u8, 100u8, 111u8, 99u8, 34u8, 58u8, 123u8, 34u8, 105u8, 100u8, 34u8, 58u8, 34u8, 100u8, 105u8, 100u8, 58u8, 48u8, 58u8, 48u8, 34u8, 44u8, 34u8, 118u8, 101u8, 114u8, 105u8, 102u8, 105u8, 99u8, 97u8, 116u8, 105u8, 111u8, 110u8, 77u8, 101u8, 116u8, 104u8, 111u8, 100u8, 34u8, 58u8, 91u8, 123u8, 34u8, 105u8, 100u8, 34u8, 58u8, 34u8, 100u8, 105u8, 100u8, 58u8, 48u8, 58u8, 48u8, 35u8, 79u8, 115u8, 55u8, 95u8, 66u8, 100u8, 74u8, 120u8, 113u8, 86u8, 119u8, 101u8, 76u8, 107u8, 56u8, 73u8, 87u8, 45u8, 76u8, 71u8, 83u8, 111u8, 52u8, 95u8, 65u8, 115u8, 52u8, 106u8, 70u8, 70u8, 86u8, 113u8, 100u8, 108u8, 74u8, 73u8, 99u8, 48u8, 45u8, 100u8, 50u8, 49u8, 73u8, 34u8, 44u8, 34u8, 99u8, 111u8, 110u8, 116u8, 114u8, 111u8, 108u8, 108u8, 101u8, 114u8, 34u8, 58u8, 34u8, 100u8, 105u8, 100u8, 58u8, 48u8, 58u8, 48u8, 34u8, 44u8, 34u8, 116u8, 121u8, 112u8, 101u8, 34u8, 58u8, 34u8, 74u8, 115u8, 111u8, 110u8, 87u8, 101u8, 98u8, 75u8, 101u8, 121u8, 34u8, 44u8, 34u8, 112u8, 117u8, 98u8, 108u8, 105u8, 99u8, 75u8, 101u8, 121u8, 74u8, 119u8, 107u8, 34u8, 58u8, 123u8, 34u8, 107u8, 116u8, 121u8, 34u8, 58u8, 34u8, 79u8, 75u8, 80u8, 34u8, 44u8, 34u8, 97u8, 108u8, 103u8, 34u8, 58u8, 34u8, 69u8, 100u8, 68u8, 83u8, 65u8, 34u8, 44u8, 34u8, 107u8, 105u8, 100u8, 34u8, 58u8, 34u8, 79u8, 115u8, 55u8, 95u8, 66u8, 100u8, 74u8, 120u8, 113u8, 86u8, 119u8, 101u8, 76u8, 107u8, 56u8, 73u8, 87u8, 45u8, 76u8, 71u8, 83u8, 111u8, 52u8, 95u8, 65u8, 115u8, 52u8, 106u8, 70u8, 70u8, 86u8, 113u8, 100u8, 108u8, 74u8, 73u8, 99u8, 48u8, 45u8, 100u8, 50u8, 49u8, 73u8, 34u8, 44u8, 34u8, 99u8, 114u8, 118u8, 34u8, 58u8, 34u8, 69u8, 100u8, 50u8, 53u8, 53u8, 49u8, 57u8, 34u8, 44u8, 34u8, 120u8, 34u8, 58u8, 34u8, 75u8, 119u8, 99u8, 54u8, 89u8, 105u8, 121u8, 121u8, 65u8, 71u8, 79u8, 103u8, 95u8, 80u8, 116u8, 118u8, 50u8, 95u8, 49u8, 67u8, 80u8, 71u8, 52u8, 98u8, 86u8, 87u8, 54u8, 102u8, 89u8, 76u8, 80u8, 83u8, 108u8, 115u8, 57u8, 112u8, 122u8, 122u8, 99u8, 78u8, 67u8, 67u8, 77u8, 34u8, 125u8, 125u8, 93u8, 125u8, 44u8, 34u8, 109u8, 101u8, 116u8, 97u8, 34u8, 58u8, 123u8, 34u8, 99u8, 114u8, 101u8, 97u8, 116u8, 101u8, 100u8, 34u8, 58u8, 34u8, 50u8, 48u8, 50u8, 52u8, 45u8, 48u8, 53u8, 45u8, 50u8, 50u8, 84u8, 49u8, 50u8, 58u8, 49u8, 52u8, 58u8, 51u8, 50u8, 90u8, 34u8, 44u8, 34u8, 117u8, 112u8, 100u8, 97u8, 116u8, 101u8, 100u8, 34u8, 58u8, 34u8, 50u8, 48u8, 50u8, 52u8, 45u8, 48u8, 53u8, 45u8, 50u8, 50u8, 84u8, 49u8, 50u8, 58u8, 49u8, 52u8, 58u8, 51u8, 50u8, 90u8, 34u8, 125u8, 125u8]" \ + --assign state_metadata \ + --move-call "0x01::option::some>" state_metadata \ + --assign state_metadata_option \ --move-call $1::alias::create_for_testing \ none \ 123u32 \ - 'some("DIDwhatever")' \ + state_metadata_option \ none \ none \ none \ diff --git a/identity_sui_name_tbd/scripts/migrate_alias_output.sh b/identity_sui_name_tbd/scripts/migrate_alias_output.sh index 9ab614a5ca..a3a29b3163 100755 --- a/identity_sui_name_tbd/scripts/migrate_alias_output.sh +++ b/identity_sui_name_tbd/scripts/migrate_alias_output.sh @@ -5,7 +5,7 @@ if [ -z "$1" ] then - echo "No arguments supplied, please pass package id as hex string" + echo "No arguments supplied, please pass identity package id as hex string" exit 1 fi diff --git a/identity_sui_name_tbd/scripts/publish_identity_package.sh b/identity_sui_name_tbd/scripts/publish_identity_package.sh new file mode 100755 index 0000000000..196df85aef --- /dev/null +++ b/identity_sui_name_tbd/scripts/publish_identity_package.sh @@ -0,0 +1,11 @@ +#!/bin/bash + +# Copyright 2020-2024 IOTA Stiftung +# SPDX-License-Identifier: Apache-2.0 + +script_dir=$(dirname $0) +package_dir=$script_dir/../packages/identity_iota + +echo "publishing package from $package_dir" +cd $package_dir +sui client publish --gas-budget 100000000 . diff --git a/identity_sui_name_tbd/src/lib.rs b/identity_sui_name_tbd/src/lib.rs index 1e4c849da7..7b553c0a48 100644 --- a/identity_sui_name_tbd/src/lib.rs +++ b/identity_sui_name_tbd/src/lib.rs @@ -3,7 +3,6 @@ mod error; pub mod migration; -pub mod resolution; pub mod utils; pub use error::Error; diff --git a/identity_sui_name_tbd/src/migration/alias.rs b/identity_sui_name_tbd/src/migration/alias.rs new file mode 100644 index 0000000000..a797ce3d20 --- /dev/null +++ b/identity_sui_name_tbd/src/migration/alias.rs @@ -0,0 +1,84 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use std::str::FromStr; + +use serde; +use serde::Deserialize; +use serde::Serialize; +use sui_sdk::rpc_types::SuiObjectDataOptions; +use sui_sdk::rpc_types::SuiParsedData; +use sui_sdk::types::base_types::ObjectID; +use sui_sdk::types::id::UID; +use sui_sdk::SuiClient; + +use crate::Error; + +pub async fn get_alias(client: &SuiClient, object_id: &str) -> Result, Error> { + let object_id = ObjectID::from_str(object_id) + .map_err(|err| Error::ObjectLookup(format!("Could not parse given object id {object_id}; {err}")))?; + let options = SuiObjectDataOptions { + show_type: true, + show_owner: true, + show_previous_transaction: true, + show_display: true, + show_content: true, + show_bcs: true, + show_storage_rebate: true, + }; + let response = client + .read_api() + .get_object_with_options(object_id, options) + .await + .map_err(|err| { + Error::ObjectLookup(format!( + "Could not get object with options for this object_id {object_id}; {err}" + )) + })?; + + // no issues with call but + let Some(data) = response.data else { + // call was successful but not data for alias id + return Ok(None); + }; + + let content = data + .content + .ok_or_else(|| Error::ObjectLookup(format!("no content in retrieved object in object id {object_id}")))?; + + let SuiParsedData::MoveObject(value) = content else { + return Err(Error::ObjectLookup(format!( + "found data at object id {object_id} is not an object" + ))); + }; + + dbg!(&value); + + let alias: UnmigratedAlias = serde_json::from_value(value.fields.to_json_value()).unwrap(); + + Ok(Some(alias)) +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct UnmigratedAlias { + /// The ID of the Alias = hash of the Output ID that created the Alias Output in Stardust. + /// This is the AliasID from Stardust. + pub id: UID, + + /// The last State Controller address assigned before the migration. + pub legacy_state_controller: Option, + /// A counter increased by 1 every time the alias was state transitioned. + pub state_index: u32, + /// State metadata that can be used to store additional information. + pub state_metadata: Option>, + + /// The sender feature. + pub sender: Option, + /// The metadata feature. + pub metadata: Option>, + + /// The immutable issuer feature. + pub immutable_issuer: Option, + /// The immutable metadata feature. + pub immutable_metadata: Option>, +} diff --git a/identity_sui_name_tbd/src/migration/document.rs b/identity_sui_name_tbd/src/migration/document.rs new file mode 100644 index 0000000000..a8c8abb7f6 --- /dev/null +++ b/identity_sui_name_tbd/src/migration/document.rs @@ -0,0 +1,65 @@ +use std::str::FromStr; + +use serde; +use serde::Deserialize; +use serde::Serialize; +use serde_json::Value; +use sui_sdk::rpc_types::SuiObjectDataOptions; +use sui_sdk::rpc_types::SuiParsedData; +use sui_sdk::types::base_types::ObjectID; +use sui_sdk::types::id::UID; +use sui_sdk::SuiClient; + +use crate::Error; + +#[derive(Debug, Deserialize, Serialize)] +pub struct Document { + pub id: UID, + pub doc: Vec, + pub iota: String, + pub native_tokens: Value, +} + +pub async fn get_identity_document(client: &SuiClient, object_id: &str) -> Result, Error> { + let object_id = ObjectID::from_str(object_id) + .map_err(|err| Error::ObjectLookup(format!("Could not parse given object id {object_id}; {err}")))?; + let options = SuiObjectDataOptions { + show_type: true, + show_owner: true, + show_previous_transaction: true, + show_display: true, + show_content: true, + show_bcs: true, + show_storage_rebate: true, + }; + let response = client + .read_api() + .get_object_with_options(object_id, options) + .await + .map_err(|err| { + Error::ObjectLookup(format!( + "Could not get object with options for this object_id {object_id}; {err}" + )) + })?; + + // no issues with call but + let Some(data) = response.data else { + // call was successful but not data for alias id + return Ok(None); + }; + + let content = data + .content + .ok_or_else(|| Error::ObjectLookup(format!("no content in retrieved object in object id {object_id}")))?; + + let SuiParsedData::MoveObject(value) = content else { + return Err(Error::ObjectLookup(format!( + "found data at object id {object_id} is not an object" + ))); + }; + dbg!(&value); + + let alias: Document = serde_json::from_value(value.fields.to_json_value()).unwrap(); + + Ok(Some(alias)) +} diff --git a/identity_sui_name_tbd/src/migration/mod.rs b/identity_sui_name_tbd/src/migration/mod.rs index e65a32d805..90e5f163b7 100644 --- a/identity_sui_name_tbd/src/migration/mod.rs +++ b/identity_sui_name_tbd/src/migration/mod.rs @@ -1,6 +1,10 @@ // Copyright 2020-2024 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 +mod alias; +mod document; mod registry; +pub use alias::*; +pub use document::*; pub use registry::*; diff --git a/identity_sui_name_tbd/src/migration/registry.rs b/identity_sui_name_tbd/src/migration/registry.rs index 9800d1fc90..48d9ab64c6 100644 --- a/identity_sui_name_tbd/src/migration/registry.rs +++ b/identity_sui_name_tbd/src/migration/registry.rs @@ -3,6 +3,7 @@ use std::sync::OnceLock; +use serde; use serde::Deserialize; use sui_sdk::rpc_types::EventFilter; use sui_sdk::rpc_types::SuiData; diff --git a/identity_sui_name_tbd/src/resolution/mod.rs b/identity_sui_name_tbd/src/resolution/mod.rs deleted file mode 100644 index 5262837fc8..0000000000 --- a/identity_sui_name_tbd/src/resolution/mod.rs +++ /dev/null @@ -1,6 +0,0 @@ -// Copyright 2020-2024 IOTA Stiftung -// SPDX-License-Identifier: Apache-2.0 - -mod unmigrated_resolver; - -pub use unmigrated_resolver::*; diff --git a/identity_sui_name_tbd/src/resolution/unmigrated_resolver.rs b/identity_sui_name_tbd/src/resolution/unmigrated_resolver.rs deleted file mode 100644 index 4999b5e542..0000000000 --- a/identity_sui_name_tbd/src/resolution/unmigrated_resolver.rs +++ /dev/null @@ -1,70 +0,0 @@ -// Copyright 2020-2024 IOTA Stiftung -// SPDX-License-Identifier: Apache-2.0 - -use std::str::FromStr; - -use serde_json::Value; -use sui_sdk::rpc_types::SuiObjectDataOptions; -use sui_sdk::rpc_types::SuiParsedData; -use sui_sdk::types::base_types::ObjectID; -use sui_sdk::SuiClient; - -use crate::utils::get_client; -use crate::Error; - -pub const LOCAL_NETWORK: &str = "http://127.0.0.1:9000"; - -pub struct UnmigratedResolver { - client: SuiClient, -} - -impl UnmigratedResolver { - pub async fn new(network: &str) -> Result { - let client = get_client(network).await?; - - Ok(Self { client }) - } - - pub async fn get_alias_output(&self, object_id: &str) -> Result { - let object_id = ObjectID::from_str(object_id) - .map_err(|err| Error::ObjectLookup(format!("Could not parse given object id {object_id}; {err}")))?; - let options = SuiObjectDataOptions { - show_type: true, - show_owner: true, - show_previous_transaction: true, - show_display: true, - show_content: true, - show_bcs: true, - show_storage_rebate: true, - }; - let response = self - .client - .read_api() - .get_object_with_options(object_id, options) - .await - .map_err(|err| { - Error::ObjectLookup(format!( - "Could not get object with options for this object_id {object_id}; {err}" - )) - })?; - - let data = response.data.ok_or_else(|| { - Error::ObjectLookup(format!( - "Call succeeded but could not get data for object id {object_id}" - )) - })?; - let content = data - .content - .ok_or_else(|| Error::ObjectLookup(format!("no content in retrieved object in object id {object_id}")))?; - - let SuiParsedData::MoveObject(value) = content else { - return Err(Error::ObjectLookup(format!( - "found data at object id {object_id} is not an object" - ))); - }; - - let alias_output = value.fields.to_json_value(); - - Ok(alias_output) - } -} diff --git a/identity_sui_name_tbd/src/signing/mod.rs b/identity_sui_name_tbd/src/signing/mod.rs new file mode 100644 index 0000000000..a51d687ee5 --- /dev/null +++ b/identity_sui_name_tbd/src/signing/mod.rs @@ -0,0 +1,52 @@ +use fastcrypto::hash::HashFunction; +use fastcrypto::traits::ToFromBytes; +use iota_sdk::client::secret::stronghold::StrongholdSecretManager; +use iota_sdk::crypto::keys::bip44::Bip44; +use shared_crypto::intent::Intent; +use shared_crypto::intent::IntentMessage; +use sui_sdk::types::crypto::DefaultHash; +use sui_sdk::types::crypto::Signature; +use sui_sdk::types::crypto::SignatureScheme; +use sui_sdk::types::transaction::TransactionData; + +const ACCOUNT_INDEX: u32 = 0; +const INTERNAL_ADDRESS: bool = false; +const ADDRESS_INDEX: u32 = 0; + +pub async fn sign_tx(tx_data: &TransactionData) -> anyhow::Result { + // should be part of the instance + let stronghold_secret_manager = StrongholdSecretManager::builder() + .password("secure_password".to_string()) + .build("test.stronghold") + .expect("Failed to create temporary stronghold"); + + // build intent message to sign + let intent = Intent::sui_transaction(); + let intent_msg = IntentMessage::new(intent, &tx_data); + let mut hasher = DefaultHash::default(); + hasher.update(bcs::to_bytes(&intent_msg)?); + let digest = hasher.finalize().digest; + + // sign with sui m/44'/784'/0'/0'/0' + let bip44_chain = Bip44::new(784) + .with_account(ACCOUNT_INDEX) + .with_change(INTERNAL_ADDRESS as _) + .with_address_index(ADDRESS_INDEX); + let signed = stronghold_secret_manager.sign_ed25519(&digest, bip44_chain).await?; + println!("signed - sui config: {:?}", &signed); + + dbg!(&signed); + + // convert to sui signature object + let binding = [ + [SignatureScheme::ED25519.flag()].as_slice(), + signed.signature().to_bytes().as_slice(), + signed.public_key_bytes().to_bytes().as_slice(), + ] + .concat(); + let signature_bytes: &[u8] = binding.as_slice(); + + let signature = Signature::from_bytes(signature_bytes)?; + + Ok(signature) +} diff --git a/identity_sui_name_tbd/src/utils.rs b/identity_sui_name_tbd/src/utils.rs index f30bc2eaf3..dc9fa74d6d 100644 --- a/identity_sui_name_tbd/src/utils.rs +++ b/identity_sui_name_tbd/src/utils.rs @@ -6,6 +6,8 @@ use sui_sdk::SuiClientBuilder; use crate::Error; +pub const LOCAL_NETWORK: &str = "http://127.0.0.1:9000"; + pub async fn get_client(network: &str) -> Result { let client = SuiClientBuilder::default() .build(network) diff --git a/identity_sui_name_tbd/tests/test.rs b/identity_sui_name_tbd/tests/test.rs index ce4e6db7ab..4eaa5dab17 100644 --- a/identity_sui_name_tbd/tests/test.rs +++ b/identity_sui_name_tbd/tests/test.rs @@ -6,9 +6,9 @@ use std::str::FromStr; use fastcrypto::hash::HashFunction; use fastcrypto::traits::ToFromBytes; -use identity_sui_name_tbd::resolution::UnmigratedResolver; -use identity_sui_name_tbd::resolution::LOCAL_NETWORK; +use identity_sui_name_tbd::migration::get_alias; use identity_sui_name_tbd::utils::get_client; +use identity_sui_name_tbd::utils::LOCAL_NETWORK; use iota_sdk::client::secret::stronghold::StrongholdSecretManager; use iota_sdk::client::secret::SecretManage; use iota_sdk::crypto::keys::bip39::Mnemonic; @@ -57,8 +57,8 @@ async fn can_import_the_test_mnemonic() -> anyhow::Result<()> { } #[tokio::test] -async fn can_initialize_resolver_for_unmigrated_alias_outputs() -> anyhow::Result<()> { - let result = UnmigratedResolver::new(LOCAL_NETWORK).await; +async fn can_initialize_new_client() -> anyhow::Result<()> { + let result = get_client(LOCAL_NETWORK).await; assert!(result.is_ok()); @@ -68,12 +68,13 @@ async fn can_initialize_resolver_for_unmigrated_alias_outputs() -> anyhow::Resul #[tokio::test] #[ignore] async fn can_fetch_alias_output_by_object_id() -> anyhow::Result<()> { - let resolver = UnmigratedResolver::new(LOCAL_NETWORK).await?; - let result = resolver - .get_alias_output("0x669c70a008a5e226813927a7b62a5029306d8f7e7366b0634ef6027b3dbda850") - .await; + let client = get_client(LOCAL_NETWORK).await?; + let result = get_alias( + &client, + "0x669c70a008a5e226813927a7b62a5029306d8f7e7366b0634ef6027b3dbda850", + ) + .await; - dbg!(&result); assert!(result.is_ok()); Ok(()) From f6ea767d317b70a44d8c12056410bc70bdcd3ea3 Mon Sep 17 00:00:00 2001 From: Enrico Marconi Date: Mon, 27 May 2024 11:26:52 +0200 Subject: [PATCH 05/62] use dynamic_object_field for migration registry --- .../src/client/kinesis_identity_client.rs | 27 +++------ .../identity_iota/sources/document.move | 58 +++++-------------- .../identity_iota/sources/migration.move | 40 +++++++++++++ .../sources/migration_registry.move | 29 ++++++---- .../scripts/migrate_alias_output.sh | 2 +- .../src/migration/registry.rs | 7 ++- 6 files changed, 84 insertions(+), 79 deletions(-) create mode 100644 identity_sui_name_tbd/packages/identity_iota/sources/migration.move diff --git a/identity_iota_core/src/client/kinesis_identity_client.rs b/identity_iota_core/src/client/kinesis_identity_client.rs index 6cdc9a1234..d0a1d06806 100644 --- a/identity_iota_core/src/client/kinesis_identity_client.rs +++ b/identity_iota_core/src/client/kinesis_identity_client.rs @@ -4,7 +4,6 @@ use std::str::FromStr; use identity_sui_name_tbd::migration::get_alias; -use identity_sui_name_tbd::migration::get_identity_document; use identity_sui_name_tbd::migration::lookup; use iota_sdk::types::block::output::AliasId; use sui_sdk::types::base_types::ObjectID; @@ -46,25 +45,15 @@ impl KinesisIotaIdentityClientExt for SuiClient { // otherwise check registry for a migrated alias let object_id = ObjectID::from_str(&alias_id_string) .map_err(|_| Error::DIDSyntaxError(identity_did::Error::InvalidMethodId))?; - let mapped_id = lookup(self, object_id).await.map_err(|err| { - Error::DIDResolutionErrorKinesis(format!("failed to look up alias id in migration registry; {err}")) - })?; - // if we found a mapping, resolve to identity package `Document` object - let document = if let Some(mapped_id_value) = mapped_id { - let mapped_id_value_string = mapped_id_value.to_string(); - get_identity_document(self, &mapped_id_value_string) - .await - .map_err(|err| Error::DIDResolutionErrorKinesis(format!("failed to resolve identity document; {err}")))? - .ok_or_else(|| { - Error::DIDResolutionErrorKinesis(format!( - "no identity document found for mapped id value {mapped_id_value_string}" - )) - })? - } else { - return Err(Error::DIDResolutionErrorKinesis(format!( + let document = lookup(self, object_id) + .await + .map_err(|err| { + Error::DIDResolutionErrorKinesis(format!("failed to look up alias id in migration registry; {err}")) + })? + .ok_or(Error::DIDResolutionErrorKinesis(format!( "could not find alias id {alias_id_string} in migration registry" - ))); - }; + )))?; + // if we found a mapping, resolve to identity package `Document` object // and get state metadata / serialized document from it document.doc }; diff --git a/identity_sui_name_tbd/packages/identity_iota/sources/document.move b/identity_sui_name_tbd/packages/identity_iota/sources/document.move index be18587215..8de2be95dd 100644 --- a/identity_sui_name_tbd/packages/identity_iota/sources/document.move +++ b/identity_sui_name_tbd/packages/identity_iota/sources/document.move @@ -1,61 +1,29 @@ module identity_iota::document { - use sui::{balance::Balance, bag::Bag, sui::SUI, transfer::share_object}; - use stardust::alias_output::{AliasOutput, extract_assets}; - use identity_iota::{controller, controller::ControllerCap, migration_registry::MigrationRegistry}; + use sui::{balance::Balance, bag::Bag, sui::SUI}; + use identity_iota::{controller::ControllerCap, controller}; const ENotADidOutput: u64 = 1; const EInvalidCapability: u64 = 2; /// DID document. - public struct Document has key { + public struct Document has key, store { id: UID, doc: vector, iota: Balance, native_tokens: Bag, } - /// Creates a new `Document` from an Iota 1.0 legacy `AliasOutput`. - public fun from_legacy_alias_output( - alias_output: AliasOutput, - migration_registry: &mut MigrationRegistry, - ctx: &mut TxContext - ): ControllerCap { - // Extract required data from output. - let (iota, native_tokens, alias_data) = extract_assets(alias_output); - let ( - alias_id, - _, - _, - mut state_metadata, - _, - _, - _, - _, - ) = alias_data.destructure(); - // Check if `state_metadata` contains a DID document. - assert!(is_did_output(state_metadata.borrow()), ENotADidOutput); - let legacy_id = alias_id.to_inner(); - // Destroy alias. - object::delete(alias_id); - - let id = object::new(ctx); - let doc_id = id.to_inner(); - // Create a capability for the governor. - let controller_capability = controller::new(doc_id, ctx); - // Create and share the new DID document. - let document = Document { - id, + /// Creates a new DID Document. + public fun new(doc: vector, iota: Balance, native_tokens: Bag, ctx: &mut TxContext): (Document, ControllerCap) { + let doc = Document { + id: object::new(ctx), + doc, iota, - native_tokens, - doc: state_metadata.extract() + native_tokens }; - share_object(document); - - // Add a migration record. - migration_registry.add(legacy_id, doc_id); - - // Transfer the capability to the governor. - controller_capability + let doc_id = doc.id.to_inner(); + + (doc, controller::new(doc_id, ctx)) } /// Updates the DID document. @@ -69,7 +37,7 @@ module identity_iota::document { /// Checks if `data` is a state matadata representing a DID. /// i.e. starts with the bytes b"DID". - fun is_did_output(data: &vector): bool { + public(package) fun is_did_output(data: &vector): bool { data[0] == 0x44 && // b'D' data[1] == 0x49 && // b'I' data[2] == 0x44 // b'D' diff --git a/identity_sui_name_tbd/packages/identity_iota/sources/migration.move b/identity_sui_name_tbd/packages/identity_iota/sources/migration.move new file mode 100644 index 0000000000..d2d13d4936 --- /dev/null +++ b/identity_sui_name_tbd/packages/identity_iota/sources/migration.move @@ -0,0 +1,40 @@ +module identity_iota::migration { + use identity_iota::{migration_registry::MigrationRegistry, document, controller::ControllerCap}; + use stardust::alias_output::{AliasOutput, extract_assets}; + + const ENotADidOutput: u64 = 1; + + /// Creates a new `Document` from an Iota 1.0 legacy `AliasOutput`. + public fun migrate_alias_output(alias_output: AliasOutput, migration_registry: &mut MigrationRegistry, ctx: &mut TxContext): ControllerCap { + // Extract required data from output. + let (iota, native_tokens, alias_data) = extract_assets(alias_output); + let ( + alias_id, + _, + _, + mut state_metadata, + _, + _, + _, + _, + ) = alias_data.destructure(); + // Check if `state_metadata` contains a DID document. + assert!(document::is_did_output(state_metadata.borrow()), ENotADidOutput); + let legacy_id = alias_id.to_inner(); + // Destroy alias. + object::delete(alias_id); + + let (document, controller_capability) = document::new( + state_metadata.extract(), + iota, + native_tokens, + ctx, + ); + + // Add a migration record. + migration_registry.add(legacy_id, document); + + // Transfer the capability to the governor. + controller_capability + } +} \ No newline at end of file diff --git a/identity_sui_name_tbd/packages/identity_iota/sources/migration_registry.move b/identity_sui_name_tbd/packages/identity_iota/sources/migration_registry.move index 83f31cf933..e27ef0d6d6 100644 --- a/identity_sui_name_tbd/packages/identity_iota/sources/migration_registry.move +++ b/identity_sui_name_tbd/packages/identity_iota/sources/migration_registry.move @@ -1,5 +1,6 @@ module identity_iota::migration_registry { - use sui::{dynamic_field as field, transfer::share_object, event}; + use sui::{dynamic_object_field as field, transfer::share_object, event}; + use identity_iota::document::Document; /// One time witness needed to construct a singleton migration registry. public struct MIGRATION_REGISTRY has drop {} @@ -27,18 +28,24 @@ module identity_iota::migration_registry { event::emit(MigrationRegistryCreated { id: registry_id }); } + /// Checks whether the given alias ID exists in the migration registry. + public fun exists(self: &MigrationRegistry, alias_id: ID): bool { + field::exists_(&self.id, alias_id) + } + /// Lookup an alias ID into the migration registry. - public fun lookup(self: &MigrationRegistry, alias_id: ID): Option { - if (field::exists_(&self.id, alias_id)) { - let entry = field::borrow(&self.id, alias_id); - option::some(*entry) - } else { - option::none() - } + public fun borrow(self: &MigrationRegistry, alias_id: ID): &Document { + field::borrow(&self.id, alias_id) + } + + /// Mutably borrow the migrated document `Document` corresponding + /// to the provided `alias_id`, if any. + public fun borrow_mut(self: &mut MigrationRegistry, alias_id: ID): &mut Document { + field::borrow_mut(&mut self.id, alias_id) } /// Adds a new Alias ID -> Object ID binding to the regitry. - public(package) fun add(self: &mut MigrationRegistry, alias_id: ID, object_id: ID) { - field::add(&mut self.id, alias_id, object_id); + public(package) fun add(self: &mut MigrationRegistry, alias_id: ID, doc: Document) { + field::add(&mut self.id, alias_id, doc); } -} \ No newline at end of file +} diff --git a/identity_sui_name_tbd/scripts/migrate_alias_output.sh b/identity_sui_name_tbd/scripts/migrate_alias_output.sh index a3a29b3163..8e63d7a00a 100755 --- a/identity_sui_name_tbd/scripts/migrate_alias_output.sh +++ b/identity_sui_name_tbd/scripts/migrate_alias_output.sh @@ -25,6 +25,6 @@ sui client ptb \ --gas-budget 50000000 \ --move-call sui::tx_context::sender \ --assign sender \ - --move-call $1::document::from_legacy_alias_output @$2 @$3 \ + --move-call $1::migration::migrate_alias_output @$2 @$3 \ --assign controller_cap \ --transfer-objects "[controller_cap]" sender diff --git a/identity_sui_name_tbd/src/migration/registry.rs b/identity_sui_name_tbd/src/migration/registry.rs index 48d9ab64c6..e990274f37 100644 --- a/identity_sui_name_tbd/src/migration/registry.rs +++ b/identity_sui_name_tbd/src/migration/registry.rs @@ -10,6 +10,8 @@ use sui_sdk::rpc_types::SuiData; use sui_sdk::types::base_types::ObjectID; use sui_sdk::SuiClient; +use super::Document; + static MIGRATION_REGISTRY_ID: OnceLock = OnceLock::new(); #[derive(thiserror::Error, Debug)] @@ -62,7 +64,7 @@ async fn migration_registry_id(sui_client: &SuiClient) -> Result Result, Error> { +pub async fn lookup(sui_client: &SuiClient, alias_id: ObjectID) -> Result, Error> { let dynamic_field_name = serde_json::from_value(serde_json::json!({ "type": "0x2::object::ID", "value": alias_id.to_string() @@ -79,8 +81,7 @@ pub async fn lookup(sui_client: &SuiClient, alias_id: ObjectID) -> Result