Skip to content

Commit 4f4d75b

Browse files
authored
Merge pull request #228 from hashed-io/develop
Release 121
2 parents 5642a22 + 227260d commit 4f4d75b

File tree

14 files changed

+197
-147
lines changed

14 files changed

+197
-147
lines changed

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,4 +30,5 @@ gcloud-md5-ingress.yml
3030

3131
.secrets
3232
collator-data
33-
relay-data
33+
relay-data
34+
.vscode/settings.json

.vscode/settings.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
{
22
"cSpell.words": [
3-
"Frunique"
4-
, "fruniques"
3+
"Frunique",
4+
"fruniques",
5+
"Permill"
56
]
67
}

pallets/bitcoin-vaults/src/tests.rs

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ fn inserting_same_xpub_should_fail() {
8787
new_test_ext().execute_with(|| {
8888
assert_ok!( BitcoinVaults::set_xpub(Origin::signed(test_pub(1)), dummy_xpub()) );
8989
assert_noop!(BitcoinVaults::set_xpub(Origin::signed(test_pub(2)), dummy_xpub()),Error::<Test>::XPubAlreadyTaken);
90-
90+
9191
});
9292
}
9393

@@ -96,7 +96,7 @@ fn inserting_without_removing_xpub_should_fail() {
9696
new_test_ext().execute_with(|| {
9797
assert_ok!( BitcoinVaults::set_xpub(Origin::signed(test_pub(1)), dummy_xpub()) );
9898
assert_noop!(BitcoinVaults::set_xpub(Origin::signed(test_pub(1)), dummy_xpub_2()),Error::<Test>::UserAlreadyHasXpub);
99-
99+
100100
});
101101
}
102102

@@ -123,7 +123,7 @@ fn removing_twice_should_not_work() {
123123
assert_ok!( BitcoinVaults::set_xpub(Origin::signed(test_pub(1)), dummy_xpub()) );
124124
assert_ok!(BitcoinVaults::remove_xpub(Origin::signed(test_pub(1))));
125125
assert_noop!(BitcoinVaults::remove_xpub(Origin::signed(test_pub(1))), Error::<Test>::XPubNotFound);
126-
126+
127127
});
128128
}
129129

@@ -132,7 +132,7 @@ fn creating_vault_should_work() {
132132
new_test_ext().execute_with(|| {
133133
assert_ok!( BitcoinVaults::set_xpub(Origin::signed(test_pub(1)), dummy_xpub()) );
134134
assert_ok!( BitcoinVaults::set_xpub(Origin::signed(test_pub(2)), dummy_xpub_2()) );
135-
135+
136136
let cosigners = BoundedVec::<<Test as frame_system::Config>::AccountId, MaxCosignersPerVault>::
137137
try_from([ test_pub(2),].to_vec()).unwrap();
138138
assert_ok!(BitcoinVaults::create_vault( Origin::signed(test_pub(1)) , 2, dummy_description(),true, cosigners) );
@@ -186,7 +186,7 @@ fn vault_with_duplicate_members_shouldnt_work() {
186186
fn vault_with_duplicate_incomplete_members() {
187187
new_test_ext().execute_with(|| {
188188
assert_ok!( BitcoinVaults::set_xpub(Origin::signed(test_pub(1)), dummy_xpub()) );
189-
189+
190190
let cosigners = BoundedVec::<<Test as frame_system::Config>::AccountId, MaxCosignersPerVault>::
191191
try_from([ test_pub(2),test_pub(1),].to_vec()).unwrap();
192192
assert_noop!(BitcoinVaults::create_vault( Origin::signed(test_pub(1)) , 1, dummy_description(), true,
@@ -259,7 +259,7 @@ fn removing_vault_should_work() {
259259
assert_ok!( BitcoinVaults::set_xpub(Origin::signed(test_pub(1)), dummy_xpub()) );
260260
assert_ok!( BitcoinVaults::set_xpub(Origin::signed(test_pub(2)), dummy_xpub_2()) );
261261
assert_ok!( BitcoinVaults::set_xpub(Origin::signed(test_pub(3)), dummy_xpub_3()) );
262-
262+
263263
// Insert a normal vault
264264
let cosigners = BoundedVec::<<Test as frame_system::Config>::AccountId, MaxCosignersPerVault>::
265265
try_from([ test_pub(2),test_pub(3)].to_vec()).unwrap();
@@ -277,7 +277,7 @@ fn removing_vault_which_isnt_yours_shoulnt_work() {
277277
new_test_ext().execute_with(|| {
278278
assert_ok!( BitcoinVaults::set_xpub(Origin::signed(test_pub(1)), dummy_xpub()) );
279279
assert_ok!( BitcoinVaults::set_xpub(Origin::signed(test_pub(2)), dummy_xpub_2()) );
280-
280+
281281
// Insert a normal vault
282282
let cosigners = BoundedVec::<<Test as frame_system::Config>::AccountId, MaxCosignersPerVault>::
283283
try_from([ test_pub(2),].to_vec()).unwrap();
@@ -295,7 +295,7 @@ fn removing_vault_and_xpub_in_order_should_work() {
295295
new_test_ext().execute_with(|| {
296296
assert_ok!( BitcoinVaults::set_xpub(Origin::signed(test_pub(1)), dummy_xpub()) );
297297
assert_ok!( BitcoinVaults::set_xpub(Origin::signed(test_pub(2)), dummy_xpub_2()) );
298-
298+
299299
// Insert a normal vault
300300
let cosigners = BoundedVec::<<Test as frame_system::Config>::AccountId, MaxCosignersPerVault>::
301301
try_from([ test_pub(2),].to_vec()).unwrap();
@@ -315,7 +315,7 @@ fn removing_xpub_before_vault_shouldnt_work() {
315315
new_test_ext().execute_with(|| {
316316
assert_ok!( BitcoinVaults::set_xpub(Origin::signed(test_pub(1)), dummy_xpub()) );
317317
assert_ok!( BitcoinVaults::set_xpub(Origin::signed(test_pub(2)), dummy_xpub_2()) );
318-
318+
319319
// Insert a normal vault
320320
let cosigners = BoundedVec::<<Test as frame_system::Config>::AccountId, MaxCosignersPerVault>::
321321
try_from([ test_pub(2),].to_vec()).unwrap();
@@ -447,7 +447,7 @@ fn saving_psbt_form_external_user_shouldnt_work(){
447447
assert_ok!(BitcoinVaults::propose(Origin::signed(test_pub(1)),vault_id,dummy_testnet_recipient_address(),1000,dummy_description()));
448448
// obtaining proposal id and saving a psbt with a user that is not in the vault
449449
let proposal_id = BitcoinVaults::proposals_by_vault(vault_id).pop().unwrap();
450-
// user 3 is not on
450+
// user 3 is not on
451451
assert_noop!(BitcoinVaults::save_psbt(Origin::signed(test_pub(3)), proposal_id, dummy_psbt()), Error::<Test>::SignerPermissionsNeeded);
452452
});
453453
}
@@ -491,8 +491,8 @@ fn finalize_psbt_should_work(){
491491
let proposal_id = BitcoinVaults::proposals_by_vault(vault_id).pop().unwrap();
492492
make_proposal_valid(proposal_id);
493493

494-
assert_ok!(BitcoinVaults::save_psbt(Origin::signed(test_pub(1)), proposal_id, dummy_psbt()) );
495-
assert_ok!(BitcoinVaults::finalize_psbt(Origin::signed(test_pub(1)), proposal_id,false));
494+
assert_ok!(BitcoinVaults::save_psbt(Origin::signed(test_pub(1)), proposal_id, dummy_psbt()));
495+
// When a proposal meets the threshold changes it status to ReadyToFinalize false
496496
assert!(BitcoinVaults::proposals(proposal_id).unwrap().status.eq(&ProposalStatus::ReadyToFinalize(false)));
497497
});
498498
}
@@ -515,7 +515,7 @@ fn finalize_psbt_twice_shouldnt_work(){
515515
make_proposal_valid(proposal_id);
516516

517517
assert_ok!(BitcoinVaults::save_psbt(Origin::signed(test_pub(1)), proposal_id, dummy_psbt()) );
518-
assert_ok!(BitcoinVaults::finalize_psbt(Origin::signed(test_pub(1)), proposal_id,false));
518+
// When a proposal meets the threshold changes it status to ReadyToFinalize false
519519
assert!(BitcoinVaults::proposals(proposal_id).unwrap().status.eq(&ProposalStatus::ReadyToFinalize(false)));
520520
assert_noop!(BitcoinVaults::finalize_psbt(Origin::signed(test_pub(1)), proposal_id,false), Error::<Test>::PendingProposalRequired);
521521
});
@@ -540,4 +540,4 @@ fn finalize_psbt_without_signatures_shouldnt_work(){
540540

541541
assert_noop!(BitcoinVaults::finalize_psbt(Origin::signed(test_pub(1)), proposal_id,false), Error::<Test>::NotEnoughSignatures);
542542
});
543-
}
543+
}

pallets/fruniques/README.md

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
### **FR**actional **Uniques**
33
> This is WIP - just being spec'd out
44
5-
This pallet is being developed **tightly coupled** to both [`pallet_assets`](https://paritytech.github.io/substrate/latest/pallet_assets/) and [`pallet_uniques`](https://paritytech.github.io/substrate/latest/pallet_uniques/index.html). These are the default [Statemine](https://github.com/paritytech/cumulus/tree/master/polkadot-parachains/statemine) pallets for fungible and non-fungible tokens.
5+
This pallet is being developed **tightly coupled** to both [`pallet_assets`](https://paritytech.github.io/substrate/latest/pallet_assets/) and [`pallet_uniques`](https://paritytech.github.io/substrate/latest/pallet_uniques/index.html). These are the default [Statemine](https://github.com/paritytech/cumulus/tree/master/polkadot-parachains/statemine) pallets for fungible and non-fungible tokens.
66

77
A Frunique is a type of Non-Fungible Token (NFT)
88

@@ -19,20 +19,20 @@ This pallet provides functionality that allows NFT holders to mint a fungible to
1919

2020
The non-fungible token is created and minted using the Statemine `pallet_uniques`.
2121

22-
The fungible token is created and minted using the Statemine `pallet_assets`.
22+
The fungible token is created and minted using the Statemine `pallet_assets`.
2323

2424
The NFT/Unique can be unlocked and released if and only if a single origin holds all of the corresponding fungible token.
2525

2626
![basket-of-fungibles](http://www.plantuml.com/plantuml/proxy?cache=no&src=https://raw.githubusercontent.com/hashed-io/hashed-substrate/main/docs/fungible-basket-frunique.iuml)
2727

2828
## NFT <--> NFTs
29-
An NFT as a trie of NFTs.
29+
An NFT as a trie of NFTs.
3030

3131
### Use cases
3232
#### Tax credit marketplace
3333
A credit is a single NFT, with an `amount`, state of redemption, expiration year, and other metadata. However, that owner can sell less than the `amount`, in which case the newly created credit NFT has all of the same associated data. The sum of the children `amount` values must be equal to the parent.
3434

35-
To support this, we'll create a `AggregatedFrunique` type that enforces the aggregation rules.
35+
To support this, we'll create a `AggregatedFrunique` type that enforces the aggregation rules.
3636

3737
#### Cannabis compliance
3838
For the NY state cannabis compliance program, all yield from all plants must be tracked. This aligns to a very similar data structure as above. Each mother plant is an NFT, each clone as an NFT, each package of flower an NFT, etc. Auditing a specific item is fairly easy via traversing all of its ancestors and descendants through to the harvest and dispensary.
@@ -58,7 +58,7 @@ yarn install
5858
```bash
5959
yarn run:api tx.fruniques.create 1 1 1 5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY 1 100 --seed "//Alice"
6060
```
61-
## Check NFT
61+
## Check NFT
6262
```bash
6363
yarn run:api query.uniques.class 1
6464
yarn run:api query.uniques.asset 1 1
@@ -71,9 +71,16 @@ yarn run:api query.assets.asset 1
7171
yarn run:api query.assets.account 1 5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY
7272
```
7373

74+
## Tests of the pallet
75+
76+
To run the test you simply run the following command
77+
```bash
78+
cargo test --package pallet-fruniques
79+
```
80+
7481
# Similar Projects
7582
### [Fractional Art](https://fractional.art/)
76-
83+
7784
### [Detailed Process Explainer with screenshots](https://medium.com/fractional-art/how-to-use-fractional-to-fractionalize-nfts-84da1a465b6d)
7885

7986
License: MIT

pallets/fruniques/src/functions.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,10 @@ impl<T: Config> Pallet<T> {
3030
u32::from_ne_bytes(input.try_into().unwrap())
3131
}
3232

33+
pub fn percent_to_permill(input: u8) -> Permill {
34+
Permill::from_percent(input as u32)
35+
}
36+
3337
pub fn bytes_to_string(input: Vec<u8>) -> String {
3438
let mut s = String::default();
3539
for x in input {
@@ -196,7 +200,6 @@ impl<T: Config> Pallet<T> {
196200
collection: T::CollectionId,
197201
item: T::ItemId,
198202
owner: <T::Lookup as sp_runtime::traits::StaticLookup>::Source,
199-
_numeric_value: Option<Permill>,
200203
attributes: Option<Vec<(BoundedVec<u8, T::KeyLimit>, BoundedVec<u8, T::ValueLimit>)>>,
201204
) -> DispatchResult {
202205

pallets/fruniques/src/lib.rs

Lines changed: 40 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ pub mod pallet {
2121
use frame_support::{pallet_prelude::*, transactional, BoundedVec};
2222
use frame_system::pallet_prelude::*;
2323
use scale_info::prelude::vec::Vec;
24-
use sp_runtime::Permill;
2524
/// Configure the pallet by specifying the parameters and types on which it depends.
2625
#[pallet::config]
2726
pub trait Config: frame_system::Config + pallet_uniques::Config {
@@ -41,22 +40,25 @@ pub mod pallet {
4140
#[pallet::event]
4241
#[pallet::generate_deposit(pub(super) fn deposit_event)]
4342
pub enum Event<T: Config> {
44-
// A frunique and asset class were succesfully created!
43+
// A frunique and asset class were successfully created!
4544
FruniqueCollectionCreated(T::AccountId, T::CollectionId),
46-
// A frunique and asset class were succesfully created!
45+
// A frunique and asset class were successfully created!
4746
FruniqueCreated(T::AccountId, T::AccountId, T::CollectionId, T::ItemId),
48-
// A frunique/unique was succesfully divided!
47+
// A frunique/unique was successfully divided!
4948
FruniqueDivided(T::AccountId, T::AccountId, T::CollectionId, T::ItemId),
5049
// Counter should work?
5150
NextFrunique(u32),
5251
}
5352

5453
#[pallet::error]
5554
pub enum Error<T> {
56-
NoneValue,
55+
// The user does not have permission to perform this action
5756
NoPermission,
57+
// Only the owner of the Frunique can perform this action
5858
NotAdmin,
59+
// The storage is full
5960
StorageOverflow,
61+
// A feature not implemented yet
6062
NotYetImplemented,
6163
// Too many fruniques were minted
6264
FruniqueCntOverflow,
@@ -70,6 +72,10 @@ pub mod pallet {
7072
AttributesEmpty,
7173
// The collection doesn't exist
7274
CollectionNotFound,
75+
// The parent doesn't exist
76+
ParentNotFound,
77+
// The frunique doesn't exist
78+
FruniqueNotFound,
7379
}
7480

7581
#[pallet::storage]
@@ -118,7 +124,7 @@ pub mod pallet {
118124
Blake2_128Concat,
119125
CollectionId, // Parent collection id
120126
Blake2_128Concat,
121-
ItemId, // Parent item id
127+
ItemId, // Parent item id
122128
Option<ChildInfo>, // ParentId and flag if it inherit attributes
123129
ValueQuery,
124130
>;
@@ -177,12 +183,12 @@ pub mod pallet {
177183

178184
/// ## Set multiple attributes to a frunique.
179185
/// `origin` must be signed by the owner of the frunique.
186+
/// - `class_id` must be a valid class of the asset class.
187+
/// - `instance_id` must be a valid instance of the asset class.
180188
/// - `attributes` must be a list of pairs of `key` and `value`.
181189
/// `key` must be a valid key for the asset class.
182190
/// `value` must be a valid value for the asset class.
183191
/// `attributes` must not be empty.
184-
/// - `instance_id` must be a valid instance of the asset class.
185-
/// - `class_id` must be a valid class of the asset class.
186192
187193
#[pallet::weight(10_000 + T::DbWeight::get().writes(1))]
188194
pub fn set_attributes(
@@ -191,6 +197,8 @@ pub mod pallet {
191197
instance_id: T::ItemId,
192198
attributes: Vec<(BoundedVec<u8, T::KeyLimit>, BoundedVec<u8, T::ValueLimit>)>,
193199
) -> DispatchResult {
200+
ensure!(Self::item_exists(&class_id, &instance_id), <Error<T>>::FruniqueNotFound);
201+
194202
// ! Ensure the admin is the one who can add attributes to the frunique.
195203
let admin = Self::admin_of(&class_id, &instance_id);
196204
let signer = core::prelude::v1::Some(ensure_signed(origin.clone())?);
@@ -223,32 +231,43 @@ pub mod pallet {
223231
///
224232
/// ### Parameters needed in order to divide a unique:
225233
/// - `class_id`: The type of NFT that the function will create, categorized by numbers.
226-
/// - `numeric_value`: The value of the NFT that the function will create.
227234
/// - `parent_info`: Information of the parent NFT and a flag to indicate the child would inherit their attributes.
228235
/// - `attributes`: Generates a list of attributes for the new NFT.
229236
///
230237
#[pallet::weight(10_000 + T::DbWeight::get().writes(4))]
231238
pub fn spawn(
232239
origin: OriginFor<T>,
233240
class_id: CollectionId,
234-
numeric_value: Option<Permill>,
235241
parent_info: Option<HierarchicalInfo>,
236242
attributes: Option<Vec<(BoundedVec<u8, T::KeyLimit>, BoundedVec<u8, T::ValueLimit>)>>,
237243
) -> DispatchResult {
244+
ensure!(Self::collection_exists(&class_id), <Error<T>>::CollectionNotFound);
245+
246+
if let Some(parent_info) = parent_info {
247+
ensure!(Self::item_exists(&class_id, &parent_info.0), <Error<T>>::ParentNotFound);
248+
}
249+
238250
let owner: T::AccountId = ensure_signed(origin.clone())?;
239251
let account_id = Self::account_id_to_lookup_source(&owner);
240252

241253
let instance_id: ItemId = <NextFrunique<T>>::try_get(class_id).unwrap_or(0);
242254
<NextFrunique<T>>::insert(class_id, instance_id + 1);
243255

244-
Self::do_spawn(
245-
origin.clone(),
246-
class_id,
247-
instance_id,
248-
account_id,
249-
numeric_value,
250-
attributes,
251-
)?;
256+
if let Some(parent_info) = parent_info {
257+
ensure!(Self::item_exists(&class_id, &parent_info.0), <Error<T>>::ParentNotFound);
258+
<FruniqueParent<T>>::insert(class_id, instance_id, Some(parent_info));
259+
260+
let child_info = ChildInfo {
261+
collection_id: class_id,
262+
child_id: instance_id,
263+
is_hierarchical: parent_info.1,
264+
weight: Self::percent_to_permill(parent_info.2),
265+
};
266+
267+
<FruniqueChild<T>>::insert(class_id, instance_id, Some(child_info));
268+
}
269+
270+
Self::do_spawn(origin.clone(), class_id, instance_id, account_id, attributes)?;
252271

253272
Self::deposit_event(Event::FruniqueCreated(
254273
owner.clone(),
@@ -257,22 +276,6 @@ pub mod pallet {
257276
instance_id,
258277
));
259278

260-
261-
if let Some(parent_info) = parent_info {
262-
ensure!(
263-
Self::item_exists(&class_id, &parent_info.0),
264-
<Error<T>>::CollectionNotFound
265-
);
266-
<FruniqueParent<T>>::insert(class_id, instance_id, Some(parent_info));
267-
268-
// let child_info = ChildInfo {
269-
// collection_id: class_id,
270-
// child_id: instance_id,
271-
// is_hierarchical: parent_info.1,
272-
// weight: numeric_value.unwrap()
273-
// }
274-
}
275-
276279
Ok(())
277280
}
278281

@@ -293,6 +296,9 @@ pub mod pallet {
293296
let _ = <FruniqueCnt<T>>::put(0);
294297
let _ = <NextCollection<T>>::put(0);
295298
let _ = <NextFrunique<T>>::clear(1000, None);
299+
let _ = <FruniqueParent<T>>::clear(1000, None);
300+
let _ = <FruniqueChild<T>>::clear(1000, None);
301+
296302
Ok(())
297303
}
298304
}

0 commit comments

Comments
 (0)