diff --git a/6-nfts-for-financial-applications/moonwork/.nvimrc b/6-nfts-for-financial-applications/moonwork/.nvimrc new file mode 100644 index 000000000..976889a7b --- /dev/null +++ b/6-nfts-for-financial-applications/moonwork/.nvimrc @@ -0,0 +1 @@ +noremap f :!scrypto fmt diff --git a/6-nfts-for-financial-applications/moonwork/Cargo.toml b/6-nfts-for-financial-applications/moonwork/Cargo.toml new file mode 100644 index 000000000..11f2477f6 --- /dev/null +++ b/6-nfts-for-financial-applications/moonwork/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "moonwork" +version = "0.1.0" +edition = "2021" + +[dependencies] +sbor = { git = "https://github.com/radixdlt/radixdlt-scrypto", tag = "v0.6.0" } +scrypto = { git = "https://github.com/radixdlt/radixdlt-scrypto", tag = "v0.6.0" } + +[dev-dependencies] +transaction = { git = "https://github.com/radixdlt/radixdlt-scrypto", tag = "v0.6.0" } +radix-engine = { git = "https://github.com/radixdlt/radixdlt-scrypto", tag = "v0.6.0" } +scrypto-unit = { git = "https://github.com/radixdlt/radixdlt-scrypto", tag = "v0.6.0" } + +[profile.release] +opt-level = 's' # Optimize for size. +lto = true # Enable Link Time Optimization. +codegen-units = 1 # Reduce number of codegen units to increase optimizations. +panic = 'abort' # Abort on panic. +strip = "debuginfo" # Strip debug info. +overflow-checks = true # Panic in the case of an overflow. + +[lib] +crate-type = ["cdylib", "lib"] diff --git a/6-nfts-for-financial-applications/moonwork/README.md b/6-nfts-for-financial-applications/moonwork/README.md new file mode 100644 index 000000000..163df89cf --- /dev/null +++ b/6-nfts-for-financial-applications/moonwork/README.md @@ -0,0 +1,132 @@ +# MoonWork - Decentralised Freelancer Platform + +## Whats The Problem and Usecase? +Freelancer platforms have been very popular allowing people to work from anywhere and likewise for those that want work done quickly, freelancer platforms are a quick solution. +Lets imagine some work was done for a project but in the end, the client refused to pay out. +This is an all too common problem we have. What if, as a contractor, you were also judged unfairly due to external reasons. +Reviews are also hard to completely judge on your own, can be spoofed, made up etc. This presents a few problems with freelancer platforms. +Now lets have a look at what use cases NFTs would have in this space for what `MoonWork` provides. + +## Why use NFTs and Decentralise Freelancer Platforms? +NFTs (in my opinion) are the perfect use case for this, the advantages of using these are: +1. Transparency - NFTs are public resources anyone can look into the ledger for. +2. Verification - You can verify NFT metadata by verifying resource NFTs (more on this later). +3. Immutability - NFTs can have some fields completely immutable and others mutable, this gives us confidence metadata is not able to be changed. + +## What is MoonWork? +Taking the features above, what `MoonWork` does are the following, split by contractors, clients and disputes and the use of NFTs: + +### Contractors +1. User registers as a contractor and receives a Contractor NFT which represents their MoonWork identity +2. Newly registered contractors also get a ContractorAccolades NFT. Which all contractors get initially + +When work gets completed, the following happens: +1. For every successfully created work, a contractor's total worth increases, their Contractor NFT gets updated +2. Contractors get rewarded a `WorkCompleted` NFT minted and is a **souldbound** NFT +3. For every 10 `WorkCompleted`, Contractor gets a new `ContractorAccolades` NFT minted with all work non fungible id they completed to achieve it +4. You can verify a Contractor's work by looking at their wallet balance for `WorkCompleted` and `ContractorAccolades` and verify the jobs completed matches the corresponding resources + +When a dispute occurs and is completed: +1. Contractors will get a `DisputeOutcome` NFT +2. Contractor NFT also gets updated with the number of disputes they've been involved in +3. In the same way, we can also verify the number of disputes by verifying it matches the balance of their `DisputeOutcome` NFT + +### Clients +1. User registers as a client. +2. Clients can create new work based on a category (Development & IT, Accounting & Finance etc.) +3. Contractors then request to take a Client's work +4. Client then accepts a Contractor they wish to work with +5. For a completed work, the Client's identity gets updated alongside the work they raised + +When a dispute occurs and is completed: + +1. Clients will get a `DisputeOutcome` NFT +2. Client NFT also gets updated with the number of disputes +3. In the same way, we can also verify the number of disputes by verifying it matches the balance of their `DisputeOutcome` NFT + +### Dispute System +1. A dispute is created with a participant criteria. In this case, for MoonWork, we have specific criteria: + 1. Participant limit - this is a limit of `Client` **AND** `Contractor` that can join and decide a dispute. For example, if participant limit is 2, we allow 2 clients and 2 contractors to join and decide a dispute + 2. Client criteria - this is a criteria a client has to meet, in this case, the number of jobs paid out + 3. Contractor criteria - this is a criteria a contractor has to meet, in this case, the number of jobs completed + +2. Either a Client or Contractor can raise a dispute for work that has been assigned to them. + 1. Disputes can be cancelled by the one who raised it +3. The client and contractor present their case by submitting `DisputeDocument` NFT which is viewable to the public. Its at their discretion to censor private information from documents. +4. Participants join and decide based on the documents submitted +5. Disputes can be completed if either: + 1. The time has expired and there is a majority vote + 2. If the majority vote has been given with participant limit reached +6. In cases where both those conditions above do not apply, i.e. an undecided decision due to split votes, the admin then takes over and has final decision + +### Promotion System +This is a promotion system for contractors, where given their efforts of using the platform, they can, for a period of time, +promote themselves which would appear on a "recommended" section on a frontend. +The purpose of this system is really to show off the utility of the NFTs as a result of work being completed. +The WorkCompleted and ContractorAccolades NFTs form the contractor's identity. + +### Enough Talk! I Want To See It In Action! + +#### Work Scenario - Completed Work +Run through a happy path for work raised which does the following: +- Creates the MoonWork service as an Admin +- Registers an account as a Contractor +- Registers an account as a Client +- Creates a bunch of work categories as an Admin +- Client creates a bunch of work in each cateogry as a Client +- Contractor requests for said work raised by Client +- Client accepts contractor for work +- Client & Contractor accept to finish work +- Finally Contractor can claim their compensation + +```bash +resim reset +source ./transactions/run_full_flow.sh +``` + +#### Dispute Scenario - Completed Dispute +Run through disputes being completed successfully. The above steps must be run for disputes flow to work correctly: +- Creates a dispute as a Client +- Contractor and client submits documents +- Other contractors and clients join and decide on the dispute +- Majority vote is given and the dispute is completed as a Contractor + +```bash +source ./transactions/dispute/run_full_flow.sh +``` + +#### Promotion Scenario - Promote Contractor +Once again, like the above, run the Work Scenario first before running promotion flow: +- Creates a promotion service as an admin +- Contractor promotes themselves as a Contractor +- Sets epoch and removes expired promotion + +```bash +source ./transactions/promotion/run_full_flow.sh +``` + +### Thats Great, What About Alternative Cases? + +We got you covered on this front luckily! So this is easy to test, instead of messing around with a bunch of bash environment variables, just run tests instead! + +**Spoiler, there are 52 integration tests in total :D** + +#### Running Tests +``` +scrypto test +``` + +## Read Documentation +You can generate docs by running: + +Install http and run a local server that serves the docs locally: +``` +cargo install https +cargo doc && target/doc +``` +### Components + +1. MoonWorkService +2. WorkService +3. DisputeService +4. PromotionService diff --git a/6-nfts-for-financial-applications/moonwork/src/dispute.rs b/6-nfts-for-financial-applications/moonwork/src/dispute.rs new file mode 100644 index 000000000..222f203d2 --- /dev/null +++ b/6-nfts-for-financial-applications/moonwork/src/dispute.rs @@ -0,0 +1,936 @@ +use crate::moonwork::DisputeDecision; +use crate::users::{Client, Contractor}; +use crate::work::{Work, WorkStatus}; +use scrypto::prelude::*; + +// Assuming an epoch interval is 30mins, 5 days expiry time +const EXPIRATION_TIME: u64 = 240; + +#[derive(Debug, Describe, TypeId, Encode, Decode, PartialEq, Eq)] +pub enum DisputeSide { + Contractor, + Client, +} + +#[derive(Debug, NonFungibleData)] +pub struct Dispute { + pub work: NonFungibleAddress, + pub expiration: u64, + pub raised_by: DisputeSide, + pub contractor: NonFungibleId, + pub client: NonFungibleId, + #[scrypto(mutable)] + pub client_documents: Vec, + #[scrypto(mutable)] + pub contractor_documents: Vec, + #[scrypto(mutable)] + pub participant_contractors: HashMap, + #[scrypto(mutable)] + pub participant_clients: HashMap, +} + +#[derive(Debug, NonFungibleData)] +pub struct DisputeDocument { + pub submitted_by: DisputeSide, + pub dispute_id: NonFungibleId, + pub document_title: String, + pub document_url: String, +} + +#[derive(Debug, Describe, TypeId, Encode, Decode, Clone, Copy)] +pub struct ContractorCriteria { + pub jobs_completed: u64, +} + +#[derive(Debug, Describe, TypeId, Encode, Decode, Clone, Copy)] +pub struct ClientCriteria { + pub jobs_paid_out: u64, +} + +#[derive(Debug, Describe, TypeId, Encode, Decode, Clone, Copy)] +pub struct ParticipantCriteria { + pub participant_limit: u64, + pub contractor: ContractorCriteria, + pub client: ClientCriteria, +} + +blueprint! { + struct DisputeService { + client: ResourceAddress, + contractor: ResourceAddress, + dispute_resource: ResourceAddress, + dispute_latest_id: u64, + dispute_vault: Vault, + dispute_document_resource: ResourceAddress, + dispute_document_latest_id: u64, + dispute_document_vault: Vault, + work_resource: ResourceAddress, + participant_incentive_vault: Vault, + service_auth: Vault, + service_component: ComponentAddress, + } + + impl DisputeService { + /// This creates a basic dispute system for a given work resource. This blueprint is + /// designed in a way that is reusable. You will have to implement a service component to + /// use this blueprint. + /// + /// Service Methods To Implement: + /// ```ignore + /// blueprint! { + /// struct Service {} + /// + /// impl Service { + /// pub fn get_dispute_participant_criteria(&self) -> ParticipantCriteria { + /// todo!() + /// } + /// + /// pub fn compensate_contractor( + /// &self, + /// client_id: NonFungibleId, + /// contractor_id: NonFungibleId, + /// total_compensation: Decimal, + /// ) -> ParticipantCriteria { + /// todo!() + /// } + /// + /// pub fn complete_dispute_outcome( + /// &self, + /// dispute_side: DisputeSide, + /// contractor_id: NonFungibleId, + /// client_id: NonFungibleId, + /// dispute_decision: DisputeDecision, + /// ) { + /// todo!() + /// } + /// + /// pub fn refund_client(&self, client_id: NonFungibleId, refund_amount: Decimal) { + /// todo!() + /// } + /// } + /// } + /// ``` + pub fn create( + service_badge: Bucket, + client: ResourceAddress, + contractor: ResourceAddress, + work_resource: ResourceAddress, + service_component: ComponentAddress, + ) -> ComponentAddress { + let dispute_resource = ResourceBuilder::new_non_fungible() + .metadata("name", "Dispute") + .metadata("service", "MoonWork") + .mintable(rule!(require(service_badge.resource_address())), LOCKED) + .burnable(rule!(require(service_badge.resource_address())), LOCKED) + .updateable_non_fungible_data( + rule!(require(service_badge.resource_address())), + LOCKED, + ) + .no_initial_supply(); + + let dispute_document_resource = ResourceBuilder::new_non_fungible() + .metadata("name", "Dispute Document") + .metadata("service", "MoonWork") + .mintable(rule!(require(service_badge.resource_address())), LOCKED) + .burnable(rule!(require(service_badge.resource_address())), LOCKED) + .updateable_non_fungible_data( + rule!(require(service_badge.resource_address())), + LOCKED, + ) + .no_initial_supply(); + + let access_rules = AccessRules::new() + .method( + "complete_dispute_as_admin", + rule!(require(service_badge.resource_address())), + ) + .default(AccessRule::AllowAll); + + let mut component = Self { + client, + contractor, + dispute_resource, + dispute_latest_id: 0, + dispute_document_resource, + dispute_document_latest_id: 0, + work_resource, + dispute_vault: Vault::new(dispute_resource), + dispute_document_vault: Vault::new(dispute_document_resource), + participant_incentive_vault: Vault::new(RADIX_TOKEN), + service_auth: Vault::with_bucket(service_badge), + service_component, + } + .instantiate(); + + component.add_access_check(access_rules); + component.globalize() + } + + /// Creates a new dispute for work that is only in progress and assigned to a contractor + /// Work carried out may have been done unfairly and not with what was agreed. + /// + /// This method does the following: + /// 1. Updates `Work` NFT `Work.work_status` with `InDispute` status + /// 2. A `Dispute` NFT is minted with an expiration (`CURRENT_EPOCH + EXPIRATION_TIME`) + /// 3. Dispute NFT is stored in a dispute vault + /// + /// # Panics: + /// - if user proof provided is an invalid `Client` or `Contractor` `ResourceAddress` + /// - if `work_resource` is invalid + /// - if `Work.work_status` is not `InProgress` + /// - if `Work.contractor_assigned` is not assigned to any `Contractor` + /// - if `Work.client` or `Work.contractor_assigned` does not match the user `Proof` + pub fn create_new_dispute( + &mut self, + work_id: NonFungibleId, + work_resource: ResourceAddress, + client_or_contractor: Proof, + ) { + let validated_client_or_contractor = client_or_contractor + .validate_proof(ProofValidationMode::ValidateResourceAddressBelongsTo( + BTreeSet::from_iter([self.client, self.contractor]), + )) + .expect("unauthorized user"); + + assert!(work_resource == self.work_resource, "invalid work"); + + let work_resource_manager = borrow_resource_manager!(work_resource); + + let work_nft = work_resource_manager.get_non_fungible_data::(&work_id); + + assert!( + work_nft.work_status == WorkStatus::InProgress, + "work not in progress" + ); + + let is_client = work_nft.client == validated_client_or_contractor.non_fungible_id(); + + let contractor_assigned = work_nft.contractor_assigned.expect("work not assigned yet"); + + let is_contractor = + contractor_assigned == validated_client_or_contractor.non_fungible_id(); + + assert!(is_client || is_contractor, "unauthorized user"); + + self.service_auth.authorize(|| { + work_resource_manager.update_non_fungible_data( + &work_id, + Work { + client: work_nft.client.clone(), + total_compensation: work_nft.total_compensation, + work_title: work_nft.work_title, + work_description: work_nft.work_description, + work_status: WorkStatus::InDispute, + skills_required: work_nft.skills_required, + contractor_requests: work_nft.contractor_requests, + contractor_assigned: Some(contractor_assigned.clone()), + }, + ); + }); + + let dispute_resource_manager = borrow_resource_manager!(self.dispute_resource); + + self.dispute_latest_id += 1; + + let id = NonFungibleId::from_u64(self.dispute_latest_id); + + let raised_by = match is_contractor { + true => DisputeSide::Contractor, + false => DisputeSide::Client, + }; + + let dispute = self.service_auth.authorize(|| { + dispute_resource_manager.mint_non_fungible( + &id, + Dispute { + work: NonFungibleAddress::new(work_resource, work_id), + expiration: Runtime::current_epoch() + EXPIRATION_TIME, + participant_contractors: HashMap::new(), + participant_clients: HashMap::new(), + contractor: contractor_assigned, + client: work_nft.client, + contractor_documents: vec![], + client_documents: vec![], + raised_by, + }, + ) + }); + + self.dispute_vault.put(dispute); + } + + /// Cancels the dispute for a given work resource. This method does the following: + /// + /// 1. Updates `Work.work_status` back to `InProgress` + /// 2. Burns the `Dispute` NFT + /// + /// # Panics: + /// - if user proof is not a `Client` or `Contractor` `ResourceAddress` + /// - if `Client` or `Contractor` is not part of the dispute + pub fn cancel_dispute(&mut self, dispute_id: NonFungibleId, client_or_contractor: Proof) { + let validated_client_or_contractor = client_or_contractor + .validate_proof(ProofValidationMode::ValidateResourceAddressBelongsTo( + BTreeSet::from_iter([self.client, self.contractor]), + )) + .expect("unauthorized user"); + + let dispute_resource_manager = borrow_resource_manager!(self.dispute_resource); + + let dispute_nft = + dispute_resource_manager.get_non_fungible_data::(&dispute_id); + + match validated_client_or_contractor.resource_address() == self.client { + true => { + let is_raised_by_client = dispute_nft.raised_by == DisputeSide::Client; + let is_matching_client_id = + dispute_nft.client == validated_client_or_contractor.non_fungible_id(); + assert!( + is_raised_by_client && is_matching_client_id, + "invalid client" + ) + } + false => { + let is_raised_by_contractor = dispute_nft.raised_by == DisputeSide::Contractor; + let is_matching_contractor_id = + dispute_nft.contractor == validated_client_or_contractor.non_fungible_id(); + assert!( + is_raised_by_contractor && is_matching_contractor_id, + "invalid contractor" + ); + } + }; + + let work_resource_manager = borrow_resource_manager!(self.work_resource); + + let work_nft = work_resource_manager + .get_non_fungible_data::(&dispute_nft.work.non_fungible_id()); + + self.service_auth.authorize(|| { + dispute_resource_manager.burn(self.dispute_vault.take_non_fungible(&dispute_id)); + + work_resource_manager.update_non_fungible_data( + &dispute_nft.work.non_fungible_id(), + Work { + client: work_nft.client, + total_compensation: work_nft.total_compensation, + work_title: work_nft.work_title, + work_description: work_nft.work_description, + work_status: WorkStatus::InProgress, + skills_required: work_nft.skills_required, + contractor_requests: work_nft.contractor_requests, + contractor_assigned: work_nft.contractor_assigned, + }, + ); + }) + } + + /// Client or Contractor that is in dispute can present all evidence that supports their + /// side of the dispute. This is used for participants to judge who they think is correct. + /// + /// Once again, leveraging NFTs, this method does the following: + /// 1. Mints a `DisputeDocument` NFT which is public and viewable by anyone. + /// + /// # Panics: + /// - if `Client` or `Contractor` is not the correct `ResourceAddress` + /// - if `Client` or `Contractor` is not part of the dispute + pub fn submit_document( + &mut self, + dispute_id: NonFungibleId, + document_title: String, + document_url: String, + client_or_contractor: Proof, + ) { + let validated_client_or_contractor = client_or_contractor + .validate_proof(ProofValidationMode::ValidateResourceAddressBelongsTo( + BTreeSet::from_iter([self.client, self.contractor]), + )) + .expect("unauthorized user"); + + let dispute_resource_manager = borrow_resource_manager!(self.dispute_resource); + + let mut dispute = + dispute_resource_manager.get_non_fungible_data::(&dispute_id); + + let is_client_part_of_dispute = + dispute.client == validated_client_or_contractor.non_fungible_id(); + + let is_contractor_part_of_dispute = + dispute.contractor == validated_client_or_contractor.non_fungible_id(); + + assert!( + is_client_part_of_dispute || is_contractor_part_of_dispute, + "unauthorized user" + ); + + let dispute_side = match is_client_part_of_dispute { + true => DisputeSide::Client, + false => DisputeSide::Contractor, + }; + + let dispute_document_resource_manager = + borrow_resource_manager!(self.dispute_document_resource); + + self.dispute_document_latest_id += 1; + + let id = NonFungibleId::from_u64(self.dispute_document_latest_id); + + let dispute_document = self.service_auth.authorize(|| { + dispute_document_resource_manager.mint_non_fungible( + &id, + DisputeDocument { + submitted_by: dispute_side, + dispute_id: dispute_id.clone(), + document_title, + document_url, + }, + ) + }); + + match is_client_part_of_dispute { + true => dispute + .client_documents + .push(dispute_document.non_fungible_id()), + false => dispute + .contractor_documents + .push(dispute_document.non_fungible_id()), + }; + + self.service_auth.authorize(|| { + dispute_resource_manager.update_non_fungible_data( + &dispute_id, + Dispute { + work: dispute.work, + expiration: dispute.expiration, + contractor: dispute.contractor, + client: dispute.client, + client_documents: dispute.client_documents, + contractor_documents: dispute.contractor_documents, + participant_contractors: dispute.participant_contractors, + participant_clients: dispute.participant_clients, + raised_by: dispute.raised_by, + }, + ) + }); + + self.dispute_document_vault.put(dispute_document); + } + + /// Removes a document from the dispute, this method does the following: + /// + /// 1. Update `Work.contractor_documents` or `Work.client_documents` depending on the user + /// wanting to remove their document + /// 2. `DisputeDocument` NFT with the corresponding NonFungibleId is burned from vault + /// + /// # Panics + /// - if user proof is not a `Client` or `Contractor` + /// - if valid user proof is not part of the dispute + pub fn remove_document(&mut self, document_id: NonFungibleId, client_or_contractor: Proof) { + let validated_client_or_contractor = client_or_contractor + .validate_proof(ProofValidationMode::ValidateResourceAddressBelongsTo( + BTreeSet::from_iter([self.client, self.contractor]), + )) + .expect("unauthorized user"); + + let dispute_document_resource_manager = + borrow_resource_manager!(self.dispute_document_resource); + + let document_nft = dispute_document_resource_manager + .get_non_fungible_data::(&document_id); + + let dispute_resource_manager = borrow_resource_manager!(self.dispute_resource); + + let mut dispute_nft = + dispute_resource_manager.get_non_fungible_data::(&document_nft.dispute_id); + + match validated_client_or_contractor.resource_address() == self.client { + true => { + assert!( + dispute_nft.client == validated_client_or_contractor.non_fungible_id(), + "unauthorized user" + ); + assert!(document_nft.submitted_by == DisputeSide::Client); + dispute_nft.client_documents = dispute_nft + .client_documents + .into_iter() + .filter(|cd| cd != &document_id) + .collect(); + } + false => { + assert!( + dispute_nft.contractor == validated_client_or_contractor.non_fungible_id(), + "unauthorized user" + ); + assert!(document_nft.submitted_by == DisputeSide::Contractor); + dispute_nft.contractor_documents = dispute_nft + .contractor_documents + .into_iter() + .filter(|cd| cd != &document_id) + .collect(); + } + } + + self.service_auth.authorize(|| { + dispute_document_resource_manager + .burn(self.dispute_document_vault.take_non_fungible(&document_id)); + + dispute_resource_manager.update_non_fungible_data( + &document_nft.dispute_id, + Dispute { + work: dispute_nft.work, + expiration: dispute_nft.expiration, + raised_by: dispute_nft.raised_by, + contractor: dispute_nft.contractor, + client: dispute_nft.client, + client_documents: dispute_nft.client_documents, + contractor_documents: dispute_nft.contractor_documents, + participant_contractors: dispute_nft.participant_contractors, + participant_clients: dispute_nft.participant_clients, + }, + ) + }) + } + + /// Participants can join a dispute to decide either between on the side of the client or + /// contractor. In order to participate, they must meet the `ParticipantCriteria` to vote. + /// Equal amounts of Client and Contractor can join. If the participant limit is 2, + /// 2 Client users and 2 Contractor can join. + /// + /// This method in detail does the following: + /// 1. Check if willing participant meets requirements + /// 2. `Dispute` NFT is updated with the participant + /// + /// # Panics + /// - if user proof is not a `Client` or `Contractor` has an invalid `ResourceAddress` + /// - if dispute has expired + /// - if user is part of the dispute itself + /// - if user has already joined the dispute + /// - if user does not meet requirements + pub fn join_and_decide_dispute( + &self, + dispute_id: NonFungibleId, + side: DisputeSide, + client_or_contractor: Proof, + ) { + let validated_client_or_contractor = client_or_contractor + .validate_proof(ProofValidationMode::ValidateResourceAddressBelongsTo( + BTreeSet::from_iter([self.client, self.contractor]), + )) + .expect("unauthorized user"); + + let dispute_resource_manager = borrow_resource_manager!(self.dispute_resource); + + let mut dispute = + dispute_resource_manager.get_non_fungible_data::(&dispute_id); + + assert!( + dispute.expiration >= Runtime::current_epoch(), + "dispute expired" + ); + + let participant_criteria = borrow_component!(self.service_component) + .call::("get_dispute_participant_criteria", args!()); + + match validated_client_or_contractor.resource_address() == self.client { + true => { + assert!( + validated_client_or_contractor.non_fungible_id() != dispute.client, + "cannot participate in own dispute" + ); + assert!( + dispute + .participant_clients + .get(&validated_client_or_contractor.non_fungible_id()) + .is_none(), + "already joined dispute" + ); + assert!( + dispute.participant_clients.len() + < participant_criteria.participant_limit as usize, + "participant limit reached" + ); + + let client_resource_manager = borrow_resource_manager!(self.client); + let client_nft = client_resource_manager.get_non_fungible_data::( + &validated_client_or_contractor.non_fungible_id(), + ); + + assert!( + client_nft.jobs_paid_out >= participant_criteria.client.jobs_paid_out, + "client criteria not met" + ); + + dispute + .participant_clients + .insert(validated_client_or_contractor.non_fungible_id(), side) + } + false => { + assert!( + validated_client_or_contractor.non_fungible_id() != dispute.contractor, + "cannot participate in own dispute" + ); + + assert!( + dispute + .participant_contractors + .get(&validated_client_or_contractor.non_fungible_id()) + .is_none(), + "already joined dispute" + ); + assert!( + dispute.participant_contractors.len() + < participant_criteria.participant_limit as usize, + "participant limit reached" + ); + + let contractor_resource_manager = borrow_resource_manager!(self.contractor); + let contractor_nft = contractor_resource_manager + .get_non_fungible_data::( + &validated_client_or_contractor.non_fungible_id(), + ); + + assert!( + contractor_nft.jobs_completed + >= participant_criteria.contractor.jobs_completed, + "contractor criteria not met" + ); + + dispute + .participant_contractors + .insert(validated_client_or_contractor.non_fungible_id(), side) + } + }; + + self.service_auth.authorize(|| { + dispute_resource_manager.update_non_fungible_data( + &dispute_id, + Dispute { + work: dispute.work, + expiration: dispute.expiration, + client: dispute.client, + contractor: dispute.contractor, + participant_contractors: dispute.participant_contractors, + participant_clients: dispute.participant_clients, + client_documents: dispute.client_documents, + contractor_documents: dispute.contractor_documents, + raised_by: dispute.raised_by, + }, + ) + }); + } + + /// If the dispute has expired and has a clear decision, either the `Client` or + /// `Contractor` can complete the dispute. Similar to the actions of + /// complete_dispute_as_admin, this methods does the following: + /// + /// 1. The associated `Work` NFT is marked as `Disputed` + /// 2. If the contractor has won, it compensates the contractor as agreed + /// 3. If the client has won, it refunds the client from the compensation agreed + /// 4. In both cases, it calls the service component to create a `DisputeOutcome` NFT + /// for both the `Client` and `Contractor` indicating if they `Won` or `Lost` + /// respectively + /// + /// # Panics: + /// - if user proof is not a `Client` or `Contractor` `ResourceAddress` + /// - if `Work.work_status` is not `InDispute` + /// - if dispute has not expired or dispute has not reached participant limit + pub fn complete_dispute(&mut self, dispute_id: NonFungibleId, client_or_contractor: Proof) { + let validated_client_or_contractor = client_or_contractor + .validate_proof(ProofValidationMode::ValidateResourceAddressBelongsTo( + BTreeSet::from_iter([self.client, self.contractor]), + )) + .expect("unauthorized user"); + + let participant_criteria = borrow_component!(self.service_component) + .call::("get_dispute_participant_criteria", args!()); + + let dispute_resource_manager = borrow_resource_manager!(self.dispute_resource); + + let dispute = dispute_resource_manager.get_non_fungible_data::(&dispute_id); + + let work_resource_manager = borrow_resource_manager!(dispute.work.resource_address()); + + let work_nft = work_resource_manager + .get_non_fungible_data::(&dispute.work.non_fungible_id()); + + assert!( + work_nft.work_status == WorkStatus::InDispute, + "work must be in dispute" + ); + + let is_client = dispute.client == validated_client_or_contractor.non_fungible_id(); + + let is_contractor = + dispute.contractor == validated_client_or_contractor.non_fungible_id(); + + assert!(is_client || is_contractor, "unauthorized user"); + + let is_dispute_expired = Runtime::current_epoch() > dispute.expiration; + + let participant_limit_reached = dispute.participant_contractors.len() + == participant_criteria.participant_limit as usize + && dispute.participant_clients.len() + == participant_criteria.participant_limit as usize; + + assert!( + is_dispute_expired || participant_limit_reached, + "requirements not met" + ); + + let clients_on_contractor_side = dispute + .participant_clients + .values() + .into_iter() + .filter(|client| **client == DisputeSide::Contractor) + .count(); + let contractors_on_contractor_side = dispute + .participant_contractors + .values() + .into_iter() + .filter(|contractor| **contractor == DisputeSide::Contractor) + .count(); + + let total_participants = + dispute.participant_clients.len() + dispute.participant_contractors.len(); + + let total_on_contractor = clients_on_contractor_side + contractors_on_contractor_side; + + let total_on_client = total_participants - total_on_contractor; + + let has_contractor_won_dispute = + ((Decimal::from(total_on_contractor) / Decimal::from(total_participants)) * 100) + > dec!(50); + + let has_client_won_dispute = + ((Decimal::from(total_on_client) / Decimal::from(total_participants)) * 100) + > dec!(50); + + assert!( + (has_client_won_dispute && !has_contractor_won_dispute) + || (!has_client_won_dispute && has_contractor_won_dispute), + "split decision - admin decision" + ); + + self.service_auth.authorize(|| { + work_resource_manager.update_non_fungible_data( + &dispute.work.non_fungible_id(), + Work { + client: work_nft.client, + total_compensation: work_nft.total_compensation, + work_title: work_nft.work_title, + work_description: work_nft.work_description, + work_status: WorkStatus::Disputed, + skills_required: work_nft.skills_required, + contractor_requests: work_nft.contractor_requests, + contractor_assigned: work_nft.contractor_assigned, + }, + ); + }); + + let work_compensation = work_nft.total_compensation; + + match has_contractor_won_dispute { + true => self.service_auth.authorize(|| { + borrow_component!(self.service_component).call::<()>( + "compensate_contractor", + args!(dispute.client, dispute.contractor, work_compensation), + ); + + borrow_component!(self.service_component).call::<()>( + "complete_dispute_outcome", + args!( + DisputeSide::Contractor, + dispute.contractor, + dispute.work, + DisputeDecision::Won + ), + ); + + borrow_component!(self.service_component).call::<()>( + "complete_dispute_outcome", + args!( + DisputeSide::Client, + dispute.client, + dispute.work, + DisputeDecision::Lost + ), + ); + }), + false => { + self.service_auth.authorize(|| { + borrow_component!(self.service_component) + .call::<()>("refund_client", args!(dispute.client, work_compensation)); + + borrow_component!(self.service_component).call::<()>( + "complete_dispute_outcome", + args!( + DisputeSide::Contractor, + dispute.contractor, + dispute.work, + DisputeDecision::Lost + ), + ); + + borrow_component!(self.service_component).call::<()>( + "complete_dispute_outcome", + args!( + DisputeSide::Client, + dispute.client, + dispute.work, + DisputeDecision::Won + ), + ); + }); + } + }; + } + + /// Admins can intervene on disputes only if certain conditions are met. That is if the + /// dispute has expired and does not have a clear overall decision. + /// + /// To encourage reusability, we actually rely on this blueprint being part of a service + /// component, just like the work blueprint. This method does a few steps to complete + /// dispute: + /// + /// 1. The associated `Work` NFT is marked as `Disputed` + /// 2. If the contractor has won, it compensates the contractor as agreed + /// 3. If the client has won, it refunds the client from the compensation agreed + /// 4. In both cases, it calls the service component to create a `DisputeOutcome` NFT + /// for both the `Client` and `Contractor` indicating if they `Won` or `Lost` + /// respectively + /// + /// # Panics: + /// - if user is not an admin + /// - if work is not in dispute + /// - if dispute has not expired or has a clear overall decision + pub fn complete_dispute_as_admin( + &mut self, + dispute_id: NonFungibleId, + dispute_side: DisputeSide, + ) { + let dispute_resource_manager = borrow_resource_manager!(self.dispute_resource); + + let dispute = dispute_resource_manager.get_non_fungible_data::(&dispute_id); + + let work_resource_manager = borrow_resource_manager!(dispute.work.resource_address()); + + let work_nft = work_resource_manager + .get_non_fungible_data::(&dispute.work.non_fungible_id()); + + assert!( + work_nft.work_status == WorkStatus::InDispute, + "work must be in dispute" + ); + + let is_dispute_expired = Runtime::current_epoch() > dispute.expiration; + + let clients_on_contractor_side = dispute + .participant_clients + .values() + .into_iter() + .filter(|client| **client == DisputeSide::Contractor) + .count(); + let contractors_on_contractor_side = dispute + .participant_contractors + .values() + .into_iter() + .filter(|contractor| **contractor == DisputeSide::Contractor) + .count(); + + let total_participants = + dispute.participant_clients.len() + dispute.participant_contractors.len(); + + let total_on_contractor = clients_on_contractor_side + contractors_on_contractor_side; + + let total_on_client = total_participants - total_on_contractor; + + let has_contractor_won_dispute = + ((Decimal::from(total_on_contractor) / Decimal::from(total_participants)) * 100) + > dec!(50); + + let has_client_won_dispute = + ((Decimal::from(total_on_client) / Decimal::from(total_participants)) * 100) + > dec!(50); + + let is_split_decision = (!has_client_won_dispute && !has_client_won_dispute) + || (has_client_won_dispute && has_contractor_won_dispute); + + assert!( + is_dispute_expired && is_split_decision, + "conditions for admin action not met" + ); + + self.service_auth.authorize(|| { + work_resource_manager.update_non_fungible_data( + &dispute.work.non_fungible_id(), + Work { + client: work_nft.client, + total_compensation: work_nft.total_compensation, + work_title: work_nft.work_title, + work_description: work_nft.work_description, + work_status: WorkStatus::Disputed, + skills_required: work_nft.skills_required, + contractor_requests: work_nft.contractor_requests, + contractor_assigned: work_nft.contractor_assigned, + }, + ); + }); + + let work_compensation = work_nft.total_compensation; + + match dispute_side { + DisputeSide::Contractor => self.service_auth.authorize(|| { + borrow_component!(self.service_component).call::<()>( + "compensate_contractor", + args!(dispute.client, dispute.contractor, work_compensation), + ); + + borrow_component!(self.service_component).call::<()>( + "complete_dispute_outcome", + args!( + DisputeSide::Contractor, + dispute.contractor, + dispute.work, + DisputeDecision::Won + ), + ); + + borrow_component!(self.service_component).call::<()>( + "complete_dispute_outcome", + args!( + DisputeSide::Client, + dispute.client, + dispute.work, + DisputeDecision::Lost + ), + ); + }), + DisputeSide::Client => { + self.service_auth.authorize(|| { + borrow_component!(self.service_component) + .call::<()>("refund_client", args!(dispute.client, work_compensation)); + + borrow_component!(self.service_component).call::<()>( + "complete_dispute_outcome", + args!( + DisputeSide::Contractor, + dispute.contractor, + dispute.work, + DisputeDecision::Lost + ), + ); + + borrow_component!(self.service_component).call::<()>( + "complete_dispute_outcome", + args!( + DisputeSide::Client, + dispute.client, + dispute.work, + DisputeDecision::Won + ), + ); + }); + } + }; + } + } +} diff --git a/6-nfts-for-financial-applications/moonwork/src/lib.rs b/6-nfts-for-financial-applications/moonwork/src/lib.rs new file mode 100644 index 000000000..655d53555 --- /dev/null +++ b/6-nfts-for-financial-applications/moonwork/src/lib.rs @@ -0,0 +1,5 @@ +pub mod dispute; +pub mod moonwork; +pub mod promotion; +pub mod users; +pub mod work; diff --git a/6-nfts-for-financial-applications/moonwork/src/moonwork.rs b/6-nfts-for-financial-applications/moonwork/src/moonwork.rs new file mode 100644 index 000000000..28c56d5d1 --- /dev/null +++ b/6-nfts-for-financial-applications/moonwork/src/moonwork.rs @@ -0,0 +1,777 @@ +use crate::dispute::{DisputeServiceComponent, DisputeSide, ParticipantCriteria}; +use crate::promotion::PromotionServiceComponent; +use crate::users::{Client, Contractor}; +use crate::work::WorkServiceComponent; +use scrypto::prelude::*; + +#[derive(Debug, NonFungibleData)] +struct CompletedWork { + category_resource: ResourceAddress, + work_id: NonFungibleId, + total_compensation: Decimal, +} + +#[derive(Debug, Describe, TypeId, Encode, Decode, PartialEq, Eq)] +#[repr(u64)] +pub enum Accolade { + NewMoon = 0, + WaxingCrescent, + FirstQuarter, + WaxingGibbous, + FullMoon, +} + +impl Accolade { + pub fn get_accolade(jobs_completed: u64) -> Accolade { + if jobs_completed >= Self::FullMoon as u64 * 10 { + Self::FullMoon + } else { + let accolade_index = jobs_completed / 10; + unsafe { std::mem::transmute(accolade_index) } + } + } +} + +#[derive(Debug, NonFungibleData)] +pub struct ContractorAccolades { + pub accolade: Accolade, + pub work_ids: Vec, +} + +#[derive(Debug, Describe, TypeId, Encode, Decode, PartialEq, Eq)] +pub enum DisputeDecision { + Won, + Lost, +} + +#[derive(Debug, NonFungibleData)] +pub struct DisputeOutcome { + pub work: NonFungibleAddress, + pub decision: DisputeDecision, +} + +blueprint! { + struct MoonWorkService { + contractor: ResourceAddress, + client: ResourceAddress, + admin_resource: ResourceAddress, + moon_work_auth: Vault, + // KeyValueStore that stores work compensation + client_work_vault: KeyValueStore, + // KeyValueStore that stores work refunds as a result of + // disputes or removed work + client_withdrawable_vault: KeyValueStore, + // KeyValueStore that stores all compensation as a result + // of finished work + contractor_vault: KeyValueStore, + // KeyValueStore that stores `CompletedWork` NFT that is + // later claimed + completed_work_vault: KeyValueStore, + completed_work_resource: ResourceAddress, + dispute_outcome_resource: ResourceAddress, + // KeyValueStore that stores all dispute outcomes as a result + // of completed disputes + client_dispute_outcome_vault: KeyValueStore, + // KeyValueStore that stores all dispute outcomes as a result + // of completed disputes + contractor_dispute_outcome_vault: KeyValueStore, + contractor_accolade_resource: ResourceAddress, + service_fee: Decimal, + minimum_work_payment: Decimal, + service_vault: Vault, + dispute_participant_criteria: ParticipantCriteria, + } + + impl MoonWorkService { + /// Creates the whole MoonWork Service that encompasses the creation of Client, Contractor, + /// WorkCompleted and DisputeOutcome NFT resources, all of which are soulbound NFTs. + /// Alongside the resources, we have 2 additional components representing the work category + /// system and dispute system for a given work resource. + /// + /// The purpose for leveraging all these NFT resources is to show a user's personal record + /// as well as verifying their record by checking a Client's Work NFT balance or + /// a Contractor's WorkCompleted NFT balance. And if there are any disputes, DisputeOutcome + /// NFTs are also shown as a soulbound token in your wallet. + /// + /// - `service_fee` a service is taken only when work has successfully completed + /// - `minimum_work_payment` this is required as a guard rail to stop disputes from being + /// gamed + /// - `dispute_participant_criteria` the number of participants on both sides, and + /// requirements for clients and contractors to have work/jobs completed + pub fn create( + service_fee: Decimal, + minimum_work_payment: Decimal, + dispute_participant_criteria: ParticipantCriteria, + ) -> (ComponentAddress, Bucket) { + let moon_work_auth: Bucket = ResourceBuilder::new_fungible().initial_supply(1); + + let service_admin: Bucket = ResourceBuilder::new_fungible() + .metadata("name", "Admin Badge") + .metadata("service", "MoonWork") + .mintable(rule!(require(moon_work_auth.resource_address())), LOCKED) + .burnable(rule!(require(moon_work_auth.resource_address())), LOCKED) + .restrict_withdraw(rule!(deny_all), LOCKED) + .initial_supply(1); + + let contractor = ResourceBuilder::new_non_fungible() + .metadata("name", "Contractor") + .metadata("service", "MoonWork") + .mintable( + rule!(require_any_of(vec![ + moon_work_auth.resource_address(), + service_admin.resource_address() + ])), + LOCKED, + ) + .updateable_non_fungible_data( + rule!(require_any_of(vec![ + moon_work_auth.resource_address(), + service_admin.resource_address() + ])), + LOCKED, + ) + .restrict_withdraw(rule!(deny_all), LOCKED) + .no_initial_supply(); + + let client = ResourceBuilder::new_non_fungible() + .metadata("name", "Client") + .metadata("service", "MoonWork") + .mintable( + rule!(require_any_of(vec![ + moon_work_auth.resource_address(), + service_admin.resource_address() + ])), + LOCKED, + ) + .updateable_non_fungible_data( + rule!(require_any_of(vec![ + moon_work_auth.resource_address(), + service_admin.resource_address() + ])), + LOCKED, + ) + .restrict_withdraw(rule!(deny_all), LOCKED) + .no_initial_supply(); + + let completed_work_resource = ResourceBuilder::new_non_fungible() + .metadata("name", "Completed Work") + .metadata("service", "MoonWork") + .mintable( + rule!(require_any_of(vec![ + moon_work_auth.resource_address(), + service_admin.resource_address() + ])), + LOCKED, + ) + .restrict_withdraw( + rule!(require_any_of(vec![ + service_admin.resource_address(), + moon_work_auth.resource_address() + ])), + LOCKED, + ) + .no_initial_supply(); + + let dispute_outcome_resource = ResourceBuilder::new_non_fungible() + .metadata("name", "Dispute Outcome") + .metadata("service", "MoonWork") + .mintable( + rule!(require_any_of(vec![ + moon_work_auth.resource_address(), + service_admin.resource_address() + ])), + LOCKED, + ) + .restrict_withdraw( + rule!(require_any_of(vec![ + service_admin.resource_address(), + moon_work_auth.resource_address() + ])), + LOCKED, + ) + .no_initial_supply(); + + let contractor_accolade_resource = ResourceBuilder::new_non_fungible() + .metadata("name", "Contractor Accolade") + .metadata("service", "MoonWork") + .mintable( + rule!(require_any_of(vec![ + moon_work_auth.resource_address(), + service_admin.resource_address() + ])), + LOCKED, + ) + .burnable( + rule!(require_any_of(vec![ + moon_work_auth.resource_address(), + service_admin.resource_address() + ])), + LOCKED, + ) + .updateable_non_fungible_data( + rule!(require_any_of(vec![ + moon_work_auth.resource_address(), + service_admin.resource_address() + ])), + LOCKED, + ) + .restrict_withdraw(rule!(deny_all), LOCKED) + .no_initial_supply(); + + let access_rules = AccessRules::new() + .method( + "deposit_compensation", + rule!(require(service_admin.resource_address())), + ) + .method( + "create_new_category", + rule!(require(service_admin.resource_address())), + ) + .method( + "finalise_work", + rule!(require(service_admin.resource_address())), + ) + .method( + "update_dispute_participant_criteria", + rule!(require(service_admin.resource_address())), + ) + .method( + "refund_client", + rule!(require(service_admin.resource_address())), + ) + .method( + "complete_dispute_outcome", + rule!(require(service_admin.resource_address())), + ) + .method( + "is_client_enabled", + rule!(require(service_admin.resource_address())), + ) + .method( + "is_contractor_enabled", + rule!(require(service_admin.resource_address())), + ) + .method( + "create_promotion_service", + rule!(require(service_admin.resource_address())), + ) + .default(AccessRule::AllowAll); + + let mut component = Self { + service_fee, + minimum_work_payment, + contractor: contractor, + client: client, + client_work_vault: KeyValueStore::new(), + client_withdrawable_vault: KeyValueStore::new(), + contractor_vault: KeyValueStore::new(), + completed_work_vault: KeyValueStore::new(), + completed_work_resource, + dispute_outcome_resource, + client_dispute_outcome_vault: KeyValueStore::new(), + contractor_dispute_outcome_vault: KeyValueStore::new(), + contractor_accolade_resource, + admin_resource: service_admin.resource_address(), + moon_work_auth: Vault::with_bucket(moon_work_auth), + dispute_participant_criteria, + service_vault: Vault::new(RADIX_TOKEN), + } + .instantiate(); + + component.add_access_check(access_rules); + let service_component = component.globalize(); + + (service_component, service_admin) + } + + /// This creates a basic promotion service with the requirements for contractors to promote + /// themselves in the service. This is an example of how we would leverage the NFTs from + /// `CompletedWork` and `ContractorAccolades` as a form of identity verification and + /// requirements. + /// + /// # Panics: + /// - if user is does not have admin in AuthZone + pub fn create_promotion_service(&self) -> ComponentAddress { + let service_auth = self + .moon_work_auth + .authorize(|| borrow_resource_manager!(self.admin_resource).mint(dec!(1))); + + PromotionServiceComponent::create( + self.contractor, + service_auth, + Runtime::actor().as_component().0, + self.completed_work_resource, + self.contractor_accolade_resource, + 20, // requires 20 work completed + 3, // requires all accolades up to ContractorAccolades::FirstQuarter + 10, + ) + } + + /// Register as a contractor which mints a Contractor NFT. This is used as a badge for work + /// and dispute services. The ID is the username/alias the user gives so no duplicate + /// usernames/aliases are allowed. + /// + /// A few resources are setup in addition: + /// 1. CompletedWork NFT is minted indicating that they have started their Contractor journey. + /// 2. Creates a contractor vault for their earnings + /// 3. Creates a work completed vault for all work successfully completed + /// 4. Creates a dispute outcome vault for all dispute outcomes that has taken place + /// + /// # Panics + /// - if username is taken + pub fn register_as_contractor(&mut self, username: String) -> (Bucket, Bucket) { + let id = NonFungibleId::from_bytes(username.as_bytes().to_vec()); + self.contractor_vault + .insert(id.clone(), Vault::new(RADIX_TOKEN)); + self.completed_work_vault + .insert(id.clone(), Vault::new(self.completed_work_resource)); + self.contractor_dispute_outcome_vault + .insert(id.clone(), Vault::new(self.dispute_outcome_resource)); + self.moon_work_auth.authorize(|| { + let contractor_badge = borrow_resource_manager!(self.contractor).mint_non_fungible( + &id, + Contractor { + jobs_completed: 0, + total_worth: Decimal::zero(), + disputed: 0, + }, + ); + + let accolade_resource_manager = + borrow_resource_manager!(self.contractor_accolade_resource); + + let id = NonFungibleId::from_u64( + (accolade_resource_manager.total_supply() + 1) + .to_string() + .parse() + .unwrap(), + ); + + let accolade = accolade_resource_manager.mint_non_fungible( + &id, + ContractorAccolades { + accolade: Accolade::NewMoon, + work_ids: vec![], + }, + ); + + (contractor_badge, accolade) + }) + } + + /// Register as a Client which mints a Client NFT. + /// + /// A few resources are setup in addition: + /// 1. Creates a client vault where all work created will have all payments upfront stored + /// in this vault + /// 2. Creates a withdrawable vault where refunds as a result of disputes or removed work + /// are stored + /// 3. Creates a dispute outcome vault for all dispute outcomes that has taken place + /// + /// # Panics + /// - if username is taken + pub fn register_as_client(&mut self, username: String) -> Bucket { + let id = NonFungibleId::from_bytes(username.as_bytes().to_vec()); + self.client_work_vault + .insert(id.clone(), Vault::new(RADIX_TOKEN)); + self.client_withdrawable_vault + .insert(id.clone(), Vault::new(RADIX_TOKEN)); + self.client_dispute_outcome_vault + .insert(id.clone(), Vault::new(self.dispute_outcome_resource)); + self.moon_work_auth.authorize(|| { + borrow_resource_manager!(self.client).mint_non_fungible( + &id, + Client { + jobs_created: 0, + jobs_paid_out: 0, + total_paid_out: Decimal::zero(), + disputed: 0, + }, + ) + }) + } + + /// Gets the minimum payment amount, this is required for the work components which are + /// used to enforce the work raised must be at or above the minimum work amount + pub fn minimum_work_payment_amount(&self) -> Decimal { + self.minimum_work_payment + } + + pub fn is_client_enabled(&self, client_id: NonFungibleId) -> bool { + let dispute_outcome_amount = self + .client_dispute_outcome_vault + .get(&client_id) + .unwrap() + .amount(); + + dispute_outcome_amount == Decimal::zero() + } + + pub fn is_contractor_enabled(&self, contractor_id: NonFungibleId) -> bool { + let dispute_outcome_amount = self + .contractor_dispute_outcome_vault + .get(&contractor_id) + .unwrap() + .amount(); + + dispute_outcome_amount == Decimal::zero() + } + + /// When work is created, the compensation is dposited into the corresponding client vault + /// + /// # Panics: + /// - if caller does not have a service badge resource in Auth Zone + pub fn deposit_compensation(&mut self, client_id: NonFungibleId, compensation: Bucket) { + self.client_work_vault + .get_mut(&client_id) + .unwrap() + .put(compensation); + } + + /// This is called when work has been successfully completed as a result of `Client` and + /// `Contractor` agreeing. This method ensures the compensation from a client vault + /// transfers the compensation to the contractor vault. + /// + /// # Panics: + /// - if caller does not have a service badge resource in Auth Zone + pub fn compensate_contractor( + &mut self, + client_id: NonFungibleId, + contractor_id: NonFungibleId, + total_compensation: Decimal, + ) { + let mut contractor_compensation = self + .client_work_vault + .get_mut(&client_id) + .unwrap() + .take(total_compensation); + + let service_fee_amount = (self.service_fee / 100) * contractor_compensation.amount(); + + self.service_vault + .put(contractor_compensation.take(service_fee_amount)); + + self.contractor_vault + .get_mut(&contractor_id) + .unwrap() + .put(contractor_compensation); + } + + /// Due to a dispute or removing work, this method is called. As a result of this, the + /// client work originally meant for a contractor's work is transferred to the client + /// withdrawable vault + /// + /// # Panics: + /// - if caller does not have a service badge resource in Auth Zone + pub fn refund_client(&mut self, client_id: NonFungibleId, refund_amount: Decimal) { + let client_work_refund_bucket = self + .client_work_vault + .get_mut(&client_id) + .unwrap() + .take(refund_amount); + + self.client_withdrawable_vault + .get_mut(&client_id) + .unwrap() + .put(client_work_refund_bucket); + } + + /// Assuming disputes have been handled and client has won the dispute or if the client + /// created work and wanted to remove work, all the amounts are then transferred to + /// a refund vault. This method does the following: + /// + /// 1. Get all refunds its owed as a result of the reasons said above + /// 2. Returns a soulbound Dispute NFT with their record and result + /// + /// # Panics: + /// - if `Client` proof has incorrect `ResourceAddress` + pub fn claim_client_work_refund(&mut self, client: Proof) -> (Bucket, Bucket) { + let validated_client = client + .validate_proof(ProofValidationMode::ValidateContainsAmount( + self.client, + dec!(1), + )) + .expect("unauthorized access"); + + let client_refund = self + .client_withdrawable_vault + .get_mut(&validated_client.non_fungible_id()) + .unwrap() + .take_all(); + + let completed_dispute = self.moon_work_auth.authorize(|| { + self.client_dispute_outcome_vault + .get_mut(&validated_client.non_fungible_id()) + .unwrap() + .take_all() + }); + + let client_resource_manager = borrow_resource_manager!(self.client); + + let mut client = client_resource_manager + .get_non_fungible_data::(&validated_client.non_fungible_id()); + + client.disputed += completed_dispute + .amount() + .to_string() + .parse::() + .unwrap(); + + self.moon_work_auth.authorize(|| { + client_resource_manager + .update_non_fungible_data(&validated_client.non_fungible_id(), client); + }); + + (client_refund, completed_dispute) + } + + /// When work has been completed successfully, we mint a WorkCompleted NFT to indicate + /// this. Because this only happens when called by a work component, the `CompletedWork` is + /// stored in a completed work vault + /// + /// # Panics: + /// - if `Contractor` has an incorrect `ResourceAddress` + pub fn finalise_work( + &mut self, + category_resource: ResourceAddress, + work_id: NonFungibleId, + total_compensation: Decimal, + contractor: NonFungibleId, + ) { + let completed_work_resource_manager = + borrow_resource_manager!(self.completed_work_resource); + + let id = NonFungibleId::from_u64( + (completed_work_resource_manager.total_supply() + 1) + .to_string() + .parse() + .unwrap(), + ); + + let completed_work = self.moon_work_auth.authorize(|| { + completed_work_resource_manager.mint_non_fungible( + &id, + CompletedWork { + category_resource, + work_id, + total_compensation, + }, + ) + }); + + self.completed_work_vault + .get_mut(&contractor) + .unwrap() + .put(completed_work); + } + + /// This is the meat of the additional rewards system for Contractors. This method does + /// a few things under the hood: + /// + /// 1. We return all payment compensation (XRD) + /// 2. We return all `CompletedWork` NFTs + /// 3. For every 10 work completed, we mint an `ContractorAccolades` NFT with all the Work + /// IDs used to win that accolade. Because of this, its a truly unique NFT for the + /// individual Contractor + /// 4. We also return all `DisputeOutcome` NFTs for every dispute they Won/Lost + /// respectively + pub fn claim_contractor_compensation( + &mut self, + contractor: Proof, + ) -> (Bucket, Bucket, Bucket, Bucket) { + let validated_contractor = contractor + .validate_proof(ProofValidationMode::ValidateContainsAmount( + self.contractor, + dec!(1), + )) + .expect("unauthorized access"); + let contractor_compensation = self + .contractor_vault + .get_mut(&validated_contractor.non_fungible_id()) + .unwrap() + .take_all(); + + let completed_work = self.moon_work_auth.authorize(|| { + let completed_work = self + .completed_work_vault + .get_mut(&validated_contractor.non_fungible_id()) + .unwrap() + .take_all(); + + completed_work + }); + + let contractor_resource_manager = borrow_resource_manager!(self.contractor); + + let contractor_nft = contractor_resource_manager + .get_non_fungible_data::(&validated_contractor.non_fungible_id()); + + let mut current_acollade = Accolade::get_accolade(contractor_nft.jobs_completed); + let mut new_accolades: Vec = vec![]; + let mut work_ids: Vec = vec![]; + + let mut updated_contractor = completed_work + .non_fungibles::() + .iter() + .fold(contractor_nft, |mut contractor, work| { + let data = work.data(); + contractor.jobs_completed = contractor.jobs_completed + 1; + contractor.total_worth = contractor.total_worth + data.total_compensation; + + work_ids.push(work.address()); + + if current_acollade != Accolade::get_accolade(contractor.jobs_completed) { + new_accolades.push(ContractorAccolades { + accolade: Accolade::get_accolade(contractor.jobs_completed), + work_ids: work_ids.clone(), + }); + current_acollade = Accolade::get_accolade(contractor.jobs_completed); + work_ids = vec![]; + } + + contractor + }); + + let contractor_accolade_resource_manager = + borrow_resource_manager!(self.contractor_accolade_resource); + + let mut accolade_bucket = Bucket::new(self.contractor_accolade_resource); + + let completed_dispute = self.moon_work_auth.authorize(|| { + self.contractor_dispute_outcome_vault + .get_mut(&validated_contractor.non_fungible_id()) + .unwrap() + .take_all() + }); + + updated_contractor.disputed += completed_dispute + .amount() + .to_string() + .parse::() + .unwrap(); + + self.moon_work_auth.authorize(|| { + new_accolades.into_iter().for_each(|accolade| { + let id = NonFungibleId::from_u64( + (contractor_accolade_resource_manager.total_supply() + 1) + .to_string() + .parse() + .unwrap(), + ); + + let new_accolade = + contractor_accolade_resource_manager.mint_non_fungible(&id, accolade); + accolade_bucket.put(new_accolade); + }); + contractor_resource_manager.update_non_fungible_data( + &validated_contractor.non_fungible_id(), + updated_contractor, + ); + }); + + ( + contractor_compensation, + completed_work, + accolade_bucket, + completed_dispute, + ) + } + + /// This is where we create 2 components to make sure we keep components scalable. For + /// a new work category, this method: + /// + /// 1. Creates a new `WorkServiceComponent` + /// 2. Creates a corresponding `DisputeServiceComponent` + /// + /// # Panics: + /// - if admin badge is not in the Auth Zone + pub fn create_new_category( + &mut self, + work_type: String, + ) -> (ComponentAddress, ComponentAddress) { + let mut service_auth = self + .moon_work_auth + .authorize(|| borrow_resource_manager!(self.admin_resource).mint(dec!(2))); + + let (moon_work_component_address, _, _) = Runtime::actor().as_component(); + + let work_component = WorkServiceComponent::create( + work_type, + moon_work_component_address, + service_auth.take(1), + self.client, + self.contractor, + ); + + let work_service: WorkServiceComponent = work_component.into(); + + let dispute_service = DisputeServiceComponent::create( + service_auth, + self.client, + self.contractor, + work_service.get_work_resource(), + moon_work_component_address, + ); + + (work_component, dispute_service) + } + + /// Gets the participant criteria used for disputes + pub fn get_dispute_participant_criteria(&self) -> ParticipantCriteria { + self.dispute_participant_criteria + } + + /// Updates the participant criteria used for disputes + pub fn update_dispute_participant_criteria( + &mut self, + participant_criteria: ParticipantCriteria, + ) { + self.dispute_participant_criteria = participant_criteria; + } + + /// This completes the outcome assuming the `DisputeServiceComponent` has reached a verdict + /// for both `Client` and `Contractor`. This method simply mints a `DisputeOutcome` NFT + /// which is then put into the corresponding `Client` or `Contractor` dispute outcome vault + /// + /// # Panics: + /// - if admin badge is not in Auth Zone + pub fn complete_dispute_outcome( + &mut self, + dispute_side: DisputeSide, + client_or_contractor_id: NonFungibleId, + work: NonFungibleAddress, + decision: DisputeDecision, + ) { + let dispute_outcome_resource_manager = + borrow_resource_manager!(self.dispute_outcome_resource); + + let id = NonFungibleId::from_u64( + (dispute_outcome_resource_manager.total_supply() + 1) + .to_string() + .parse() + .unwrap(), + ); + + let completed_dispute = self.moon_work_auth.authorize(|| { + dispute_outcome_resource_manager + .mint_non_fungible(&id, DisputeOutcome { work, decision }) + }); + + match dispute_side { + DisputeSide::Contractor => { + self.contractor_dispute_outcome_vault + .get_mut(&client_or_contractor_id) + .unwrap() + .put(completed_dispute); + } + DisputeSide::Client => { + self.client_dispute_outcome_vault + .get_mut(&client_or_contractor_id) + .unwrap() + .put(completed_dispute); + } + } + } + } +} diff --git a/6-nfts-for-financial-applications/moonwork/src/promotion.rs b/6-nfts-for-financial-applications/moonwork/src/promotion.rs new file mode 100644 index 000000000..d03316d88 --- /dev/null +++ b/6-nfts-for-financial-applications/moonwork/src/promotion.rs @@ -0,0 +1,240 @@ +use scrypto::prelude::*; + +// Assuming an epoch interval is 30mins, 10 days expiry time +const PROMOTION_EXPIRATION_TIME: u64 = 480; + +#[derive(Debug, NonFungibleData)] +pub struct PromotedContractor { + pub expiry: u64, + pub contractor_id: NonFungibleId, +} + +blueprint! { + struct PromotionService { + contractor: ResourceAddress, + service_auth: Vault, + service_component: ComponentAddress, + promoted_contractors: Vault, + // because we burn, its unreliable to use total_supply + 1 for non fungible ids, so this + // is another way of minting a unique id everytime + latest_promoted_contractors_id: u64, + completed_work_required: u8, + accolades_required: u8, + promoted_contractors_limit: u64, + completed_work_resource: ResourceAddress, + accolades_resource: ResourceAddress, + } + + impl PromotionService { + /// This is an example of how to then leverage and verify a contractor's credentials and + /// work done by presenting WorkCompleted NFTs and ContractorAccolades NFTs. This can be + /// further evolved by allow client's work to be promoted as well given some requirements + /// such as: number of work finish must exceed 10, as an example + pub fn create( + contractor: ResourceAddress, + service_badge: Bucket, + service_component: ComponentAddress, + completed_work_resource: ResourceAddress, + accolades_resource: ResourceAddress, + completed_work_required: u8, + accolades_required: u8, + promoted_contractors_limit: u64, + ) -> ComponentAddress { + let promoted_contractors = ResourceBuilder::new_non_fungible() + .metadata("name", "Promoted Contractors") + .mintable(rule!(require(service_badge.resource_address())), LOCKED) + .burnable(rule!(require(service_badge.resource_address())), LOCKED) + .updateable_non_fungible_data( + rule!(require(service_badge.resource_address())), + LOCKED, + ) + .no_initial_supply(); + + let access_rules = AccessRules::new() + // Ideally wanted to use access rules to enforce number of completed work and + // accolades required + // .method( + // "promote_contractor", + // rule!( + // require_n_of("completed_work_required", vec![completed_work]) + // && require_n_of("accolades_required", vec![accolade]) + // ), + // ) + .method( + "update_contractor_promotion_requirements", + rule!(require(service_badge.resource_address())), + ) + .method( + "remove_expired_promotions", + rule!(require(service_badge.resource_address())), + ) + .default(AccessRule::AllowAll); + + let mut component = Self { + contractor, + service_auth: Vault::with_bucket(service_badge), + service_component, + promoted_contractors: Vault::new(promoted_contractors), + completed_work_resource, + accolades_resource, + completed_work_required, + accolades_required, + promoted_contractors_limit, + latest_promoted_contractors_id: 0, + } + .instantiate(); + + component.add_access_check(access_rules); + + component.globalize() + } + + /// Simple admin method which is used to tweak requirements for being able to promote + /// a contractor. + pub fn update_contractor_promotion_requirements( + &mut self, + new_completed_work: u8, + accolades_required: u8, + ) { + self.completed_work_required = new_completed_work; + self.accolades_required = accolades_required; + } + + /// Its important to call this every so often, not really thought out. Perhaps while + /// querying using the gateway API, checks if there has been a build up of expired + /// promotions and must be cleaned up in an automated manner. + pub fn remove_expired_promotions(&mut self) { + let expired_promoted_contractors: Vec = self + .promoted_contractors + .non_fungibles::() + .iter() + .filter(|promoted_contractor| { + Runtime::current_epoch() > promoted_contractor.data().expiry + }) + .map(|promoted_contractor| promoted_contractor.id()) + .collect(); + + self.service_auth.authorize(|| { + let resource_manager = + borrow_resource_manager!(self.promoted_contractors.resource_address()); + for expired_promotions in expired_promoted_contractors { + resource_manager.burn( + self.promoted_contractors + .take_non_fungible(&expired_promotions), + ); + } + }) + } + + /// Contractors must present their work done and accolades collected so they have the + /// priviledge to promote themselves. Ideally we should use access rules instead of + /// explicit proof validations on a method level. + /// + /// # Panics: + /// - if not enough work completed proofs are given + /// - if not enough accolade proofs are given + /// - if the limit for promotions has been reached + /// - if contractor is already promoted + pub fn promote_contractor( + &mut self, + work_completed: Proof, + accolades: Proof, + contractor: Proof, + ) { + let has_work_completed = self.validate_enough_work_completed(work_completed); + let has_accolades = self.validate_enough_accolades_earned(accolades); + + assert!( + has_work_completed.is_ok() && has_accolades.is_ok(), + "not enough work" + ); + + let validated_contractor = self + .validate_contractor(contractor) + .expect("unauthorized access"); + + let non_expired_promoted_contractors = self.get_non_expired_promoted_contractors(); + + assert!( + non_expired_promoted_contractors + .iter() + .find(|c| c.data().contractor_id == validated_contractor.non_fungible_id()) + .is_none(), + "already promoted" + ); + + let non_expired_promoted_count = non_expired_promoted_contractors.len(); + + assert!( + non_expired_promoted_count as u64 != self.promoted_contractors_limit, + "promoted limit reached" + ); + + let promoted_contractor = self.mint_promotion_for_contractor(validated_contractor); + + self.promoted_contractors.put(promoted_contractor); + } + + fn mint_promotion_for_contractor( + &mut self, + validated_contractor: ValidatedProof, + ) -> Bucket { + self.latest_promoted_contractors_id = self.latest_promoted_contractors_id + 1; + + let id = NonFungibleId::from_u64(self.latest_promoted_contractors_id); + self.service_auth.authorize(|| { + borrow_resource_manager!(self.promoted_contractors.resource_address()) + .mint_non_fungible( + &id, + PromotedContractor { + expiry: Runtime::current_epoch() + PROMOTION_EXPIRATION_TIME, + contractor_id: validated_contractor.non_fungible_id(), + }, + ) + }) + } + + fn get_non_expired_promoted_contractors(&mut self) -> Vec> { + let promoted_contractors = self + .promoted_contractors + .non_fungibles::(); + let non_expired_promoted_contractors = + promoted_contractors + .into_iter() + .filter(|promoted_contractor| { + Runtime::current_epoch() <= promoted_contractor.data().expiry + }); + non_expired_promoted_contractors.collect() + } + + fn validate_contractor( + &mut self, + contractor: Proof, + ) -> Result { + contractor.validate_proof(ProofValidationMode::ValidateContainsAmount( + self.contractor, + dec!(1), + )) + } + + fn validate_enough_accolades_earned( + &mut self, + accolades: Proof, + ) -> Result { + accolades.validate_proof(ProofValidationMode::ValidateContainsAmount( + self.accolades_resource, + self.accolades_required.into(), + )) + } + + fn validate_enough_work_completed( + &mut self, + work_completed: Proof, + ) -> Result { + work_completed.validate_proof(ProofValidationMode::ValidateContainsAmount( + self.completed_work_resource, + self.completed_work_required.into(), + )) + } + } +} diff --git a/6-nfts-for-financial-applications/moonwork/src/users.rs b/6-nfts-for-financial-applications/moonwork/src/users.rs new file mode 100644 index 000000000..c3c24e6dc --- /dev/null +++ b/6-nfts-for-financial-applications/moonwork/src/users.rs @@ -0,0 +1,23 @@ +use scrypto::prelude::*; + +#[derive(Debug, NonFungibleData)] +pub struct Client { + #[scrypto(mutable)] + pub jobs_paid_out: u64, + #[scrypto(mutable)] + pub jobs_created: u64, + #[scrypto(mutable)] + pub total_paid_out: Decimal, + #[scrypto(mutable)] + pub disputed: u64, +} + +#[derive(Debug, NonFungibleData)] +pub struct Contractor { + #[scrypto(mutable)] + pub jobs_completed: u64, + #[scrypto(mutable)] + pub total_worth: Decimal, + #[scrypto(mutable)] + pub disputed: u64, +} diff --git a/6-nfts-for-financial-applications/moonwork/src/work.rs b/6-nfts-for-financial-applications/moonwork/src/work.rs new file mode 100644 index 000000000..0bb8a5170 --- /dev/null +++ b/6-nfts-for-financial-applications/moonwork/src/work.rs @@ -0,0 +1,638 @@ +use crate::users::{Client, Contractor}; +use scrypto::prelude::*; + +#[derive(Debug, Describe, TypeId, Encode, Decode, PartialEq, Eq)] +pub enum WorkStatus { + NotStarted, + InProgress, + Finished, + InDispute, + Disputed, + Delisted, +} + +#[derive(Debug, NonFungibleData)] +pub struct Work { + pub client: NonFungibleId, + pub total_compensation: Decimal, + pub work_title: String, + pub work_description: String, + #[scrypto(mutable)] + pub work_status: WorkStatus, + pub skills_required: Vec, + #[scrypto(mutable)] + pub contractor_requests: HashMap, + #[scrypto(mutable)] + pub contractor_assigned: Option, +} + +blueprint! { + struct WorkService { + contractor: ResourceAddress, + client: ResourceAddress, + work_resource: ResourceAddress, + service_auth: Vault, + service_component: ComponentAddress, + } + + impl WorkService { + /// Creates an independant work category. + /// + /// This is designed to be a reusable component so + /// callable methods can be made by creating a service componet blueprint + /// + /// # ParentComponent Methods to Implement: + /// ```ignore + /// use scrypto::prelude::*; + /// + /// blueprint! { + /// + /// struct Service { + /// } + /// + /// impl Service { + /// pub fn minimum_work_payment_amount(&self) -> Decimal { + /// todo!() + /// } + /// pub fn deposit_compensation(&self, contractor_id: NonFungibleId, xrd_payment: Bucket) { + /// todo!() + /// } + /// pub fn refund_client(&self, client_id: NonFungibleId, refund_amount: Bucket) { + /// todo!() + /// } + /// pub fn compensate_contractor( + /// &self, + /// client_id: NonFungibleId, + /// contractor_id: NonFungibleId, + /// total_compensation: Decimal + /// ) { + /// todo!() + /// } + /// pub fn finalise_work( + /// &self, + /// work_resource: ResourceAddress, + /// work_id: NonFungibleId, + /// total_compensation: Decimal, + /// contractor_id: NonFungibleId + /// ) { + /// todo!() + /// } + /// pub fn is_client_enabled( + /// &self, + /// client_id: NonFungibleId + /// ) -> bool { + /// todo!() + /// } + /// pub fn is_contractor_enabled( + /// &self, + /// contractor_id: NonFungibleId + /// ) -> bool { + /// todo!() + /// } + /// } + /// } + /// ``` + /// + /// # Arguments: + /// - `category` name of the work category, for example Development & IT, Accounting + /// & Finance etc + /// - `service_component` the parent component, in this case moonwork service, you will + /// require certain methods to be implemented if you wanted your own service + /// - `service_badge` the service badge is required in this service as we need access rules + /// to update resources, it also has access to work resource. Alongside this, we need to be + /// able to call moonwork service methods which are only callable by admin + /// - `client` This resource must be an NFT + /// - `contractor` This resource must be an NFT + pub fn create( + category: String, + service_component: ComponentAddress, + service_badge: Bucket, + client: ResourceAddress, + contractor: ResourceAddress, + ) -> ComponentAddress { + let work_resource = ResourceBuilder::new_non_fungible() + .metadata("name", category) + .mintable(rule!(require(service_badge.resource_address())), LOCKED) + .burnable(rule!(require(service_badge.resource_address())), LOCKED) + .updateable_non_fungible_data( + rule!(require(service_badge.resource_address())), + LOCKED, + ) + .restrict_withdraw(rule!(deny_all), LOCKED) + .no_initial_supply(); + + let component = Self { + contractor, + client, + work_resource, + service_auth: Vault::with_bucket(service_badge), + service_component, + } + .instantiate(); + + component.globalize() + } + + /// Simply returns the work resource address, required for the service component + pub fn get_work_resource(&self) -> ResourceAddress { + self.work_resource + } + + /// Allows a client to create work for contractors to pick up. To keep the exchange as safe + /// as possible, the client needs the payment amount upfront which is then stored in + /// a client bucket. This method in summary does 2 things: + /// 1. Mint a Work NFT which is then returned to the Client as a **soulbound** token + /// 2. Takes XRD as the payment currency and is stored on a client payment vault which is + /// stored in a parent component + /// + /// # Arguments: + /// - `total_compensation` - The total compensation that the work pays out to be. This is + /// additionally validated by the xrd_payment bucket that is given matches the parameter + /// - `work_title` - Metadata representing the title + /// - `work_description` - Metadata representing a short description of the description of + /// work + /// - `skills_required` - Metadata list of keyword skillsets required by the contractor + /// - `client` - A proof representing the client badge + /// + /// # Panics: + /// - If uses a different resource as a proof + /// - If `xrd_payment` is not a `RADIX_TOKEN` resource + /// - If `xrd_payment` amount does not need minimum work payment + /// - If `xrd_payment` does is not equal to `total_compensation` + pub fn create_new_work( + &self, + total_compensation: Decimal, + work_title: String, + work_description: String, + skills_required: Vec, + xrd_payment: Bucket, + client: Proof, + ) -> Bucket { + let validated_client = self.validate_client(client).expect("unauthorized access"); + + self.service_auth.authorize(|| { + assert!( + borrow_component!(self.service_component).call::( + "is_client_enabled", + args!(validated_client.non_fungible_id()) + ), + "disputes pending withdrawal" + ); + }); + + assert!( + xrd_payment.resource_address() == RADIX_TOKEN, + "invalid payment" + ); + + let minimum_work_payment = borrow_component!(self.service_component) + .call::("minimum_work_payment_amount", args!()); + + assert!( + xrd_payment.amount() >= minimum_work_payment, + "minimum payment not met" + ); + assert!(xrd_payment.amount() == total_compensation, "invalid amount"); + + self.update_client_jobs_created(&validated_client); + + let work = self.mint_new_work( + &validated_client, + total_compensation, + work_description, + work_title, + skills_required, + ); + + self.service_auth.authorize(|| { + borrow_component!(self.service_component).call::<()>( + "deposit_compensation", + args!(validated_client.non_fungible_id(), xrd_payment), + ); + }); + + work + } + + /// Removes work only if it has not started. + /// + /// This method does 2 things: + /// - Update the Work NFT Work Status to `Delisted`. This is so that we can update the frontend + /// - Refunds the client by placing the total compensation amount to a client refund vault + /// + /// # Panics: + /// - if work is not a valid resource address + pub fn remove_work(&self, work: Proof) { + let validated_work = self.validate_work(work).expect("invalid work"); + + let work_nft = validated_work.non_fungible::().data(); + + self.service_auth.authorize(|| { + assert!( + borrow_component!(self.service_component) + .call::("is_client_enabled", args!(work_nft.client)), + "disputes pending withdrawal" + ); + }); + + self.update_work_delist(validated_work, work_nft); + } + + /// Contractor requests for work that has been raised by the client + /// + /// This method updates Work NFT to include contractor `NonFungibleId` into + /// `Work.contractor_requests` + /// + /// # Panics: + /// - if contractor proof is not the correct `ResourceAddress` + /// - if work has not already started + /// - if you try to request for a job you already requested + pub fn request_work(&self, work_id: NonFungibleId, contractor: Proof) { + let validated_contractor = self + .validate_contractor(contractor) + .expect("unauthorized access"); + + self.service_auth.authorize(|| { + assert!( + borrow_component!(self.service_component).call::( + "is_contractor_enabled", + args!(validated_contractor.non_fungible_id()) + ), + "disputes pending withdrawal" + ); + }); + + let work_resource_manager = borrow_resource_manager!(self.work_resource); + + let work = work_resource_manager.get_non_fungible_data::(&work_id); + + assert!( + work.work_status == WorkStatus::NotStarted, + "work must not be started" + ); + + assert!( + work.contractor_requests + .get(&validated_contractor.non_fungible_id()) + .is_none(), + "already requested job" + ); + + self.update_work_with_contractor_request( + work_resource_manager, + validated_contractor, + work, + work_id, + ); + } + + /// Assuming work has been requested by contractors, we accept a specific contractor to + /// take on the work. + /// + /// When client accepts a contractor for a particular work: + /// 1. Updates `Work.contractor_assigned` to the contractor `NonFungibleId` given + /// 2. Updates `Work.work_status` to `InProgress` + /// + /// # Panics: + /// - if client has an incorrrect `ResourceAddress` + /// - if contractor `NonFungibleId` is incorrect/does not exist + pub fn accept_contractor_for_work( + &self, + work: Proof, + contractor: NonFungibleId, + client: Proof, + ) { + let validated_client = self.validate_client(client).expect("unauthorized access"); + let validated_work = self.validate_work(work).expect("incorrect work"); + + self.service_auth.authorize(|| { + assert!( + borrow_component!(self.service_component).call::( + "is_client_enabled", + args!(validated_client.non_fungible_id()) + ), + "disputes pending withdrawal" + ); + }); + + self.update_work_with_assigned_contractor(contractor, validated_work); + } + + /// If both client and contractor agrees that the work has been completed. + /// This requires multi-signature from the client and contractor + /// This method successfully finishes the work which does the following: + /// + /// 1. Updates the `Client` NFT `Client.jobs_paid_out` and `Client.total_paid_out` from + /// information of `Work` NFT + /// 2. We compensate the contractor externally from the service component + /// 3. Mint a `CompletedWork` NFT which is deposited to a WorkCompleted vault + /// + /// # Panics: + /// - if work `ResourceAddress` is invalid + /// - if client `ResourceAddress` is invalid + /// - if contractor `ResourceAddress` is invalid + pub fn finish_work(&mut self, work: Proof, client: Proof, contractor: Proof) { + let validated_work = self.validate_work(work).expect("invalid work"); + + let validated_client = self.validate_client(client).expect("unauthorized access"); + + let validated_contractor = self + .validate_contractor(contractor) + .expect("unauthorized access"); + + self.service_auth.authorize(|| { + assert!( + borrow_component!(self.service_component).call::( + "is_client_enabled", + args!(validated_client.non_fungible_id()) + ), + "disputes pending withdrawal" + ); + }); + + self.service_auth.authorize(|| { + assert!( + borrow_component!(self.service_component).call::( + "is_contractor_enabled", + args!(validated_contractor.non_fungible_id()) + ), + "disputes pending withdrawal" + ); + }); + + let client_resource_manager = borrow_resource_manager!(self.client); + let work_resource_manager = borrow_resource_manager!(self.work_resource); + let service_component = borrow_component!(self.service_component); + + let client_nft = client_resource_manager + .get_non_fungible_data::(&validated_client.non_fungible_id()); + let work_nft = work_resource_manager + .get_non_fungible_data::(&validated_work.non_fungible_id()); + + self.service_auth.authorize(|| { + self.compensate_contractor( + service_component, + &validated_client, + &validated_contractor, + &work_nft, + ); + + self.finalise_work( + service_component, + &validated_work, + &work_nft, + validated_contractor, + ); + + self.update_client_total_paid_out( + client_resource_manager, + validated_client, + client_nft, + &work_nft, + ); + + self.update_work_to_finished(work_resource_manager, validated_work, work_nft); + }); + } + + /// Mints a new work from arguments given + fn mint_new_work( + &self, + validated_client: &ValidatedProof, + total_compensation: Decimal, + work_description: String, + work_title: String, + skills_required: Vec, + ) -> Bucket { + let work_resource_manager = borrow_resource_manager!(self.work_resource); + let id = NonFungibleId::from_u64( + (work_resource_manager.total_supply() + 1) + .to_string() + .parse() + .unwrap(), + ); + let work = self.service_auth.authorize(|| { + work_resource_manager.mint_non_fungible( + &id, + Work { + client: validated_client.non_fungible_id(), + total_compensation, + work_description, + work_title, + work_status: WorkStatus::NotStarted, + skills_required, + contractor_requests: HashMap::new(), + contractor_assigned: None, + }, + ) + }); + work + } + + /// Update a work NFT to be marked as delisted + fn update_work_delist(&self, validated_work: ValidatedProof, work_nft: Work) { + self.service_auth.authorize(|| { + borrow_resource_manager!(self.work_resource).update_non_fungible_data( + &validated_work.non_fungible_id(), + Work { + client: work_nft.client.clone(), + total_compensation: work_nft.total_compensation, + work_title: work_nft.work_title, + work_description: work_nft.work_description, + work_status: WorkStatus::Delisted, + skills_required: work_nft.skills_required, + contractor_requests: work_nft.contractor_requests, + contractor_assigned: work_nft.contractor_assigned, + }, + ); + + borrow_component!(self.service_component).call::<()>( + "refund_client", + args!(work_nft.client, work_nft.total_compensation), + ); + }); + } + + /// Update client to increment jobs created + fn update_client_jobs_created(&self, validated_client: &ValidatedProof) { + let client_resource_manager = borrow_resource_manager!(self.client); + let client = client_resource_manager + .get_non_fungible_data::(&validated_client.non_fungible_id()); + self.service_auth.authorize(|| { + client_resource_manager.update_non_fungible_data( + &validated_client.non_fungible_id(), + Client { + jobs_created: client.jobs_created + 1, + total_paid_out: client.total_paid_out, + disputed: client.disputed, + jobs_paid_out: client.jobs_paid_out, + }, + ); + }); + } + + /// Validate the client for the correct `ResourceAddress` + fn validate_client( + &self, + client: Proof, + ) -> Result { + client.validate_proof(ProofValidationMode::ValidateContainsAmount( + self.client, + dec!(1), + )) + } + + /// Validate work ResourceAddress + fn validate_work( + &self, + work: Proof, + ) -> Result { + work.validate_proof(ProofValidationMode::ValidateContainsAmount( + self.work_resource, + dec!(1), + )) + } + + /// Validate that the use is a contractor + fn validate_contractor( + &self, + contractor: Proof, + ) -> Result { + contractor.validate_proof(ProofValidationMode::ValidateContainsAmount( + self.contractor, + dec!(1), + )) + } + + // Assign a contractor to the work NFT + fn update_work_with_assigned_contractor( + &self, + contractor: NonFungibleId, + validated_work: ValidatedProof, + ) { + let contractor_resource_manager = borrow_resource_manager!(self.contractor); + contractor_resource_manager.get_non_fungible_data::(&contractor); + let work_resource_manager = borrow_resource_manager!(self.work_resource); + let work_nft = work_resource_manager + .get_non_fungible_data::(&validated_work.non_fungible_id()); + self.service_auth.authorize(|| { + work_resource_manager.update_non_fungible_data( + &validated_work.non_fungible_id(), + Work { + client: work_nft.client, + total_compensation: work_nft.total_compensation, + work_title: work_nft.work_title, + work_description: work_nft.work_description, + work_status: WorkStatus::InProgress, + skills_required: work_nft.skills_required, + contractor_requests: work_nft.contractor_requests, + contractor_assigned: Some(contractor), + }, + ) + }); + } + + /// Update work NFT with the contractor non fungible ID + fn update_work_with_contractor_request( + &self, + work_resource_manager: &mut ResourceManager, + validated_contractor: ValidatedProof, + mut work: Work, + work_id: NonFungibleId, + ) { + work.contractor_requests + .insert(validated_contractor.non_fungible_id(), true); + self.service_auth.authorize(|| { + work_resource_manager.update_non_fungible_data( + &work_id, + Work { + client: work.client, + total_compensation: work.total_compensation, + work_description: work.work_description, + work_title: work.work_title, + work_status: work.work_status, + skills_required: work.skills_required, + contractor_requests: work.contractor_requests, + contractor_assigned: work.contractor_assigned, + }, + ) + }); + } + + /// Calls finalise work from the service component + fn finalise_work( + &self, + service_component: &Component, + validated_work: &ValidatedProof, + work_nft: &Work, + validated_contractor: ValidatedProof, + ) { + service_component.call::<()>( + "finalise_work", + args!( + self.work_resource, + validated_work.non_fungible_id(), + work_nft.total_compensation, + validated_contractor.non_fungible_id() + ), + ); + } + + // Calls to compensate the contractor from the service component + fn compensate_contractor( + &self, + service_component: &Component, + validated_client: &ValidatedProof, + validated_contractor: &ValidatedProof, + work_nft: &Work, + ) { + service_component.call::<()>( + "compensate_contractor", + args!( + validated_client.non_fungible_id(), + validated_contractor.non_fungible_id(), + work_nft.total_compensation + ), + ); + } + + fn update_work_to_finished( + &self, + work_resource_manager: &mut ResourceManager, + validated_work: ValidatedProof, + work_nft: Work, + ) { + work_resource_manager.update_non_fungible_data( + &validated_work.non_fungible_id(), + Work { + client: work_nft.client, + total_compensation: work_nft.total_compensation, + work_title: work_nft.work_title, + work_description: work_nft.work_description, + work_status: WorkStatus::Finished, + skills_required: work_nft.skills_required, + contractor_requests: work_nft.contractor_requests, + contractor_assigned: work_nft.contractor_assigned, + }, + ); + } + + fn update_client_total_paid_out( + &self, + client_resource_manager: &mut ResourceManager, + validated_client: ValidatedProof, + client_nft: Client, + work_nft: &Work, + ) { + client_resource_manager.update_non_fungible_data( + &validated_client.non_fungible_id(), + Client { + jobs_created: client_nft.jobs_created, + total_paid_out: client_nft.total_paid_out + work_nft.total_compensation, + disputed: client_nft.disputed, + jobs_paid_out: client_nft.jobs_paid_out + 1, + }, + ); + } + } +} diff --git a/6-nfts-for-financial-applications/moonwork/tests/lib.rs b/6-nfts-for-financial-applications/moonwork/tests/lib.rs new file mode 100644 index 000000000..f8580dbe1 --- /dev/null +++ b/6-nfts-for-financial-applications/moonwork/tests/lib.rs @@ -0,0 +1,4847 @@ +use manifests::*; +use moonwork::dispute::Dispute; +use moonwork::dispute::DisputeDocument; +use moonwork::dispute::DisputeSide; +use moonwork::moonwork::ContractorAccolades; +use moonwork::moonwork::DisputeDecision; +use moonwork::moonwork::DisputeOutcome; +use moonwork::promotion::PromotedContractor; +use moonwork::users::Client; +use moonwork::users::Contractor; +use moonwork::work::Work; +use moonwork::work::WorkStatus; +use radix_engine::ledger::*; +use radix_engine::types::*; +use scrypto_unit::*; + +mod manifests; + +#[test] +fn test_create_moonwork_service() { + // Setup the environment + let mut store = TypedInMemorySubstateStore::with_bootstrap(); + let mut test_runner = TestRunner::new(false, &mut store); + + let (public_key, _private_key, account_component) = test_runner.new_account(); + + // Creating moonwork service + create_moonwork_service(&mut test_runner, account_component, public_key); +} + +#[test] +fn moonwork_service_register_as_contractor() { + // Setup the environment + let mut store = TypedInMemorySubstateStore::with_bootstrap(); + let mut test_runner = TestRunner::new(false, &mut store); + + let (public_key, _private_key, account_component) = test_runner.new_account(); + + // arrange + let (component, resources) = + create_moonwork_service(&mut test_runner, account_component, public_key); + + // act + let receipt = register_as_contractor( + &mut test_runner, + component, + account_component, + public_key, + "beemdvp", + ); + receipt.expect_commit_success(); + + // assert + let contractor_badge_balance = get_account_balance( + &mut test_runner, + account_component, + public_key, + resources.contractor_badge, + ); + + let accolade_balance = get_account_balance( + &mut test_runner, + account_component, + public_key, + resources.contractor_accolade_resource, + ); + + assert_eq!(contractor_badge_balance, dec!(1)); + assert_eq!(accolade_balance, dec!(1)); +} + +#[test] +fn moonwork_service_register_as_contractor_fails_duplicate() { + // Setup the environment + let mut store = TypedInMemorySubstateStore::with_bootstrap(); + let mut test_runner = TestRunner::new(false, &mut store); + + let (public_key, _private_key, account_component) = test_runner.new_account(); + + // arrange + let (component, _resources) = + create_moonwork_service(&mut test_runner, account_component, public_key); + + register_as_contractor( + &mut test_runner, + component, + account_component, + public_key, + "beemdvp", + ); + + // act + let receipt = register_as_contractor( + &mut test_runner, + component, + account_component, + public_key, + "beemdvp", + ); + + // assert + receipt.expect_commit_failure(); +} + +#[test] +fn moonwork_service_register_as_client() { + // Setup the environment + let mut store = TypedInMemorySubstateStore::with_bootstrap(); + let mut test_runner = TestRunner::new(false, &mut store); + + // arrange + let (public_key, _private_key, account_component) = test_runner.new_account(); + + let (component, resources) = + create_moonwork_service(&mut test_runner, account_component, public_key); + + // act + let receipt = register_as_client( + &mut test_runner, + component, + account_component, + public_key, + "slysmik", + ); + receipt.expect_commit_success(); + + // assert + let balance = get_account_balance( + &mut test_runner, + account_component, + public_key, + resources.client_badge, + ); + + assert_eq!(balance, dec!(1)); +} + +#[test] +fn moonwork_service_register_as_client_fails_duplicate() { + // Setup the environment + let mut store = TypedInMemorySubstateStore::with_bootstrap(); + let mut test_runner = TestRunner::new(false, &mut store); + + // arrange + let (public_key, _private_key, account_component) = test_runner.new_account(); + + let (component, _resources) = + create_moonwork_service(&mut test_runner, account_component, public_key); + + register_as_client( + &mut test_runner, + component, + account_component, + public_key, + "slysmik", + ); + + // act + let receipt = register_as_client( + &mut test_runner, + component, + account_component, + public_key, + "slysmik", + ); + + // assert + receipt.expect_commit_failure(); +} + +#[test] +fn moonwork_service_create_new_category_client_badge_fails() { + // Setup the environment + let mut store = TypedInMemorySubstateStore::with_bootstrap(); + let mut test_runner = TestRunner::new(false, &mut store); + + let (public_key, _private_key, account_component) = test_runner.new_account(); + + // arrange + let (component, resources) = + create_moonwork_service(&mut test_runner, account_component, public_key); + + let (new_client_public_key, _private_key, new_client_account) = test_runner.new_account(); + + register_as_client( + &mut test_runner, + component, + new_client_account, + new_client_public_key, + "foobar", + ); + + // act + let receipt = create_new_category( + &mut test_runner, + resources.client_badge, + new_client_account, + new_client_public_key, + component, + ); + + // assert + receipt.expect_commit_failure(); +} + +#[test] +fn moonwork_service_create_new_category_contractor_badge_fails() { + // Setup the environment + let mut store = TypedInMemorySubstateStore::with_bootstrap(); + let mut test_runner = TestRunner::new(false, &mut store); + + let (public_key, _private_key, account_component) = test_runner.new_account(); + + // arrange + let (component, resources) = + create_moonwork_service(&mut test_runner, account_component, public_key); + let (new_contractor_public_key, _private_key, new_contractor_account) = + test_runner.new_account(); + + register_as_contractor( + &mut test_runner, + component, + new_contractor_account, + new_contractor_public_key, + "foobar", + ); + + // act + let receipt = create_new_category( + &mut test_runner, + resources.contractor_badge, + new_contractor_account, + new_contractor_public_key, + component, + ); + + // assert + receipt.expect_commit_failure(); +} + +#[test] +fn moonwork_service_create_new_work_as_client() { + // Setup the environment + let mut store = TypedInMemorySubstateStore::with_bootstrap(); + let mut test_runner = TestRunner::new(false, &mut store); + + let (public_key, _private_key, account_component) = test_runner.new_account(); + + // arrange + let service = create_moon_work_service_with_work_category(&mut test_runner); + + register_as_client( + &mut test_runner, + service.moon_work_component, + account_component, + public_key, + "slysmik", + ); + // act + let receipt = create_new_work( + &mut test_runner, + account_component, + public_key, + service.moon_work_resources.client_badge, + &service.work_components, + ); + receipt.expect_commit_success(); + + // assert + let work_resource_balance = get_account_balance( + &mut test_runner, + account_component, + public_key, + service.work_components.work_resource, + ); + + assert_eq!(work_resource_balance, dec!(1)); +} + +#[test] +fn moonwork_service_remove_new_work_as_client() { + // Setup the environment + let mut store = TypedInMemorySubstateStore::with_bootstrap(); + let mut test_runner = TestRunner::new(false, &mut store); + + let (public_key, _private_key, account_component) = test_runner.new_account(); + + // arrange + let service = create_moon_work_service_with_work_category(&mut test_runner); + + register_as_client( + &mut test_runner, + service.moon_work_component, + account_component, + public_key, + "slysmik", + ); + // act + create_new_work( + &mut test_runner, + account_component, + public_key, + service.moon_work_resources.client_badge, + &service.work_components, + ); + + let work_id = NonFungibleId::from_u64(1); + let receipt = remove_work( + &mut test_runner, + &service, + account_component, + public_key, + work_id, + ); + + receipt.expect_commit_success(); + + // assert + let work = get_non_fungible_data::( + &store, + service.work_components.work_resource, + NonFungibleId::from_u64(1), + ); + + assert!(work.work_status == WorkStatus::Delisted); +} + +#[test] +fn moonwork_service_request_work_as_contractor() { + // Setup the environment + let mut store = TypedInMemorySubstateStore::with_bootstrap(); + let mut test_runner = TestRunner::new(false, &mut store); + + let service = create_moon_work_service_with_work_category(&mut test_runner); + + let (client_public_key, _private_key, client_account_component) = test_runner.new_account(); + + register_as_client( + &mut test_runner, + service.moon_work_component, + client_account_component, + client_public_key, + "slysmik", + ); + + let (contractor_public_key, _private_key, contractor_account_component) = + test_runner.new_account(); + + register_as_contractor( + &mut test_runner, + service.moon_work_component, + contractor_account_component, + contractor_public_key, + "beemdvp", + ); + + create_new_work( + &mut test_runner, + client_account_component, + client_public_key, + service.moon_work_resources.client_badge, + &service.work_components, + ); + + // act + let receipt = request_work( + &mut test_runner, + contractor_account_component, + contractor_public_key, + service.moon_work_resources.contractor_badge, + service.work_components.work_component, + NonFungibleId::from_u64(1), + ); + + let work = get_non_fungible_data::( + &store, + service.work_components.work_resource, + NonFungibleId::from_u64(1), + ); + + assert_eq!( + work.contractor_requests + .get(&NonFungibleId::from_bytes("beemdvp".as_bytes().to_vec())), + Some(&true) + ); + // assert + receipt.expect_commit_success(); +} + +#[test] +fn moonwork_service_accept_contractor_for_work_as_client() { + // Setup the environment + let mut store = TypedInMemorySubstateStore::with_bootstrap(); + let mut test_runner = TestRunner::new(false, &mut store); + + let (client_public_key, _private_key, client_account) = test_runner.new_account(); + + let service = create_moon_work_service_with_work_category(&mut test_runner); + + register_as_client( + &mut test_runner, + service.moon_work_component, + client_account, + client_public_key, + "slysmik", + ); + + let (contractor_public_key, _private_key, contractor_account) = test_runner.new_account(); + + register_as_contractor( + &mut test_runner, + service.moon_work_component, + contractor_account, + contractor_public_key, + "beemdvp", + ); + + create_new_work( + &mut test_runner, + client_account, + client_public_key, + service.moon_work_resources.client_badge, + &service.work_components, + ); + + request_work( + &mut test_runner, + contractor_account, + contractor_public_key, + service.moon_work_resources.contractor_badge, + service.work_components.work_component, + NonFungibleId::from_u64(1), + ); + + let receipt = accept_contractor_for_work( + &mut test_runner, + service.work_components.work_component, + service.moon_work_resources.client_badge, + client_account, + client_public_key, + service.work_components.work_resource, + NonFungibleId::from_bytes("beemdvp".as_bytes().to_vec()), + NonFungibleId::from_u64(1), + ); + + let work = get_non_fungible_data::( + &store, + service.work_components.work_resource, + NonFungibleId::from_u64(1), + ); + + assert_eq!( + work.contractor_assigned, + Some(NonFungibleId::from_bytes("beemdvp".as_bytes().to_vec())) + ); + receipt.expect_commit_success(); +} + +#[test] +fn moonwork_service_finish_work_as_client_and_contractor() { + // Setup the environment + let mut store = TypedInMemorySubstateStore::with_bootstrap(); + let mut test_runner = TestRunner::new(false, &mut store); + + let (client_public_key, _private_key, client_account) = test_runner.new_account(); + + let service = create_moon_work_service_with_work_category(&mut test_runner); + + register_as_client( + &mut test_runner, + service.moon_work_component, + client_account, + client_public_key, + "slysmik", + ); + + let (contractor_public_key, _private_key, contractor_account) = test_runner.new_account(); + + register_as_contractor( + &mut test_runner, + service.moon_work_component, + contractor_account, + contractor_public_key, + "beemdvp", + ); + + create_new_work( + &mut test_runner, + client_account, + client_public_key, + service.moon_work_resources.client_badge, + &service.work_components, + ); + + request_work( + &mut test_runner, + contractor_account, + contractor_public_key, + service.moon_work_resources.contractor_badge, + service.work_components.work_component, + NonFungibleId::from_u64(1), + ); + + accept_contractor_for_work( + &mut test_runner, + service.work_components.work_component, + service.moon_work_resources.client_badge, + client_account, + client_public_key, + service.work_components.work_resource, + NonFungibleId::from_bytes("beemdvp".as_bytes().to_vec()), + NonFungibleId::from_u64(1), + ); + + // act + let work_id = NonFungibleId::from_u64(1); + + let receipt = finish_work( + &mut test_runner, + &service.moon_work_resources, + client_account, + client_public_key, + contractor_account, + contractor_public_key, + work_id, + &service.work_components, + ); + + receipt.expect_commit_success(); + + let work = get_non_fungible_data::( + &store, + service.work_components.work_resource, + NonFungibleId::from_u64(1), + ); + + // assert + assert_eq!(work.work_status, WorkStatus::Finished); +} + +#[test] +fn moonwork_service_claim_compensation_as_contractor() { + // Setup the environment + let mut store = TypedInMemorySubstateStore::with_bootstrap(); + let mut test_runner = TestRunner::new(false, &mut store); + + let service = create_moon_work_service_with_work_category(&mut test_runner); + + let (client_public_key, _private_key, client_account) = test_runner.new_account(); + + register_as_client( + &mut test_runner, + service.moon_work_component, + client_account, + client_public_key, + "slysmik", + ); + + let (contractor_public_key, _private_key, contractor_account) = test_runner.new_account(); + + register_as_contractor( + &mut test_runner, + service.moon_work_component, + contractor_account, + contractor_public_key, + "beemdvp", + ); + + create_work_to_finish_work( + &mut test_runner, + client_account, + contractor_account, + "beemdvp", + &service.moon_work_resources, + &service.work_components, + client_public_key, + contractor_public_key, + 1, + ); + + let receipt = claim_contractor_compensation( + &mut test_runner, + &service.moon_work_resources, + service.moon_work_component, + contractor_account, + contractor_public_key, + ); + + receipt.expect_commit_success(); + + let completed_work_balance = get_account_balance( + &mut test_runner, + contractor_account, + contractor_public_key, + service.moon_work_resources.completed_work_resource, + ); + + let contractor = get_non_fungible_data::( + &store, + service.moon_work_resources.contractor_badge, + NonFungibleId::from_bytes("beemdvp".as_bytes().to_vec()), + ); + let client = get_non_fungible_data::( + &store, + service.moon_work_resources.client_badge, + NonFungibleId::from_bytes("slysmik".as_bytes().to_vec()), + ); + + assert_eq!(client.jobs_paid_out, 1); + assert_eq!(client.total_paid_out, dec!(1)); + + assert_eq!(contractor.jobs_completed, 1); + assert_eq!(contractor.total_worth, dec!(1)); + assert_eq!(completed_work_balance, dec!(1)); +} + +#[test] +fn moonwork_service_claim_compensation_as_client_fails() { + // Setup the environment + let mut store = TypedInMemorySubstateStore::with_bootstrap(); + let mut test_runner = TestRunner::new(false, &mut store); + + let service = create_moon_work_service_with_work_category(&mut test_runner); + + let (client_public_key, _private_key, client_account) = test_runner.new_account(); + + register_as_client( + &mut test_runner, + service.moon_work_component, + client_account, + client_public_key, + "slysmik", + ); + + let (contractor_public_key, _private_key, contractor_account) = test_runner.new_account(); + + register_as_contractor( + &mut test_runner, + service.moon_work_component, + contractor_account, + contractor_public_key, + "beemdvp", + ); + + create_work_to_finish_work( + &mut test_runner, + client_account, + contractor_account, + "beemdvp", + &service.moon_work_resources, + &service.work_components, + client_public_key, + contractor_public_key, + 1, + ); + + let receipt = claim_contractor_compensation( + &mut test_runner, + &service.moon_work_resources, + service.moon_work_component, + client_account, + client_public_key, + ); + + receipt.expect_commit_failure(); +} + +#[test] +fn moonwork_service_claim_compensation_with_waxing_crescent_accolade() { + // Setup the environment + let mut store = TypedInMemorySubstateStore::with_bootstrap(); + let mut test_runner = TestRunner::new(false, &mut store); + + let (client_public_key, _private_key, client_account) = test_runner.new_account(); + + let service = create_moon_work_service_with_work_category(&mut test_runner); + + register_as_client( + &mut test_runner, + service.moon_work_component, + client_account, + client_public_key, + "slysmik", + ); + + let (contractor_public_key, _private_key, contractor_account) = test_runner.new_account(); + + register_as_contractor( + &mut test_runner, + service.moon_work_component, + contractor_account, + contractor_public_key, + "beemdvp", + ); + + // in order to test the accolade system, we're doing quite a few things to arrange: + // 1. As a client, create new work for contractor + // 2. As a contractor, request to take on created work + // 3. As a client, accept to take on contractor for work + // 4. As both client and contractor (multisig) - finish work + for id in 1..11 { + create_work_to_finish_work( + &mut test_runner, + client_account, + contractor_account, + "beemdvp", + &service.moon_work_resources, + &service.work_components, + client_public_key, + contractor_public_key, + id, + ); + } + + // act + // Finally claim contractor compensation which also mints them their very own accolade NFT + let receipt = claim_contractor_compensation( + &mut test_runner, + &service.moon_work_resources, + service.moon_work_component, + contractor_account, + contractor_public_key, + ); + + receipt.expect_commit_success(); + + let work_completed_balance = get_account_balance( + &mut test_runner, + contractor_account, + contractor_public_key, + service.moon_work_resources.completed_work_resource, + ); + + let accolade_balance = get_account_balance( + &mut test_runner, + contractor_account, + contractor_public_key, + service.moon_work_resources.contractor_accolade_resource, + ); + + let waxing_crescent_nft = get_non_fungible_data::( + &store, + service.moon_work_resources.contractor_accolade_resource, + NonFungibleId::from_u64(2), + ); + + let work_ids: Vec = (1..11) + .map(|id| { + NonFungibleAddress::new( + service.moon_work_resources.completed_work_resource, + NonFungibleId::from_u64(id), + ) + }) + .collect(); + + // all work ids used to achieve the accolade + assert_eq!(waxing_crescent_nft.work_ids, work_ids); + // 10 contracts completed! + assert_eq!(work_completed_balance, dec!(10)); + // NewMoon and WaxingCrescent accolades achieved + assert_eq!(accolade_balance, dec!(2)); +} + +#[test] +fn moonwork_service_claim_compensation_with_first_quarter_accolade() { + // Setup the environment + let mut store = TypedInMemorySubstateStore::with_bootstrap(); + let mut test_runner = TestRunner::new(false, &mut store); + + let (client_public_key, _private_key, client_account) = test_runner.new_account(); + + let service = create_moon_work_service_with_work_category(&mut test_runner); + + register_as_client( + &mut test_runner, + service.moon_work_component, + client_account, + client_public_key, + "slysmik", + ); + + let (contractor_public_key, _private_key, contractor_account) = test_runner.new_account(); + + register_as_contractor( + &mut test_runner, + service.moon_work_component, + contractor_account, + contractor_public_key, + "beemdvp", + ); + + for id in 1..21 { + create_work_to_finish_work( + &mut test_runner, + client_account, + contractor_account, + "beemdvp", + &service.moon_work_resources, + &service.work_components, + client_public_key, + contractor_public_key, + id, + ); + } + + let receipt = claim_contractor_compensation( + &mut test_runner, + &service.moon_work_resources, + service.moon_work_component, + contractor_account, + contractor_public_key, + ); + + receipt.expect_commit_success(); + + let work_completed_balance = get_account_balance( + &mut test_runner, + contractor_account, + contractor_public_key, + service.moon_work_resources.completed_work_resource, + ); + + let accolade_balance = get_account_balance( + &mut test_runner, + contractor_account, + contractor_public_key, + service.moon_work_resources.contractor_accolade_resource, + ); + + let first_quarter_nft = get_non_fungible_data::( + &store, + service.moon_work_resources.contractor_accolade_resource, + NonFungibleId::from_u64(3), + ); + + let work_ids: Vec = (11..21) + .map(|id| { + NonFungibleAddress::new( + service.moon_work_resources.completed_work_resource, + NonFungibleId::from_u64(id), + ) + }) + .collect(); + + // all work ids used to achieve the accolade + assert_eq!(first_quarter_nft.work_ids, work_ids); + // 20 contracts completed! + assert_eq!(work_completed_balance, dec!(20)); + // NewMoon, WaxingCrescent and FirstQuarter accolades achieved! + assert_eq!(accolade_balance, dec!(3)); +} + +#[test] +fn moonwork_service_claim_compensation_with_waxing_gibbous_accolade() { + // Setup the environment + let mut store = TypedInMemorySubstateStore::with_bootstrap(); + let mut test_runner = TestRunner::new(false, &mut store); + + let (client_public_key, _private_key, client_account) = test_runner.new_account(); + + let service = create_moon_work_service_with_work_category(&mut test_runner); + + register_as_client( + &mut test_runner, + service.moon_work_component, + client_account, + client_public_key, + "slysmik", + ); + + let (contractor_public_key, _private_key, contractor_account) = test_runner.new_account(); + + register_as_contractor( + &mut test_runner, + service.moon_work_component, + contractor_account, + contractor_public_key, + "beemdvp", + ); + + for id in 1..31 { + create_work_to_finish_work( + &mut test_runner, + client_account, + contractor_account, + "beemdvp", + &service.moon_work_resources, + &service.work_components, + client_public_key, + contractor_public_key, + id, + ); + } + + let receipt = claim_contractor_compensation( + &mut test_runner, + &service.moon_work_resources, + service.moon_work_component, + contractor_account, + contractor_public_key, + ); + + receipt.expect_commit_success(); + + let work_completed_balance = get_account_balance( + &mut test_runner, + contractor_account, + contractor_public_key, + service.moon_work_resources.completed_work_resource, + ); + + let accolade_balance = get_account_balance( + &mut test_runner, + contractor_account, + contractor_public_key, + service.moon_work_resources.contractor_accolade_resource, + ); + + // 30 contracts completed! + assert_eq!(work_completed_balance, dec!(30)); + // NewMoon, WaxingCrescent, FirstQuarter and WaxingGibbous accolades achieved! + assert_eq!(accolade_balance, dec!(4)); +} + +#[test] +fn moonwork_service_claim_compensation_with_full_moon_accolade() { + // Setup the environment + let mut store = TypedInMemorySubstateStore::with_bootstrap(); + let mut test_runner = TestRunner::new(false, &mut store); + + let (client_public_key, _private_key, client_account) = test_runner.new_account(); + + let service = create_moon_work_service_with_work_category(&mut test_runner); + + register_as_client( + &mut test_runner, + service.moon_work_component, + client_account, + client_public_key, + "slysmik", + ); + + let (contractor_public_key, _private_key, contractor_account) = test_runner.new_account(); + + register_as_contractor( + &mut test_runner, + service.moon_work_component, + contractor_account, + contractor_public_key, + "beemdvp", + ); + + for id in 1..41 { + create_work_to_finish_work( + &mut test_runner, + client_account, + contractor_account, + "beemdvp", + &service.moon_work_resources, + &service.work_components, + client_public_key, + contractor_public_key, + id, + ); + } + + let receipt = claim_contractor_compensation( + &mut test_runner, + &service.moon_work_resources, + service.moon_work_component, + contractor_account, + contractor_public_key, + ); + + receipt.expect_commit_success(); + + let work_completed_balance = get_account_balance( + &mut test_runner, + contractor_account, + contractor_public_key, + service.moon_work_resources.completed_work_resource, + ); + + let accolade_balance = get_account_balance( + &mut test_runner, + contractor_account, + contractor_public_key, + service.moon_work_resources.contractor_accolade_resource, + ); + + // 40 contracts completed! + assert_eq!(work_completed_balance, dec!(40)); + // NewMoon, WaxingCrescent, FirstQuarter, WaxingGibbous and FullMoon accolades achieved! + assert_eq!(accolade_balance, dec!(5)); +} + +#[test] +fn moonwork_service_create_dispute_as_contractor() { + // Setup the environment + let mut store = TypedInMemorySubstateStore::with_bootstrap(); + let mut test_runner = TestRunner::new(false, &mut store); + + let (client_public_key, _private_key, client_account) = test_runner.new_account(); + + let service = create_moon_work_service_with_work_category(&mut test_runner); + + register_as_client( + &mut test_runner, + service.moon_work_component, + client_account, + client_public_key, + "slysmik", + ); + + let (contractor_public_key, _private_key, contractor_account) = test_runner.new_account(); + + register_as_contractor( + &mut test_runner, + service.moon_work_component, + contractor_account, + contractor_public_key, + "beemdvp", + ); + + create_new_work( + &mut test_runner, + client_account, + client_public_key, + service.moon_work_resources.client_badge, + &service.work_components, + ); + + let work_id = NonFungibleId::from_u64(1); + + request_work( + &mut test_runner, + contractor_account, + contractor_public_key, + service.moon_work_resources.contractor_badge, + service.work_components.work_component, + work_id.clone(), + ); + + accept_contractor_for_work( + &mut test_runner, + service.work_components.work_component, + service.moon_work_resources.client_badge, + client_account, + client_public_key, + service.work_components.work_resource, + NonFungibleId::from_bytes("beemdvp".as_bytes().to_vec()), + NonFungibleId::from_u64(1), + ); + + // act + let receipt = create_new_dispute( + &mut test_runner, + service.moon_work_resources.contractor_badge, + contractor_account, + contractor_public_key, + &service.work_components, + work_id, + ); + + receipt.expect_commit_success(); + + let dispute_nft = get_non_fungible_data::( + &store, + service.work_components.dispute_resource, + NonFungibleId::from_u64(1), + ); + + // assert + assert_eq!( + dispute_nft.work, + NonFungibleAddress::new( + service.work_components.work_resource, + NonFungibleId::from_u64(1) + ) + ); + assert_eq!(dispute_nft.raised_by, DisputeSide::Contractor); + assert_eq!( + dispute_nft.contractor, + NonFungibleId::from_bytes("beemdvp".as_bytes().to_vec()) + ); + assert_eq!( + dispute_nft.client, + NonFungibleId::from_bytes("slysmik".as_bytes().to_vec()) + ); +} + +#[test] +fn moonwork_service_create_dispute_fails_as_outside_contractor() { + // Setup the environment + let mut store = TypedInMemorySubstateStore::with_bootstrap(); + let mut test_runner = TestRunner::new(false, &mut store); + + let (client_public_key, _private_key, client_account) = test_runner.new_account(); + + let service = create_moon_work_service_with_work_category(&mut test_runner); + + register_as_client( + &mut test_runner, + service.moon_work_component, + client_account, + client_public_key, + "slysmik", + ); + + let (contractor_public_key, _private_key, contractor_account) = test_runner.new_account(); + + register_as_contractor( + &mut test_runner, + service.moon_work_component, + contractor_account, + contractor_public_key, + "beemdvp", + ); + + create_new_work( + &mut test_runner, + client_account, + client_public_key, + service.moon_work_resources.client_badge, + &service.work_components, + ); + + let work_id = NonFungibleId::from_u64(1); + + request_work( + &mut test_runner, + contractor_account, + contractor_public_key, + service.moon_work_resources.contractor_badge, + service.work_components.work_component, + work_id.clone(), + ); + + accept_contractor_for_work( + &mut test_runner, + service.work_components.work_component, + service.moon_work_resources.client_badge, + client_account, + client_public_key, + service.work_components.work_resource, + NonFungibleId::from_bytes("beemdvp".as_bytes().to_vec()), + NonFungibleId::from_u64(1), + ); + + let (outside_contractor_public_key, _private_key, outside_contractor_account) = + test_runner.new_account(); + + register_as_contractor( + &mut test_runner, + service.moon_work_component, + outside_contractor_account, + outside_contractor_public_key, + "outside contractor", + ); + + // act + let receipt = create_new_dispute( + &mut test_runner, + service.moon_work_resources.contractor_badge, + outside_contractor_account, + outside_contractor_public_key, + &service.work_components, + work_id, + ); + + receipt.expect_commit_failure(); + assert_error_message(&receipt, "unauthorized user"); +} + +#[test] +fn moonwork_service_create_dispute_as_client() { + // Setup the environment + let mut store = TypedInMemorySubstateStore::with_bootstrap(); + let mut test_runner = TestRunner::new(false, &mut store); + + let (client_public_key, _private_key, client_account) = test_runner.new_account(); + + let service = create_moon_work_service_with_work_category(&mut test_runner); + + register_as_client( + &mut test_runner, + service.moon_work_component, + client_account, + client_public_key, + "slysmik", + ); + + let (contractor_public_key, _private_key, contractor_account) = test_runner.new_account(); + + register_as_contractor( + &mut test_runner, + service.moon_work_component, + contractor_account, + contractor_public_key, + "beemdvp", + ); + + create_new_work( + &mut test_runner, + client_account, + client_public_key, + service.moon_work_resources.client_badge, + &service.work_components, + ); + + let (contractor_public_key, _private_key, contractor_account) = test_runner.new_account(); + + let work_id = NonFungibleId::from_u64(1); + + request_work( + &mut test_runner, + contractor_account, + contractor_public_key, + service.moon_work_resources.contractor_badge, + service.work_components.work_component, + work_id.clone(), + ); + + accept_contractor_for_work( + &mut test_runner, + service.work_components.work_component, + service.moon_work_resources.client_badge, + client_account, + client_public_key, + service.work_components.work_resource, + NonFungibleId::from_bytes("beemdvp".as_bytes().to_vec()), + NonFungibleId::from_u64(1), + ); + + // act + let receipt = create_new_dispute( + &mut test_runner, + service.moon_work_resources.client_badge, + client_account, + client_public_key, + &service.work_components, + work_id, + ); + + let dispute_nft = get_non_fungible_data::( + &store, + service.work_components.dispute_resource, + NonFungibleId::from_u64(1), + ); + + // assert + assert_eq!( + dispute_nft.work, + NonFungibleAddress::new( + service.work_components.work_resource, + NonFungibleId::from_u64(1) + ) + ); + assert_eq!(dispute_nft.raised_by, DisputeSide::Client); + assert_eq!( + dispute_nft.contractor, + NonFungibleId::from_bytes("beemdvp".as_bytes().to_vec()) + ); + assert_eq!( + dispute_nft.client, + NonFungibleId::from_bytes("slysmik".as_bytes().to_vec()) + ); + receipt.expect_commit_success(); +} + +#[test] +fn moonwork_service_create_dispute_fails_as_outside_client() { + // Setup the environment + let mut store = TypedInMemorySubstateStore::with_bootstrap(); + let mut test_runner = TestRunner::new(false, &mut store); + + let (client_public_key, _private_key, client_account) = test_runner.new_account(); + + let service = create_moon_work_service_with_work_category(&mut test_runner); + + register_as_client( + &mut test_runner, + service.moon_work_component, + client_account, + client_public_key, + "slysmik", + ); + + let (contractor_public_key, _private_key, contractor_account) = test_runner.new_account(); + + register_as_contractor( + &mut test_runner, + service.moon_work_component, + contractor_account, + contractor_public_key, + "beemdvp", + ); + + create_new_work( + &mut test_runner, + client_account, + client_public_key, + service.moon_work_resources.client_badge, + &service.work_components, + ); + + let work_id = NonFungibleId::from_u64(1); + + request_work( + &mut test_runner, + contractor_account, + contractor_public_key, + service.moon_work_resources.contractor_badge, + service.work_components.work_component, + work_id.clone(), + ); + + accept_contractor_for_work( + &mut test_runner, + service.work_components.work_component, + service.moon_work_resources.client_badge, + client_account, + client_public_key, + service.work_components.work_resource, + NonFungibleId::from_bytes("beemdvp".as_bytes().to_vec()), + NonFungibleId::from_u64(1), + ); + + let (outside_client_public_key, _private_key, outside_client_account) = + test_runner.new_account(); + + register_as_client( + &mut test_runner, + service.moon_work_component, + outside_client_account, + outside_client_public_key, + "outside client", + ); + + // act + let receipt = create_new_dispute( + &mut test_runner, + service.moon_work_resources.client_badge, + outside_client_account, + outside_client_public_key, + &service.work_components, + work_id, + ); + + receipt.expect_commit_failure(); + assert_error_message(&receipt, "unauthorized user"); +} + +#[test] +fn moonwork_service_cancel_dispute_as_contractor() { + // Setup the environment + let mut store = TypedInMemorySubstateStore::with_bootstrap(); + let mut test_runner = TestRunner::new(false, &mut store); + + let (client_public_key, _private_key, client_account) = test_runner.new_account(); + + let service = create_moon_work_service_with_work_category(&mut test_runner); + + register_as_client( + &mut test_runner, + service.moon_work_component, + client_account, + client_public_key, + "slysmik", + ); + + let (contractor_public_key, _private_key, contractor_account) = test_runner.new_account(); + + register_as_contractor( + &mut test_runner, + service.moon_work_component, + contractor_account, + contractor_public_key, + "beemdvp", + ); + + create_new_work( + &mut test_runner, + client_account, + client_public_key, + service.moon_work_resources.client_badge, + &service.work_components, + ); + + let work_id = NonFungibleId::from_u64(1); + + request_work( + &mut test_runner, + contractor_account, + contractor_public_key, + service.moon_work_resources.contractor_badge, + service.work_components.work_component, + work_id.clone(), + ); + + accept_contractor_for_work( + &mut test_runner, + service.work_components.work_component, + service.moon_work_resources.client_badge, + client_account, + client_public_key, + service.work_components.work_resource, + NonFungibleId::from_bytes("beemdvp".as_bytes().to_vec()), + NonFungibleId::from_u64(1), + ); + + // act + create_new_dispute( + &mut test_runner, + service.moon_work_resources.contractor_badge, + contractor_account, + contractor_public_key, + &service.work_components, + work_id, + ); + + let dispute_id = NonFungibleId::from_u64(1); + + let receipt = cancel_dispute( + &mut test_runner, + &service, + service.moon_work_resources.contractor_badge, + contractor_account, + contractor_public_key, + dispute_id, + ); + + receipt.expect_commit_success(); + + assert_eq!( + does_non_fungible_exist( + &store, + service.work_components.dispute_resource, + NonFungibleId::from_u64(1) + ), + false + ); + + let work_nft = get_non_fungible_data::( + &store, + service.work_components.work_resource, + NonFungibleId::from_u64(1), + ); + + assert_eq!(work_nft.work_status, WorkStatus::InProgress); +} + +#[test] +fn moonwork_service_cancel_dispute_as_client() { + // Setup the environment + let mut store = TypedInMemorySubstateStore::with_bootstrap(); + let mut test_runner = TestRunner::new(false, &mut store); + + let (client_public_key, _private_key, client_account) = test_runner.new_account(); + + let service = create_moon_work_service_with_work_category(&mut test_runner); + + register_as_client( + &mut test_runner, + service.moon_work_component, + client_account, + client_public_key, + "slysmik", + ); + + let (contractor_public_key, _private_key, contractor_account) = test_runner.new_account(); + + register_as_contractor( + &mut test_runner, + service.moon_work_component, + contractor_account, + contractor_public_key, + "beemdvp", + ); + + create_new_work( + &mut test_runner, + client_account, + client_public_key, + service.moon_work_resources.client_badge, + &service.work_components, + ); + + let work_id = NonFungibleId::from_u64(1); + + request_work( + &mut test_runner, + contractor_account, + contractor_public_key, + service.moon_work_resources.contractor_badge, + service.work_components.work_component, + work_id.clone(), + ); + + accept_contractor_for_work( + &mut test_runner, + service.work_components.work_component, + service.moon_work_resources.client_badge, + client_account, + client_public_key, + service.work_components.work_resource, + NonFungibleId::from_bytes("beemdvp".as_bytes().to_vec()), + NonFungibleId::from_u64(1), + ); + + // act + create_new_dispute( + &mut test_runner, + service.moon_work_resources.client_badge, + client_account, + client_public_key, + &service.work_components, + work_id, + ); + + let dispute_id = NonFungibleId::from_u64(1); + + let receipt = cancel_dispute( + &mut test_runner, + &service, + service.moon_work_resources.client_badge, + client_account, + client_public_key, + dispute_id, + ); + + receipt.expect_commit_success(); + + assert_eq!( + does_non_fungible_exist( + &store, + service.work_components.dispute_resource, + NonFungibleId::from_u64(1) + ), + false + ); + + let work_nft = get_non_fungible_data::( + &store, + service.work_components.work_resource, + NonFungibleId::from_u64(1), + ); + + assert_eq!(work_nft.work_status, WorkStatus::InProgress); +} + +#[test] +fn moonwork_service_submit_document_as_contractor() { + // Setup the environment + let mut store = TypedInMemorySubstateStore::with_bootstrap(); + let mut test_runner = TestRunner::new(false, &mut store); + + let (client_public_key, _private_key, client_account) = test_runner.new_account(); + + let service = create_moon_work_service_with_work_category(&mut test_runner); + + register_as_client( + &mut test_runner, + service.moon_work_component, + client_account, + client_public_key, + "slysmik", + ); + + let (contractor_public_key, _private_key, contractor_account) = test_runner.new_account(); + + register_as_contractor( + &mut test_runner, + service.moon_work_component, + contractor_account, + contractor_public_key, + "beemdvp", + ); + + create_new_work( + &mut test_runner, + client_account, + client_public_key, + service.moon_work_resources.client_badge, + &service.work_components, + ); + + let work_id = NonFungibleId::from_u64(1); + + request_work( + &mut test_runner, + contractor_account, + contractor_public_key, + service.moon_work_resources.contractor_badge, + service.work_components.work_component, + work_id.clone(), + ); + + accept_contractor_for_work( + &mut test_runner, + service.work_components.work_component, + service.moon_work_resources.client_badge, + client_account, + client_public_key, + service.work_components.work_resource, + NonFungibleId::from_bytes("beemdvp".as_bytes().to_vec()), + NonFungibleId::from_u64(1), + ); + + // act + create_new_dispute( + &mut test_runner, + service.moon_work_resources.client_badge, + client_account, + client_public_key, + &service.work_components, + work_id, + ); + + let dispute_id = NonFungibleId::from_u64(1); + let dispute_document_title = "signed contract"; + let dispute_document_url = "https://example.com/doc.pdf"; + + let receipt = submit_document( + &mut test_runner, + service.moon_work_resources.contractor_badge, + contractor_account, + contractor_public_key, + &service.work_components, + dispute_id, + dispute_document_title, + dispute_document_url, + ); + + // assert + receipt.expect_commit_success(); + + let dispute_document_nft = get_non_fungible_data::( + &store, + service.work_components.dispute_document_resource, + NonFungibleId::from_u64(1), + ); + + let dispute_nft = get_non_fungible_data::( + &store, + service.work_components.dispute_resource, + NonFungibleId::from_u64(1), + ); + + assert_eq!(dispute_document_nft.document_title, dispute_document_title); + assert_eq!(dispute_document_nft.document_url, dispute_document_url); + assert_eq!( + dispute_nft.contractor_documents, + vec![NonFungibleId::from_u64(1)] + ); +} + +#[test] +fn moonwork_service_submit_document_fails_on_outside_contractor() { + // Setup the environment + let mut store = TypedInMemorySubstateStore::with_bootstrap(); + let mut test_runner = TestRunner::new(false, &mut store); + + let (client_public_key, _private_key, client_account) = test_runner.new_account(); + + let service = create_moon_work_service_with_work_category(&mut test_runner); + + register_as_client( + &mut test_runner, + service.moon_work_component, + client_account, + client_public_key, + "slysmik", + ); + + let (contractor_public_key, _private_key, contractor_account) = test_runner.new_account(); + + register_as_contractor( + &mut test_runner, + service.moon_work_component, + contractor_account, + contractor_public_key, + "beemdvp", + ); + + create_new_work( + &mut test_runner, + client_account, + client_public_key, + service.moon_work_resources.client_badge, + &service.work_components, + ); + + let work_id = NonFungibleId::from_u64(1); + + request_work( + &mut test_runner, + contractor_account, + contractor_public_key, + service.moon_work_resources.contractor_badge, + service.work_components.work_component, + work_id.clone(), + ); + + accept_contractor_for_work( + &mut test_runner, + service.work_components.work_component, + service.moon_work_resources.client_badge, + client_account, + client_public_key, + service.work_components.work_resource, + NonFungibleId::from_bytes("beemdvp".as_bytes().to_vec()), + NonFungibleId::from_u64(1), + ); + + create_new_dispute( + &mut test_runner, + service.moon_work_resources.client_badge, + client_account, + client_public_key, + &service.work_components, + work_id, + ); + + let dispute_id = NonFungibleId::from_u64(1); + let dispute_document_title = "signed contract"; + let dispute_document_url = "https://example.com/doc.pdf"; + + let (outside_contractor_public_key, _private_key, outside_contractor_account) = + test_runner.new_account(); + + register_as_contractor( + &mut test_runner, + service.moon_work_component, + outside_contractor_account, + outside_contractor_public_key, + "outside contractor", + ); + + // act + let receipt = submit_document( + &mut test_runner, + service.moon_work_resources.contractor_badge, + outside_contractor_account, + outside_contractor_public_key, + &service.work_components, + dispute_id, + dispute_document_title, + dispute_document_url, + ); + + receipt.expect_commit_failure(); + assert_error_message(&receipt, "unauthorized user"); +} + +#[test] +fn moonwork_service_submit_document_as_client() { + // Setup the environment + let mut store = TypedInMemorySubstateStore::with_bootstrap(); + let mut test_runner = TestRunner::new(false, &mut store); + + let (client_public_key, _private_key, client_account) = test_runner.new_account(); + + let service = create_moon_work_service_with_work_category(&mut test_runner); + + register_as_client( + &mut test_runner, + service.moon_work_component, + client_account, + client_public_key, + "slysmik", + ); + + let (contractor_public_key, _private_key, contractor_account) = test_runner.new_account(); + + register_as_contractor( + &mut test_runner, + service.moon_work_component, + contractor_account, + contractor_public_key, + "beemdvp", + ); + + create_new_work( + &mut test_runner, + client_account, + client_public_key, + service.moon_work_resources.client_badge, + &service.work_components, + ); + + let work_id = NonFungibleId::from_u64(1); + + request_work( + &mut test_runner, + contractor_account, + contractor_public_key, + service.moon_work_resources.contractor_badge, + service.work_components.work_component, + work_id.clone(), + ); + + accept_contractor_for_work( + &mut test_runner, + service.work_components.work_component, + service.moon_work_resources.client_badge, + client_account, + client_public_key, + service.work_components.work_resource, + NonFungibleId::from_bytes("beemdvp".as_bytes().to_vec()), + NonFungibleId::from_u64(1), + ); + + create_new_dispute( + &mut test_runner, + service.moon_work_resources.client_badge, + client_account, + client_public_key, + &service.work_components, + work_id, + ); + + let dispute_id = NonFungibleId::from_u64(1); + let dispute_document_title = "signed contract"; + let dispute_document_url = "https://example.com/doc.pdf"; + + // act + let receipt = submit_document( + &mut test_runner, + service.moon_work_resources.client_badge, + client_account, + client_public_key, + &service.work_components, + dispute_id, + dispute_document_title, + dispute_document_url, + ); + + receipt.expect_commit_success(); + + let dispute_document_nft = get_non_fungible_data::( + &store, + service.work_components.dispute_document_resource, + NonFungibleId::from_u64(1), + ); + + let dispute_nft = get_non_fungible_data::( + &store, + service.work_components.dispute_resource, + NonFungibleId::from_u64(1), + ); + + // assert + assert_eq!(dispute_document_nft.document_title, dispute_document_title); + assert_eq!(dispute_document_nft.document_url, dispute_document_url); + assert_eq!( + dispute_nft.client_documents, + vec![NonFungibleId::from_u64(1)] + ); +} + +#[test] +fn moonwork_service_submit_document_fails_on_outside_client() { + // Setup the environment + let mut store = TypedInMemorySubstateStore::with_bootstrap(); + let mut test_runner = TestRunner::new(false, &mut store); + + let (client_public_key, _private_key, client_account) = test_runner.new_account(); + + let service = create_moon_work_service_with_work_category(&mut test_runner); + + register_as_client( + &mut test_runner, + service.moon_work_component, + client_account, + client_public_key, + "slysmik", + ); + + let (contractor_public_key, _private_key, contractor_account) = test_runner.new_account(); + + register_as_contractor( + &mut test_runner, + service.moon_work_component, + contractor_account, + contractor_public_key, + "beemdvp", + ); + + create_new_work( + &mut test_runner, + client_account, + client_public_key, + service.moon_work_resources.client_badge, + &service.work_components, + ); + + let work_id = NonFungibleId::from_u64(1); + + request_work( + &mut test_runner, + contractor_account, + contractor_public_key, + service.moon_work_resources.contractor_badge, + service.work_components.work_component, + work_id.clone(), + ); + + accept_contractor_for_work( + &mut test_runner, + service.work_components.work_component, + service.moon_work_resources.client_badge, + client_account, + client_public_key, + service.work_components.work_resource, + NonFungibleId::from_bytes("beemdvp".as_bytes().to_vec()), + NonFungibleId::from_u64(1), + ); + + create_new_dispute( + &mut test_runner, + service.moon_work_resources.client_badge, + client_account, + client_public_key, + &service.work_components, + work_id, + ); + + let dispute_id = NonFungibleId::from_u64(1); + let dispute_document_title = "signed contract"; + let dispute_document_url = "https://example.com/doc.pdf"; + + let (outside_client_public_key, _private_key, outside_client_account) = + test_runner.new_account(); + + register_as_client( + &mut test_runner, + service.moon_work_component, + outside_client_account, + outside_client_public_key, + "outside client", + ); + + // act + let receipt = submit_document( + &mut test_runner, + service.moon_work_resources.client_badge, + outside_client_account, + outside_client_public_key, + &service.work_components, + dispute_id, + dispute_document_title, + dispute_document_url, + ); + + receipt.expect_commit_failure(); + assert_error_message(&receipt, "unauthorized user"); +} + +#[test] +fn moonwork_service_join_and_decide_dispute() { + // Setup the environment + let mut store = TypedInMemorySubstateStore::with_bootstrap(); + let mut test_runner = TestRunner::new(false, &mut store); + + let service = create_moon_work_service_with_work_category(&mut test_runner); + + let (participant_client_public_key, _private_key, participant_client_account) = + test_runner.new_account(); + + let (participant_contractor_public_key, _private_key, participant_contractor_account) = + test_runner.new_account(); + + register_as_client( + &mut test_runner, + service.moon_work_component, + participant_client_account, + participant_client_public_key, + "bar", + ); + + register_as_contractor( + &mut test_runner, + service.moon_work_component, + participant_contractor_account, + participant_contractor_public_key, + "foo", + ); + + create_work_to_finish_work( + &mut test_runner, + participant_client_account, + participant_contractor_account, + "foo", + &service.moon_work_resources, + &service.work_components, + participant_client_public_key, + participant_contractor_public_key, + 1, + ); + + claim_contractor_compensation( + &mut test_runner, + &service.moon_work_resources, + service.moon_work_component, + participant_contractor_account, + participant_contractor_public_key, + ); + + let (client_public_key, _private_key, client_account) = test_runner.new_account(); + + let (contractor_public_key, _private_key, contractor_account) = test_runner.new_account(); + + register_as_client( + &mut test_runner, + service.moon_work_component, + client_account, + client_public_key, + "slysmik", + ); + + create_new_work( + &mut test_runner, + client_account, + client_public_key, + service.moon_work_resources.client_badge, + &service.work_components, + ); + + register_as_contractor( + &mut test_runner, + service.moon_work_component, + contractor_account, + contractor_public_key, + "beemdvp", + ); + + let work_id = NonFungibleId::from_u64(2); + + request_work( + &mut test_runner, + contractor_account, + contractor_public_key, + service.moon_work_resources.contractor_badge, + service.work_components.work_component, + work_id.clone(), + ); + + accept_contractor_for_work( + &mut test_runner, + service.work_components.work_component, + service.moon_work_resources.client_badge, + client_account, + client_public_key, + service.work_components.work_resource, + NonFungibleId::from_bytes("beemdvp".as_bytes().to_vec()), + work_id.clone(), + ); + + create_new_dispute( + &mut test_runner, + service.moon_work_resources.client_badge, + client_account, + client_public_key, + &service.work_components, + work_id, + ); + + let dispute_id = NonFungibleId::from_u64(1); + + let client_participant_receipt = join_and_decide_dispute( + &mut test_runner, + participant_client_account, + participant_client_public_key, + service.moon_work_resources.client_badge, + &service, + dispute_id.clone(), + DisputeSide::Contractor, + ); + + let contractor_participant_receipt = join_and_decide_dispute( + &mut test_runner, + participant_contractor_account, + participant_contractor_public_key, + service.moon_work_resources.contractor_badge, + &service, + dispute_id, + DisputeSide::Contractor, + ); + + // assert + client_participant_receipt.expect_commit_success(); + contractor_participant_receipt.expect_commit_success(); + + let dispute_nft = get_non_fungible_data::( + &store, + service.work_components.dispute_resource, + NonFungibleId::from_u64(1), + ); + + assert_eq!( + dispute_nft + .participant_clients + .get(&NonFungibleId::from_bytes("bar".as_bytes().to_vec())), + Some(&DisputeSide::Contractor) + ); + assert_eq!( + dispute_nft + .participant_contractors + .get(&NonFungibleId::from_bytes("foo".as_bytes().to_vec())), + Some(&DisputeSide::Contractor) + ); +} + +#[test] +fn moonwork_service_join_and_decide_dispute_fails_if_contractor_participates_again() { + // Setup the environment + let mut store = TypedInMemorySubstateStore::with_bootstrap(); + let mut test_runner = TestRunner::new(false, &mut store); + + let service = create_moon_work_service_with_work_category(&mut test_runner); + + let (participant_client_public_key, _private_key, participant_client_account) = + test_runner.new_account(); + + let (participant_contractor_public_key, _private_key, participant_contractor_account) = + test_runner.new_account(); + + register_as_client( + &mut test_runner, + service.moon_work_component, + participant_client_account, + participant_client_public_key, + "bar", + ); + + register_as_contractor( + &mut test_runner, + service.moon_work_component, + participant_contractor_account, + participant_contractor_public_key, + "foo", + ); + + create_work_to_finish_work( + &mut test_runner, + participant_client_account, + participant_contractor_account, + "foo", + &service.moon_work_resources, + &service.work_components, + participant_client_public_key, + participant_contractor_public_key, + 1, + ); + + claim_contractor_compensation( + &mut test_runner, + &service.moon_work_resources, + service.moon_work_component, + participant_contractor_account, + participant_contractor_public_key, + ); + + let (client_public_key, _private_key, client_account) = test_runner.new_account(); + + let (contractor_public_key, _private_key, contractor_account) = test_runner.new_account(); + + register_as_client( + &mut test_runner, + service.moon_work_component, + client_account, + client_public_key, + "slysmik", + ); + + create_new_work( + &mut test_runner, + client_account, + client_public_key, + service.moon_work_resources.client_badge, + &service.work_components, + ); + + register_as_contractor( + &mut test_runner, + service.moon_work_component, + contractor_account, + contractor_public_key, + "beemdvp", + ); + + let work_id = NonFungibleId::from_u64(2); + + request_work( + &mut test_runner, + contractor_account, + contractor_public_key, + service.moon_work_resources.contractor_badge, + service.work_components.work_component, + work_id.clone(), + ); + + accept_contractor_for_work( + &mut test_runner, + service.work_components.work_component, + service.moon_work_resources.client_badge, + client_account, + client_public_key, + service.work_components.work_resource, + NonFungibleId::from_bytes("beemdvp".as_bytes().to_vec()), + work_id.clone(), + ); + + create_new_dispute( + &mut test_runner, + service.moon_work_resources.client_badge, + client_account, + client_public_key, + &service.work_components, + work_id, + ); + + let dispute_id = NonFungibleId::from_u64(1); + + join_and_decide_dispute( + &mut test_runner, + participant_client_account, + participant_client_public_key, + service.moon_work_resources.client_badge, + &service, + dispute_id.clone(), + DisputeSide::Contractor, + ); + + join_and_decide_dispute( + &mut test_runner, + participant_contractor_account, + participant_contractor_public_key, + service.moon_work_resources.contractor_badge, + &service, + dispute_id.clone(), + DisputeSide::Contractor, + ); + + let receipt = join_and_decide_dispute( + &mut test_runner, + participant_contractor_account, + participant_contractor_public_key, + service.moon_work_resources.contractor_badge, + &service, + dispute_id, + DisputeSide::Contractor, + ); + + receipt.expect_commit_failure(); + assert_error_message(&receipt, "already joined dispute"); +} + +#[test] +fn moonwork_service_join_and_decide_dispute_fails_if_client_in_own_dispute_joins_as_participant() { + // Setup the environment + let mut store = TypedInMemorySubstateStore::with_bootstrap(); + let mut test_runner = TestRunner::new(false, &mut store); + + let service = create_moon_work_service_with_work_category(&mut test_runner); + + let (participant_client_public_key, _private_key, participant_client_account) = + test_runner.new_account(); + + let (participant_contractor_public_key, _private_key, participant_contractor_account) = + test_runner.new_account(); + + register_as_client( + &mut test_runner, + service.moon_work_component, + participant_client_account, + participant_client_public_key, + "bar", + ); + + register_as_contractor( + &mut test_runner, + service.moon_work_component, + participant_contractor_account, + participant_contractor_public_key, + "foo", + ); + + create_work_to_finish_work( + &mut test_runner, + participant_client_account, + participant_contractor_account, + "foo", + &service.moon_work_resources, + &service.work_components, + participant_client_public_key, + participant_contractor_public_key, + 1, + ); + + claim_contractor_compensation( + &mut test_runner, + &service.moon_work_resources, + service.moon_work_component, + participant_contractor_account, + participant_contractor_public_key, + ); + + let (client_public_key, _private_key, client_account) = test_runner.new_account(); + + let (contractor_public_key, _private_key, contractor_account) = test_runner.new_account(); + + register_as_client( + &mut test_runner, + service.moon_work_component, + client_account, + client_public_key, + "slysmik", + ); + + create_new_work( + &mut test_runner, + client_account, + client_public_key, + service.moon_work_resources.client_badge, + &service.work_components, + ); + + register_as_contractor( + &mut test_runner, + service.moon_work_component, + contractor_account, + contractor_public_key, + "beemdvp", + ); + + let work_id = NonFungibleId::from_u64(2); + + request_work( + &mut test_runner, + contractor_account, + contractor_public_key, + service.moon_work_resources.contractor_badge, + service.work_components.work_component, + work_id.clone(), + ); + + accept_contractor_for_work( + &mut test_runner, + service.work_components.work_component, + service.moon_work_resources.client_badge, + client_account, + client_public_key, + service.work_components.work_resource, + NonFungibleId::from_bytes("beemdvp".as_bytes().to_vec()), + work_id.clone(), + ); + + create_new_dispute( + &mut test_runner, + service.moon_work_resources.client_badge, + client_account, + client_public_key, + &service.work_components, + work_id, + ); + + let dispute_id = NonFungibleId::from_u64(1); + + join_and_decide_dispute( + &mut test_runner, + participant_client_account, + participant_client_public_key, + service.moon_work_resources.client_badge, + &service, + dispute_id.clone(), + DisputeSide::Contractor, + ); + + join_and_decide_dispute( + &mut test_runner, + participant_contractor_account, + participant_contractor_public_key, + service.moon_work_resources.contractor_badge, + &service, + dispute_id.clone(), + DisputeSide::Contractor, + ); + + let receipt = join_and_decide_dispute( + &mut test_runner, + client_account, + client_public_key, + service.moon_work_resources.client_badge, + &service, + dispute_id, + DisputeSide::Client, + ); + + receipt.expect_commit_failure(); + assert_error_message(&receipt, "cannot participate in own dispute"); +} + +#[test] +fn moonwork_service_join_and_decide_dispute_fails_if_contractor_in_own_dispute_joins_as_participant( +) { + // Setup the environment + let mut store = TypedInMemorySubstateStore::with_bootstrap(); + let mut test_runner = TestRunner::new(false, &mut store); + + let service = create_moon_work_service_with_work_category(&mut test_runner); + + let (participant_client_public_key, _private_key, participant_client_account) = + test_runner.new_account(); + + let (participant_contractor_public_key, _private_key, participant_contractor_account) = + test_runner.new_account(); + + register_as_client( + &mut test_runner, + service.moon_work_component, + participant_client_account, + participant_client_public_key, + "bar", + ); + + register_as_contractor( + &mut test_runner, + service.moon_work_component, + participant_contractor_account, + participant_contractor_public_key, + "foo", + ); + + create_work_to_finish_work( + &mut test_runner, + participant_client_account, + participant_contractor_account, + "foo", + &service.moon_work_resources, + &service.work_components, + participant_client_public_key, + participant_contractor_public_key, + 1, + ); + + claim_contractor_compensation( + &mut test_runner, + &service.moon_work_resources, + service.moon_work_component, + participant_contractor_account, + participant_contractor_public_key, + ); + + let (client_public_key, _private_key, client_account) = test_runner.new_account(); + + let (contractor_public_key, _private_key, contractor_account) = test_runner.new_account(); + + register_as_client( + &mut test_runner, + service.moon_work_component, + client_account, + client_public_key, + "slysmik", + ); + + create_new_work( + &mut test_runner, + client_account, + client_public_key, + service.moon_work_resources.client_badge, + &service.work_components, + ); + + register_as_contractor( + &mut test_runner, + service.moon_work_component, + contractor_account, + contractor_public_key, + "beemdvp", + ); + + let work_id = NonFungibleId::from_u64(2); + + request_work( + &mut test_runner, + contractor_account, + contractor_public_key, + service.moon_work_resources.contractor_badge, + service.work_components.work_component, + work_id.clone(), + ); + + accept_contractor_for_work( + &mut test_runner, + service.work_components.work_component, + service.moon_work_resources.client_badge, + client_account, + client_public_key, + service.work_components.work_resource, + NonFungibleId::from_bytes("beemdvp".as_bytes().to_vec()), + work_id.clone(), + ); + + create_new_dispute( + &mut test_runner, + service.moon_work_resources.client_badge, + client_account, + client_public_key, + &service.work_components, + work_id, + ); + + let dispute_id = NonFungibleId::from_u64(1); + + join_and_decide_dispute( + &mut test_runner, + participant_client_account, + participant_client_public_key, + service.moon_work_resources.client_badge, + &service, + dispute_id.clone(), + DisputeSide::Contractor, + ); + + join_and_decide_dispute( + &mut test_runner, + participant_contractor_account, + participant_contractor_public_key, + service.moon_work_resources.contractor_badge, + &service, + dispute_id.clone(), + DisputeSide::Contractor, + ); + + let receipt = join_and_decide_dispute( + &mut test_runner, + contractor_account, + contractor_public_key, + service.moon_work_resources.contractor_badge, + &service, + dispute_id, + DisputeSide::Contractor, + ); + + receipt.expect_commit_failure(); + assert_error_message(&receipt, "cannot participate in own dispute"); +} + +#[test] +fn moonwork_service_join_and_decide_dispute_fails_if_participation_expired() { + // Setup the environment + let mut store = TypedInMemorySubstateStore::with_bootstrap(); + let mut test_runner = TestRunner::new(false, &mut store); + + let service = create_moon_work_service_with_work_category(&mut test_runner); + + let (participant_client_public_key, _private_key, participant_client_account) = + test_runner.new_account(); + + let (participant_contractor_public_key, _private_key, participant_contractor_account) = + test_runner.new_account(); + + register_as_client( + &mut test_runner, + service.moon_work_component, + participant_client_account, + participant_client_public_key, + "bar", + ); + + register_as_contractor( + &mut test_runner, + service.moon_work_component, + participant_contractor_account, + participant_contractor_public_key, + "foo", + ); + + create_work_to_finish_work( + &mut test_runner, + participant_client_account, + participant_contractor_account, + "foo", + &service.moon_work_resources, + &service.work_components, + participant_client_public_key, + participant_contractor_public_key, + 1, + ); + + claim_contractor_compensation( + &mut test_runner, + &service.moon_work_resources, + service.moon_work_component, + participant_contractor_account, + participant_contractor_public_key, + ); + + let (client_public_key, _private_key, client_account) = test_runner.new_account(); + + let (contractor_public_key, _private_key, contractor_account) = test_runner.new_account(); + + register_as_client( + &mut test_runner, + service.moon_work_component, + client_account, + client_public_key, + "slysmik", + ); + + create_new_work( + &mut test_runner, + client_account, + client_public_key, + service.moon_work_resources.client_badge, + &service.work_components, + ); + + register_as_contractor( + &mut test_runner, + service.moon_work_component, + contractor_account, + contractor_public_key, + "beemdvp", + ); + + let work_id = NonFungibleId::from_u64(2); + + request_work( + &mut test_runner, + contractor_account, + contractor_public_key, + service.moon_work_resources.contractor_badge, + service.work_components.work_component, + work_id.clone(), + ); + + accept_contractor_for_work( + &mut test_runner, + service.work_components.work_component, + service.moon_work_resources.client_badge, + client_account, + client_public_key, + service.work_components.work_resource, + NonFungibleId::from_bytes("beemdvp".as_bytes().to_vec()), + work_id.clone(), + ); + + create_new_dispute( + &mut test_runner, + service.moon_work_resources.client_badge, + client_account, + client_public_key, + &service.work_components, + work_id, + ); + + let dispute_id = NonFungibleId::from_u64(1); + + let client_participant_receipt = join_and_decide_dispute( + &mut test_runner, + participant_client_account, + participant_client_public_key, + service.moon_work_resources.client_badge, + &service, + dispute_id.clone(), + DisputeSide::Contractor, + ); + + // assert + client_participant_receipt.expect_commit_success(); + + test_runner.set_current_epoch(241); + + let contractor_participant_receipt = join_and_decide_dispute( + &mut test_runner, + participant_contractor_account, + participant_contractor_public_key, + service.moon_work_resources.contractor_badge, + &service, + dispute_id, + DisputeSide::Contractor, + ); + + // assert + contractor_participant_receipt.expect_commit_failure(); + assert_error_message(&contractor_participant_receipt, "dispute expired"); +} + +#[test] +fn moonwork_service_join_and_decide_dispute_fails_if_participation_limit_has_been_reached() { + // Setup the environment + let mut store = TypedInMemorySubstateStore::with_bootstrap(); + let mut test_runner = TestRunner::new(false, &mut store); + + let service = create_moon_work_service_with_work_category(&mut test_runner); + + let (participant_client_public_key, _private_key, participant_client_account) = + test_runner.new_account(); + + let (participant_contractor_public_key, _private_key, participant_contractor_account) = + test_runner.new_account(); + + register_as_client( + &mut test_runner, + service.moon_work_component, + participant_client_account, + participant_client_public_key, + "bar", + ); + + register_as_contractor( + &mut test_runner, + service.moon_work_component, + participant_contractor_account, + participant_contractor_public_key, + "foo", + ); + + create_work_to_finish_work( + &mut test_runner, + participant_client_account, + participant_contractor_account, + "foo", + &service.moon_work_resources, + &service.work_components, + participant_client_public_key, + participant_contractor_public_key, + 1, + ); + + // create an extra participant who should not be able to join a dispute + let ( + extra_participant_contractor_public_key, + _private_key, + extra_participant_contractor_account, + ) = test_runner.new_account(); + + register_as_contractor( + &mut test_runner, + service.moon_work_component, + extra_participant_contractor_account, + extra_participant_contractor_public_key, + "extra", + ); + + create_work_to_finish_work( + &mut test_runner, + participant_client_account, + extra_participant_contractor_account, + "extra", + &service.moon_work_resources, + &service.work_components, + participant_client_public_key, + extra_participant_contractor_public_key, + 2, + ); + + claim_contractor_compensation( + &mut test_runner, + &service.moon_work_resources, + service.moon_work_component, + participant_contractor_account, + participant_contractor_public_key, + ); + + let (client_public_key, _private_key, client_account) = test_runner.new_account(); + + let (contractor_public_key, _private_key, contractor_account) = test_runner.new_account(); + + register_as_client( + &mut test_runner, + service.moon_work_component, + client_account, + client_public_key, + "slysmik", + ); + + create_new_work( + &mut test_runner, + client_account, + client_public_key, + service.moon_work_resources.client_badge, + &service.work_components, + ); + + register_as_contractor( + &mut test_runner, + service.moon_work_component, + contractor_account, + contractor_public_key, + "beemdvp", + ); + + let work_id = NonFungibleId::from_u64(3); + + request_work( + &mut test_runner, + contractor_account, + contractor_public_key, + service.moon_work_resources.contractor_badge, + service.work_components.work_component, + work_id.clone(), + ); + + accept_contractor_for_work( + &mut test_runner, + service.work_components.work_component, + service.moon_work_resources.client_badge, + client_account, + client_public_key, + service.work_components.work_resource, + NonFungibleId::from_bytes("beemdvp".as_bytes().to_vec()), + work_id.clone(), + ); + + create_new_dispute( + &mut test_runner, + service.moon_work_resources.client_badge, + client_account, + client_public_key, + &service.work_components, + work_id, + ); + + let dispute_id = NonFungibleId::from_u64(1); + + join_and_decide_dispute( + &mut test_runner, + participant_client_account, + participant_client_public_key, + service.moon_work_resources.client_badge, + &service, + dispute_id.clone(), + DisputeSide::Contractor, + ); + + join_and_decide_dispute( + &mut test_runner, + participant_contractor_account, + participant_contractor_public_key, + service.moon_work_resources.contractor_badge, + &service, + dispute_id.clone(), + DisputeSide::Contractor, + ); + + // try to have another contractor join a full participant + let receipt = join_and_decide_dispute( + &mut test_runner, + extra_participant_contractor_account, + extra_participant_contractor_public_key, + service.moon_work_resources.contractor_badge, + &service, + dispute_id, + DisputeSide::Contractor, + ); + + receipt.expect_commit_failure(); + assert_error_message(&receipt, "participant limit reached"); +} + +#[test] +fn moonwork_service_join_and_decide_dispute_client_fails_if_participation_limit_has_been_reached() { + // Setup the environment + let mut store = TypedInMemorySubstateStore::with_bootstrap(); + let mut test_runner = TestRunner::new(false, &mut store); + + let service = create_moon_work_service_with_work_category(&mut test_runner); + + let (participant_client_public_key, _private_key, participant_client_account) = + test_runner.new_account(); + + let (participant_contractor_public_key, _private_key, participant_contractor_account) = + test_runner.new_account(); + + register_as_client( + &mut test_runner, + service.moon_work_component, + participant_client_account, + participant_client_public_key, + "bar", + ); + + register_as_contractor( + &mut test_runner, + service.moon_work_component, + participant_contractor_account, + participant_contractor_public_key, + "foo", + ); + + create_work_to_finish_work( + &mut test_runner, + participant_client_account, + participant_contractor_account, + "foo", + &service.moon_work_resources, + &service.work_components, + participant_client_public_key, + participant_contractor_public_key, + 1, + ); + + // create an extra participant who should not be able to join a dispute + let (extra_participant_client_public_key, _private_key, extra_participant_client_account) = + test_runner.new_account(); + + register_as_client( + &mut test_runner, + service.moon_work_component, + extra_participant_client_account, + extra_participant_client_public_key, + "extra", + ); + + create_work_to_finish_work( + &mut test_runner, + extra_participant_client_account, + participant_contractor_account, + "foo", + &service.moon_work_resources, + &service.work_components, + extra_participant_client_public_key, + participant_contractor_public_key, + 2, + ); + + claim_contractor_compensation( + &mut test_runner, + &service.moon_work_resources, + service.moon_work_component, + participant_contractor_account, + participant_contractor_public_key, + ); + + let (client_public_key, _private_key, client_account) = test_runner.new_account(); + + let (contractor_public_key, _private_key, contractor_account) = test_runner.new_account(); + + register_as_client( + &mut test_runner, + service.moon_work_component, + client_account, + client_public_key, + "slysmik", + ); + + create_new_work( + &mut test_runner, + client_account, + client_public_key, + service.moon_work_resources.client_badge, + &service.work_components, + ); + + register_as_contractor( + &mut test_runner, + service.moon_work_component, + contractor_account, + contractor_public_key, + "beemdvp", + ); + + let work_id = NonFungibleId::from_u64(3); + + request_work( + &mut test_runner, + contractor_account, + contractor_public_key, + service.moon_work_resources.contractor_badge, + service.work_components.work_component, + work_id.clone(), + ); + + accept_contractor_for_work( + &mut test_runner, + service.work_components.work_component, + service.moon_work_resources.client_badge, + client_account, + client_public_key, + service.work_components.work_resource, + NonFungibleId::from_bytes("beemdvp".as_bytes().to_vec()), + work_id.clone(), + ); + + create_new_dispute( + &mut test_runner, + service.moon_work_resources.client_badge, + client_account, + client_public_key, + &service.work_components, + work_id, + ); + + let dispute_id = NonFungibleId::from_u64(1); + + join_and_decide_dispute( + &mut test_runner, + participant_client_account, + participant_client_public_key, + service.moon_work_resources.client_badge, + &service, + dispute_id.clone(), + DisputeSide::Contractor, + ); + + join_and_decide_dispute( + &mut test_runner, + participant_contractor_account, + participant_contractor_public_key, + service.moon_work_resources.contractor_badge, + &service, + dispute_id.clone(), + DisputeSide::Contractor, + ); + + // try to have another contractor join a full participant + let receipt = join_and_decide_dispute( + &mut test_runner, + extra_participant_client_account, + extra_participant_client_public_key, + service.moon_work_resources.client_badge, + &service, + dispute_id, + DisputeSide::Contractor, + ); + + receipt.expect_commit_failure(); + assert_error_message(&receipt, "participant limit reached"); +} + +#[test] +fn moonwork_service_join_and_decide_dispute_fails_if_contractor_does_not_meet_criteria() { + // Setup the environment + let mut store = TypedInMemorySubstateStore::with_bootstrap(); + let mut test_runner = TestRunner::new(false, &mut store); + + let service = create_moon_work_service_with_work_category(&mut test_runner); + + let (participant_contractor_public_key, _private_key, participant_contractor_account) = + test_runner.new_account(); + + register_as_contractor( + &mut test_runner, + service.moon_work_component, + participant_contractor_account, + participant_contractor_public_key, + "foo", + ); + + let (client_public_key, _private_key, client_account) = test_runner.new_account(); + + let (contractor_public_key, _private_key, contractor_account) = test_runner.new_account(); + + register_as_client( + &mut test_runner, + service.moon_work_component, + client_account, + client_public_key, + "slysmik", + ); + + create_new_work( + &mut test_runner, + client_account, + client_public_key, + service.moon_work_resources.client_badge, + &service.work_components, + ); + + register_as_contractor( + &mut test_runner, + service.moon_work_component, + contractor_account, + contractor_public_key, + "beemdvp", + ); + + let work_id = NonFungibleId::from_u64(1); + + request_work( + &mut test_runner, + contractor_account, + contractor_public_key, + service.moon_work_resources.contractor_badge, + service.work_components.work_component, + work_id.clone(), + ); + + accept_contractor_for_work( + &mut test_runner, + service.work_components.work_component, + service.moon_work_resources.client_badge, + client_account, + client_public_key, + service.work_components.work_resource, + NonFungibleId::from_bytes("beemdvp".as_bytes().to_vec()), + work_id.clone(), + ); + + create_new_dispute( + &mut test_runner, + service.moon_work_resources.client_badge, + client_account, + client_public_key, + &service.work_components, + work_id, + ); + + let dispute_id = NonFungibleId::from_u64(1); + + // act + // participant does not meet requirements (i.e. complete 1 work successfully) so this should + // fail + let contractor_participant_receipt = join_and_decide_dispute( + &mut test_runner, + participant_contractor_account, + participant_contractor_public_key, + service.moon_work_resources.contractor_badge, + &service, + dispute_id, + DisputeSide::Contractor, + ); + + // assert + contractor_participant_receipt.expect_commit_failure(); + assert_error_message( + &contractor_participant_receipt, + "contractor criteria not met", + ); +} + +#[test] +fn moonwork_service_join_and_decide_dispute_fails_if_client_does_not_meet_criteria() { + // Setup the environment + let mut store = TypedInMemorySubstateStore::with_bootstrap(); + let mut test_runner = TestRunner::new(false, &mut store); + + let service = create_moon_work_service_with_work_category(&mut test_runner); + + let (participant_client_public_key, _private_key, participant_client_account) = + test_runner.new_account(); + + register_as_client( + &mut test_runner, + service.moon_work_component, + participant_client_account, + participant_client_public_key, + "foo", + ); + + let (client_public_key, _private_key, client_account) = test_runner.new_account(); + + let (contractor_public_key, _private_key, contractor_account) = test_runner.new_account(); + + register_as_client( + &mut test_runner, + service.moon_work_component, + client_account, + client_public_key, + "slysmik", + ); + + create_new_work( + &mut test_runner, + client_account, + client_public_key, + service.moon_work_resources.client_badge, + &service.work_components, + ); + + register_as_contractor( + &mut test_runner, + service.moon_work_component, + contractor_account, + contractor_public_key, + "beemdvp", + ); + + let work_id = NonFungibleId::from_u64(1); + + request_work( + &mut test_runner, + contractor_account, + contractor_public_key, + service.moon_work_resources.contractor_badge, + service.work_components.work_component, + work_id.clone(), + ); + + accept_contractor_for_work( + &mut test_runner, + service.work_components.work_component, + service.moon_work_resources.client_badge, + client_account, + client_public_key, + service.work_components.work_resource, + NonFungibleId::from_bytes("beemdvp".as_bytes().to_vec()), + work_id.clone(), + ); + + create_new_dispute( + &mut test_runner, + service.moon_work_resources.client_badge, + client_account, + client_public_key, + &service.work_components, + work_id, + ); + + let dispute_id = NonFungibleId::from_u64(1); + + // act + // participant does not meet requirements (i.e. complete 1 work successfully) so this should + // fail + let client_participant_receipt = join_and_decide_dispute( + &mut test_runner, + participant_client_account, + participant_client_public_key, + service.moon_work_resources.client_badge, + &service, + dispute_id, + DisputeSide::Contractor, + ); + + // assert + client_participant_receipt.expect_commit_failure(); + assert_error_message(&client_participant_receipt, "client criteria not met"); +} + +#[test] +fn moonwork_service_complete_dispute() { + // Setup the environment + let mut store = TypedInMemorySubstateStore::with_bootstrap(); + let mut test_runner = TestRunner::new(false, &mut store); + + let service = create_moon_work_service_with_work_category(&mut test_runner); + + let (participant_client_public_key, _private_key, participant_client_account) = + test_runner.new_account(); + + let (participant_contractor_public_key, _private_key, participant_contractor_account) = + test_runner.new_account(); + + register_as_client( + &mut test_runner, + service.moon_work_component, + participant_client_account, + participant_client_public_key, + "bar", + ); + + register_as_contractor( + &mut test_runner, + service.moon_work_component, + participant_contractor_account, + participant_contractor_public_key, + "foo", + ); + + create_work_to_finish_work( + &mut test_runner, + participant_client_account, + participant_contractor_account, + "foo", + &service.moon_work_resources, + &service.work_components, + participant_client_public_key, + participant_contractor_public_key, + 1, + ); + + claim_contractor_compensation( + &mut test_runner, + &service.moon_work_resources, + service.moon_work_component, + participant_contractor_account, + participant_contractor_public_key, + ); + + let (client_public_key, _private_key, client_account) = test_runner.new_account(); + + let (contractor_public_key, _private_key, contractor_account) = test_runner.new_account(); + + register_as_client( + &mut test_runner, + service.moon_work_component, + client_account, + client_public_key, + "slysmik", + ); + + create_new_work( + &mut test_runner, + client_account, + client_public_key, + service.moon_work_resources.client_badge, + &service.work_components, + ); + + register_as_contractor( + &mut test_runner, + service.moon_work_component, + contractor_account, + contractor_public_key, + "beemdvp", + ); + + let work_id = NonFungibleId::from_u64(2); + + request_work( + &mut test_runner, + contractor_account, + contractor_public_key, + service.moon_work_resources.contractor_badge, + service.work_components.work_component, + work_id.clone(), + ); + + accept_contractor_for_work( + &mut test_runner, + service.work_components.work_component, + service.moon_work_resources.client_badge, + client_account, + client_public_key, + service.work_components.work_resource, + NonFungibleId::from_bytes("beemdvp".as_bytes().to_vec()), + work_id.clone(), + ); + + create_new_dispute( + &mut test_runner, + service.moon_work_resources.client_badge, + client_account, + client_public_key, + &service.work_components, + work_id.clone(), + ); + + let dispute_id = NonFungibleId::from_u64(1); + + join_and_decide_dispute( + &mut test_runner, + participant_client_account, + participant_client_public_key, + service.moon_work_resources.client_badge, + &service, + dispute_id.clone(), + DisputeSide::Contractor, + ); + + join_and_decide_dispute( + &mut test_runner, + participant_contractor_account, + participant_contractor_public_key, + service.moon_work_resources.contractor_badge, + &service, + dispute_id.clone(), + DisputeSide::Contractor, + ); + + let receipt = complete_dispute( + &mut test_runner, + &service, + client_account, + client_public_key, + dispute_id, + ); + + // assert + receipt.expect_commit_success(); + + let contractor_dispute_outcome_nft = get_non_fungible_data::( + &store, + service.moon_work_resources.disputed_outcome_resource, + NonFungibleId::from_u64(1), + ); + let client_dispute_outcome_nft = get_non_fungible_data::( + &store, + service.moon_work_resources.disputed_outcome_resource, + NonFungibleId::from_u64(2), + ); + + assert_eq!( + contractor_dispute_outcome_nft.work, + NonFungibleAddress::new(service.work_components.work_resource, work_id.clone()) + ); + assert_eq!( + contractor_dispute_outcome_nft.decision, + DisputeDecision::Won + ); + + assert_eq!( + client_dispute_outcome_nft.work, + NonFungibleAddress::new(service.work_components.work_resource, work_id) + ); + assert_eq!(client_dispute_outcome_nft.decision, DisputeDecision::Lost); +} + +#[test] +fn moonwork_service_complete_dispute_claim_work_refund_as_contractor() { + // Setup the environment + let mut store = TypedInMemorySubstateStore::with_bootstrap(); + let mut test_runner = TestRunner::new(false, &mut store); + + let service = create_moon_work_service_with_work_category(&mut test_runner); + + let (participant_client_public_key, _private_key, participant_client_account) = + test_runner.new_account(); + + let (participant_contractor_public_key, _private_key, participant_contractor_account) = + test_runner.new_account(); + + register_as_client( + &mut test_runner, + service.moon_work_component, + participant_client_account, + participant_client_public_key, + "bar", + ); + + register_as_contractor( + &mut test_runner, + service.moon_work_component, + participant_contractor_account, + participant_contractor_public_key, + "foo", + ); + + create_work_to_finish_work( + &mut test_runner, + participant_client_account, + participant_contractor_account, + "foo", + &service.moon_work_resources, + &service.work_components, + participant_client_public_key, + participant_contractor_public_key, + 1, + ); + + claim_contractor_compensation( + &mut test_runner, + &service.moon_work_resources, + service.moon_work_component, + participant_contractor_account, + participant_contractor_public_key, + ); + + let (client_public_key, _private_key, client_account) = test_runner.new_account(); + + let (contractor_public_key, _private_key, contractor_account) = test_runner.new_account(); + + register_as_client( + &mut test_runner, + service.moon_work_component, + client_account, + client_public_key, + "slysmik", + ); + + create_new_work( + &mut test_runner, + client_account, + client_public_key, + service.moon_work_resources.client_badge, + &service.work_components, + ); + + register_as_contractor( + &mut test_runner, + service.moon_work_component, + contractor_account, + contractor_public_key, + "beemdvp", + ); + + let work_id = NonFungibleId::from_u64(2); + + request_work( + &mut test_runner, + contractor_account, + contractor_public_key, + service.moon_work_resources.contractor_badge, + service.work_components.work_component, + work_id.clone(), + ); + + accept_contractor_for_work( + &mut test_runner, + service.work_components.work_component, + service.moon_work_resources.client_badge, + client_account, + client_public_key, + service.work_components.work_resource, + NonFungibleId::from_bytes("beemdvp".as_bytes().to_vec()), + work_id.clone(), + ); + + create_new_dispute( + &mut test_runner, + service.moon_work_resources.client_badge, + client_account, + client_public_key, + &service.work_components, + work_id.clone(), + ); + + let dispute_id = NonFungibleId::from_u64(1); + + join_and_decide_dispute( + &mut test_runner, + participant_client_account, + participant_client_public_key, + service.moon_work_resources.client_badge, + &service, + dispute_id.clone(), + DisputeSide::Client, + ); + + join_and_decide_dispute( + &mut test_runner, + participant_contractor_account, + participant_contractor_public_key, + service.moon_work_resources.contractor_badge, + &service, + dispute_id.clone(), + DisputeSide::Client, + ); + + let receipt = complete_dispute( + &mut test_runner, + &service, + client_account, + client_public_key, + dispute_id, + ); + + // assert + receipt.expect_commit_success(); + + let receipt = claim_client_work_refund( + &mut test_runner, + &service, + client_account, + client_public_key, + ); + + receipt.expect_commit_success(); + + let dispute_outcome_balance = get_account_balance( + &mut test_runner, + client_account, + client_public_key, + service.moon_work_resources.disputed_outcome_resource, + ); + + let client_nft = get_non_fungible_data::( + &store, + service.moon_work_resources.client_badge, + NonFungibleId::from_bytes("slysmik".as_bytes().to_vec()), + ); + + assert_eq!(dispute_outcome_balance, dec!(1)); + assert_eq!(client_nft.disputed, 1); +} + +#[test] +fn moonwork_service_complete_dispute_fails_if_not_expired_and_no_clear_decision() { + // Setup the environment + let mut store = TypedInMemorySubstateStore::with_bootstrap(); + let mut test_runner = TestRunner::new(false, &mut store); + + let service = create_moon_work_service_with_work_category(&mut test_runner); + + let (participant_client_public_key, _private_key, participant_client_account) = + test_runner.new_account(); + + let (participant_contractor_public_key, _private_key, participant_contractor_account) = + test_runner.new_account(); + + register_as_client( + &mut test_runner, + service.moon_work_component, + participant_client_account, + participant_client_public_key, + "bar", + ); + + register_as_contractor( + &mut test_runner, + service.moon_work_component, + participant_contractor_account, + participant_contractor_public_key, + "foo", + ); + + create_work_to_finish_work( + &mut test_runner, + participant_client_account, + participant_contractor_account, + "foo", + &service.moon_work_resources, + &service.work_components, + participant_client_public_key, + participant_contractor_public_key, + 1, + ); + + claim_contractor_compensation( + &mut test_runner, + &service.moon_work_resources, + service.moon_work_component, + participant_contractor_account, + participant_contractor_public_key, + ); + + let (client_public_key, _private_key, client_account) = test_runner.new_account(); + + let (contractor_public_key, _private_key, contractor_account) = test_runner.new_account(); + + register_as_client( + &mut test_runner, + service.moon_work_component, + client_account, + client_public_key, + "slysmik", + ); + + create_new_work( + &mut test_runner, + client_account, + client_public_key, + service.moon_work_resources.client_badge, + &service.work_components, + ); + + register_as_contractor( + &mut test_runner, + service.moon_work_component, + contractor_account, + contractor_public_key, + "beemdvp", + ); + + let work_id = NonFungibleId::from_u64(2); + + request_work( + &mut test_runner, + contractor_account, + contractor_public_key, + service.moon_work_resources.contractor_badge, + service.work_components.work_component, + work_id.clone(), + ); + + accept_contractor_for_work( + &mut test_runner, + service.work_components.work_component, + service.moon_work_resources.client_badge, + client_account, + client_public_key, + service.work_components.work_resource, + NonFungibleId::from_bytes("beemdvp".as_bytes().to_vec()), + work_id.clone(), + ); + + create_new_dispute( + &mut test_runner, + service.moon_work_resources.client_badge, + client_account, + client_public_key, + &service.work_components, + work_id.clone(), + ); + + let dispute_id = NonFungibleId::from_u64(1); + + join_and_decide_dispute( + &mut test_runner, + participant_client_account, + participant_client_public_key, + service.moon_work_resources.client_badge, + &service, + dispute_id.clone(), + DisputeSide::Contractor, + ); + + let receipt = complete_dispute( + &mut test_runner, + &service, + client_account, + client_public_key, + dispute_id, + ); + + // assert + receipt.expect_commit_failure(); + assert_error_message(&receipt, "requirements not met"); +} + +#[test] +fn moonwork_service_create_new_work_as_client_fails_if_dispute_outstanding_withrawal() { + let mut store = TypedInMemorySubstateStore::with_bootstrap(); + let mut test_runner = TestRunner::new(false, &mut store); + + let (client_public_key, _private_key, client_account) = test_runner.new_account(); + let (contractor_public_key, _private_key, contractor_account) = test_runner.new_account(); + + let service = create_moon_work_service_with_work_category(&mut test_runner); + + register_as_client( + &mut test_runner, + service.moon_work_component, + client_account, + client_public_key, + "slysmik", + ); + + register_as_contractor( + &mut test_runner, + service.moon_work_component, + contractor_account, + contractor_public_key, + "beemdvp", + ); + + let receipt = create_work_to_complete_dispute( + &mut test_runner, + &service, + client_account, + client_public_key, + contractor_account, + contractor_public_key, + 0, + ); + + receipt.expect_commit_success(); + + let receipt = create_new_work( + &mut test_runner, + client_account, + client_public_key, + service.moon_work_resources.client_badge, + &service.work_components, + ); + + receipt.expect_commit_failure(); + assert_error_message(&receipt, "disputes pending withdrawal"); +} + +#[test] +fn moonwork_service_remove_work_as_client_fails_if_dispute_outstanding_withrawal() { + let mut store = TypedInMemorySubstateStore::with_bootstrap(); + let mut test_runner = TestRunner::new(false, &mut store); + + let (client_public_key, _private_key, client_account) = test_runner.new_account(); + let (contractor_public_key, _private_key, contractor_account) = test_runner.new_account(); + + let service = create_moon_work_service_with_work_category(&mut test_runner); + + register_as_client( + &mut test_runner, + service.moon_work_component, + client_account, + client_public_key, + "slysmik", + ); + + register_as_contractor( + &mut test_runner, + service.moon_work_component, + contractor_account, + contractor_public_key, + "beemdvp", + ); + + create_new_work( + &mut test_runner, + client_account, + client_public_key, + service.moon_work_resources.client_badge, + &service.work_components, + ); + + let receipt = create_work_to_complete_dispute( + &mut test_runner, + &service, + client_account, + client_public_key, + contractor_account, + contractor_public_key, + 1, + ); + + receipt.expect_commit_success(); + + let receipt = remove_work( + &mut test_runner, + &service, + client_account, + client_public_key, + NonFungibleId::from_u64(1), + ); + + assert_error_message(&receipt, "disputes pending withdrawal"); + receipt.expect_commit_failure(); +} + +#[test] +fn moonwork_service_request_work_as_contractor_fails_if_outstanding_dispute_withdrawal() { + let mut store = TypedInMemorySubstateStore::with_bootstrap(); + let mut test_runner = TestRunner::new(false, &mut store); + + let (client_public_key, _private_key, client_account) = test_runner.new_account(); + let (contractor_public_key, _private_key, contractor_account) = test_runner.new_account(); + + let service = create_moon_work_service_with_work_category(&mut test_runner); + + register_as_client( + &mut test_runner, + service.moon_work_component, + client_account, + client_public_key, + "slysmik", + ); + + register_as_contractor( + &mut test_runner, + service.moon_work_component, + contractor_account, + contractor_public_key, + "beemdvp", + ); + + create_new_work( + &mut test_runner, + client_account, + client_public_key, + service.moon_work_resources.client_badge, + &service.work_components, + ); + + let receipt = create_work_to_complete_dispute( + &mut test_runner, + &service, + client_account, + client_public_key, + contractor_account, + contractor_public_key, + 1, + ); + + receipt.expect_commit_success(); + + let receipt = request_work( + &mut test_runner, + contractor_account, + contractor_public_key, + service.moon_work_resources.contractor_badge, + service.work_components.work_component, + NonFungibleId::from_u64(1), + ); + + receipt.expect_commit_failure(); + assert_error_message(&receipt, "disputes pending withdrawal"); +} + +#[test] +fn moonwork_service_complete_dispute_fails_because_split_decision() { + // Setup the environment + let mut store = TypedInMemorySubstateStore::with_bootstrap(); + let mut test_runner = TestRunner::new(false, &mut store); + + let service = create_moon_work_service_with_work_category(&mut test_runner); + + let (participant_client_public_key, _private_key, participant_client_account) = + test_runner.new_account(); + + let (participant_contractor_public_key, _private_key, participant_contractor_account) = + test_runner.new_account(); + + register_as_client( + &mut test_runner, + service.moon_work_component, + participant_client_account, + participant_client_public_key, + "bar", + ); + + register_as_contractor( + &mut test_runner, + service.moon_work_component, + participant_contractor_account, + participant_contractor_public_key, + "foo", + ); + + create_work_to_finish_work( + &mut test_runner, + participant_client_account, + participant_contractor_account, + "foo", + &service.moon_work_resources, + &service.work_components, + participant_client_public_key, + participant_contractor_public_key, + 1, + ); + + claim_contractor_compensation( + &mut test_runner, + &service.moon_work_resources, + service.moon_work_component, + participant_contractor_account, + participant_contractor_public_key, + ); + + let (client_public_key, _private_key, client_account) = test_runner.new_account(); + + let (contractor_public_key, _private_key, contractor_account) = test_runner.new_account(); + + register_as_client( + &mut test_runner, + service.moon_work_component, + client_account, + client_public_key, + "slysmik", + ); + + create_new_work( + &mut test_runner, + client_account, + client_public_key, + service.moon_work_resources.client_badge, + &service.work_components, + ); + + register_as_contractor( + &mut test_runner, + service.moon_work_component, + contractor_account, + contractor_public_key, + "beemdvp", + ); + + let work_id = NonFungibleId::from_u64(2); + + request_work( + &mut test_runner, + contractor_account, + contractor_public_key, + service.moon_work_resources.contractor_badge, + service.work_components.work_component, + work_id.clone(), + ); + + accept_contractor_for_work( + &mut test_runner, + service.work_components.work_component, + service.moon_work_resources.client_badge, + client_account, + client_public_key, + service.work_components.work_resource, + NonFungibleId::from_bytes("beemdvp".as_bytes().to_vec()), + work_id.clone(), + ); + + create_new_dispute( + &mut test_runner, + service.moon_work_resources.client_badge, + client_account, + client_public_key, + &service.work_components, + work_id.clone(), + ); + + let dispute_id = NonFungibleId::from_u64(1); + + join_and_decide_dispute( + &mut test_runner, + participant_client_account, + participant_client_public_key, + service.moon_work_resources.client_badge, + &service, + dispute_id.clone(), + DisputeSide::Client, + ); + + join_and_decide_dispute( + &mut test_runner, + participant_contractor_account, + participant_contractor_public_key, + service.moon_work_resources.contractor_badge, + &service, + dispute_id.clone(), + DisputeSide::Contractor, + ); + + let receipt = complete_dispute( + &mut test_runner, + &service, + client_account, + client_public_key, + dispute_id, + ); + + // assert + receipt.expect_commit_failure(); + assert_error_message(&receipt, "split decision - admin decision"); +} + +#[test] +fn moonwork_service_complete_dispute_fails_if_not_involved() { + // Setup the environment + let mut store = TypedInMemorySubstateStore::with_bootstrap(); + let mut test_runner = TestRunner::new(false, &mut store); + + let service = create_moon_work_service_with_work_category(&mut test_runner); + + let (participant_client_public_key, _private_key, participant_client_account) = + test_runner.new_account(); + + let (participant_contractor_public_key, _private_key, participant_contractor_account) = + test_runner.new_account(); + + register_as_client( + &mut test_runner, + service.moon_work_component, + participant_client_account, + participant_client_public_key, + "bar", + ); + + register_as_contractor( + &mut test_runner, + service.moon_work_component, + participant_contractor_account, + participant_contractor_public_key, + "foo", + ); + + create_work_to_finish_work( + &mut test_runner, + participant_client_account, + participant_contractor_account, + "foo", + &service.moon_work_resources, + &service.work_components, + participant_client_public_key, + participant_contractor_public_key, + 1, + ); + + claim_contractor_compensation( + &mut test_runner, + &service.moon_work_resources, + service.moon_work_component, + participant_contractor_account, + participant_contractor_public_key, + ); + + let (client_public_key, _private_key, client_account) = test_runner.new_account(); + + let (contractor_public_key, _private_key, contractor_account) = test_runner.new_account(); + + register_as_client( + &mut test_runner, + service.moon_work_component, + client_account, + client_public_key, + "slysmik", + ); + + create_new_work( + &mut test_runner, + client_account, + client_public_key, + service.moon_work_resources.client_badge, + &service.work_components, + ); + + register_as_contractor( + &mut test_runner, + service.moon_work_component, + contractor_account, + contractor_public_key, + "beemdvp", + ); + + let work_id = NonFungibleId::from_u64(2); + + request_work( + &mut test_runner, + contractor_account, + contractor_public_key, + service.moon_work_resources.contractor_badge, + service.work_components.work_component, + work_id.clone(), + ); + + accept_contractor_for_work( + &mut test_runner, + service.work_components.work_component, + service.moon_work_resources.client_badge, + client_account, + client_public_key, + service.work_components.work_resource, + NonFungibleId::from_bytes("beemdvp".as_bytes().to_vec()), + work_id.clone(), + ); + + create_new_dispute( + &mut test_runner, + service.moon_work_resources.client_badge, + client_account, + client_public_key, + &service.work_components, + work_id.clone(), + ); + + let dispute_id = NonFungibleId::from_u64(1); + + join_and_decide_dispute( + &mut test_runner, + participant_client_account, + participant_client_public_key, + service.moon_work_resources.client_badge, + &service, + dispute_id.clone(), + DisputeSide::Contractor, + ); + + join_and_decide_dispute( + &mut test_runner, + participant_contractor_account, + participant_contractor_public_key, + service.moon_work_resources.contractor_badge, + &service, + dispute_id.clone(), + DisputeSide::Contractor, + ); + + let receipt = complete_dispute( + &mut test_runner, + &service, + // an account outside to complete dispute, this should fail + participant_client_account, + participant_contractor_public_key, + dispute_id, + ); + + // assert + receipt.expect_commit_failure(); +} + +#[test] +fn moonwork_service_complete_dispute_contractor_win_as_admin_only_when_split_decision_and_dispute_expired( +) { + // Setup the environment + let mut store = TypedInMemorySubstateStore::with_bootstrap(); + let mut test_runner = TestRunner::new(false, &mut store); + + let service = create_moon_work_service_with_work_category(&mut test_runner); + + let (participant_client_public_key, _private_key, participant_client_account) = + test_runner.new_account(); + + let (participant_contractor_public_key, _private_key, participant_contractor_account) = + test_runner.new_account(); + + register_as_client( + &mut test_runner, + service.moon_work_component, + participant_client_account, + participant_client_public_key, + "bar", + ); + + register_as_contractor( + &mut test_runner, + service.moon_work_component, + participant_contractor_account, + participant_contractor_public_key, + "foo", + ); + + create_work_to_finish_work( + &mut test_runner, + participant_client_account, + participant_contractor_account, + "foo", + &service.moon_work_resources, + &service.work_components, + participant_client_public_key, + participant_contractor_public_key, + 1, + ); + + claim_contractor_compensation( + &mut test_runner, + &service.moon_work_resources, + service.moon_work_component, + participant_contractor_account, + participant_contractor_public_key, + ); + + let (client_public_key, _private_key, client_account) = test_runner.new_account(); + + let (contractor_public_key, _private_key, contractor_account) = test_runner.new_account(); + + register_as_client( + &mut test_runner, + service.moon_work_component, + client_account, + client_public_key, + "slysmik", + ); + + create_new_work( + &mut test_runner, + client_account, + client_public_key, + service.moon_work_resources.client_badge, + &service.work_components, + ); + + register_as_contractor( + &mut test_runner, + service.moon_work_component, + contractor_account, + contractor_public_key, + "beemdvp", + ); + + let work_id = NonFungibleId::from_u64(2); + + request_work( + &mut test_runner, + contractor_account, + contractor_public_key, + service.moon_work_resources.contractor_badge, + service.work_components.work_component, + work_id.clone(), + ); + + accept_contractor_for_work( + &mut test_runner, + service.work_components.work_component, + service.moon_work_resources.client_badge, + client_account, + client_public_key, + service.work_components.work_resource, + NonFungibleId::from_bytes("beemdvp".as_bytes().to_vec()), + work_id.clone(), + ); + + create_new_dispute( + &mut test_runner, + service.moon_work_resources.client_badge, + client_account, + client_public_key, + &service.work_components, + work_id.clone(), + ); + + let dispute_id = NonFungibleId::from_u64(1); + + join_and_decide_dispute( + &mut test_runner, + participant_client_account, + participant_client_public_key, + service.moon_work_resources.client_badge, + &service, + dispute_id.clone(), + DisputeSide::Client, + ); + + join_and_decide_dispute( + &mut test_runner, + participant_contractor_account, + participant_contractor_public_key, + service.moon_work_resources.contractor_badge, + &service, + dispute_id.clone(), + DisputeSide::Contractor, + ); + + test_runner.set_current_epoch(241); + + let receipt = complete_dispute_as_admin( + &mut test_runner, + &service, + dispute_id, + DisputeSide::Contractor, + ); + // assert + receipt.expect_commit_success(); + + let contractor_dispute_outcome = get_non_fungible_data::( + &store, + service.moon_work_resources.disputed_outcome_resource, + NonFungibleId::from_u64(1), + ); + + let client_dispute_outcome = get_non_fungible_data::( + &store, + service.moon_work_resources.disputed_outcome_resource, + NonFungibleId::from_u64(2), + ); + + assert_eq!(contractor_dispute_outcome.decision, DisputeDecision::Won); + assert_eq!(client_dispute_outcome.decision, DisputeDecision::Lost); +} + +#[test] +fn moonwork_service_complete_dispute_client_win_as_admin_only_when_split_decision_and_dispute_expired( +) { + // Setup the environment + let mut store = TypedInMemorySubstateStore::with_bootstrap(); + let mut test_runner = TestRunner::new(false, &mut store); + + let service = create_moon_work_service_with_work_category(&mut test_runner); + + let (participant_client_public_key, _private_key, participant_client_account) = + test_runner.new_account(); + + let (participant_contractor_public_key, _private_key, participant_contractor_account) = + test_runner.new_account(); + + register_as_client( + &mut test_runner, + service.moon_work_component, + participant_client_account, + participant_client_public_key, + "bar", + ); + + register_as_contractor( + &mut test_runner, + service.moon_work_component, + participant_contractor_account, + participant_contractor_public_key, + "foo", + ); + + create_work_to_finish_work( + &mut test_runner, + participant_client_account, + participant_contractor_account, + "foo", + &service.moon_work_resources, + &service.work_components, + participant_client_public_key, + participant_contractor_public_key, + 1, + ); + + claim_contractor_compensation( + &mut test_runner, + &service.moon_work_resources, + service.moon_work_component, + participant_contractor_account, + participant_contractor_public_key, + ); + + let (client_public_key, _private_key, client_account) = test_runner.new_account(); + + let (contractor_public_key, _private_key, contractor_account) = test_runner.new_account(); + + register_as_client( + &mut test_runner, + service.moon_work_component, + client_account, + client_public_key, + "slysmik", + ); + + create_new_work( + &mut test_runner, + client_account, + client_public_key, + service.moon_work_resources.client_badge, + &service.work_components, + ); + + register_as_contractor( + &mut test_runner, + service.moon_work_component, + contractor_account, + contractor_public_key, + "beemdvp", + ); + + let work_id = NonFungibleId::from_u64(2); + + request_work( + &mut test_runner, + contractor_account, + contractor_public_key, + service.moon_work_resources.contractor_badge, + service.work_components.work_component, + work_id.clone(), + ); + + accept_contractor_for_work( + &mut test_runner, + service.work_components.work_component, + service.moon_work_resources.client_badge, + client_account, + client_public_key, + service.work_components.work_resource, + NonFungibleId::from_bytes("beemdvp".as_bytes().to_vec()), + work_id.clone(), + ); + + create_new_dispute( + &mut test_runner, + service.moon_work_resources.client_badge, + client_account, + client_public_key, + &service.work_components, + work_id.clone(), + ); + + let dispute_id = NonFungibleId::from_u64(1); + + join_and_decide_dispute( + &mut test_runner, + participant_client_account, + participant_client_public_key, + service.moon_work_resources.client_badge, + &service, + dispute_id.clone(), + DisputeSide::Client, + ); + + join_and_decide_dispute( + &mut test_runner, + participant_contractor_account, + participant_contractor_public_key, + service.moon_work_resources.contractor_badge, + &service, + dispute_id.clone(), + DisputeSide::Contractor, + ); + + test_runner.set_current_epoch(241); + + let receipt = + complete_dispute_as_admin(&mut test_runner, &service, dispute_id, DisputeSide::Client); + // assert + receipt.expect_commit_success(); + + let contractor_dispute_outcome = get_non_fungible_data::( + &store, + service.moon_work_resources.disputed_outcome_resource, + NonFungibleId::from_u64(1), + ); + + let client_dispute_outcome = get_non_fungible_data::( + &store, + service.moon_work_resources.disputed_outcome_resource, + NonFungibleId::from_u64(2), + ); + + assert_eq!(contractor_dispute_outcome.decision, DisputeDecision::Lost); + assert_eq!(client_dispute_outcome.decision, DisputeDecision::Won); +} + +#[test] +fn test_create_moonwork_service_create_promotion_service() { + // Setup the environment + let mut store = TypedInMemorySubstateStore::with_bootstrap(); + let mut test_runner = TestRunner::new(false, &mut store); + + let (public_key, _private_key, account_component) = test_runner.new_account(); + + // Creating moonwork service + let (component, resources) = + create_moonwork_service(&mut test_runner, account_component, public_key); + + create_promotion_service( + &mut test_runner, + &resources, + account_component, + component, + public_key, + ); +} + +#[test] +fn test_create_moonwork_service_promotion_service_promote_contractor() { + // Setup the environment + let mut store = TypedInMemorySubstateStore::with_bootstrap(); + let mut test_runner = TestRunner::new(false, &mut store); + + let (client_public_key, _private_key, client_account) = test_runner.new_account(); + + let service = create_moon_work_service_with_work_category(&mut test_runner); + + let promotion_service = create_promotion_service( + &mut test_runner, + &service.moon_work_resources, + service.service_admin_component_address, + service.moon_work_component, + service.service_admin_public_key, + ); + + register_as_client( + &mut test_runner, + service.moon_work_component, + client_account, + client_public_key, + "slysmik", + ); + + let (contractor_public_key, _private_key, contractor_account) = test_runner.new_account(); + + register_as_contractor( + &mut test_runner, + service.moon_work_component, + contractor_account, + contractor_public_key, + "beemdvp", + ); + + for id in 1..21 { + create_work_to_finish_work( + &mut test_runner, + client_account, + contractor_account, + "beemdvp", + &service.moon_work_resources, + &service.work_components, + client_public_key, + contractor_public_key, + id, + ); + } + + claim_contractor_compensation( + &mut test_runner, + &service.moon_work_resources, + service.moon_work_component, + contractor_account, + contractor_public_key, + ); + + let receipt = promote_contractor( + &mut test_runner, + &service, + &promotion_service, + contractor_account, + contractor_public_key, + 20, + 3, + ); + + receipt.expect_commit_success(); + + let promoted_contractor = get_non_fungible_data::( + &store, + promotion_service.contractor_promotion, + NonFungibleId::from_u64(1), + ); + + assert_eq!( + promoted_contractor.contractor_id, + NonFungibleId::from_bytes("beemdvp".as_bytes().to_vec()) + ); +} + +#[test] +fn test_create_moonwork_service_promotion_service_promote_contractor_fails_if_already_running_promotion( +) { + // Setup the environment + let mut store = TypedInMemorySubstateStore::with_bootstrap(); + let mut test_runner = TestRunner::new(false, &mut store); + + let (client_public_key, _private_key, client_account) = test_runner.new_account(); + + let service = create_moon_work_service_with_work_category(&mut test_runner); + + let promotion_service = create_promotion_service( + &mut test_runner, + &service.moon_work_resources, + service.service_admin_component_address, + service.moon_work_component, + service.service_admin_public_key, + ); + + register_as_client( + &mut test_runner, + service.moon_work_component, + client_account, + client_public_key, + "slysmik", + ); + + let (contractor_public_key, _private_key, contractor_account) = test_runner.new_account(); + + register_as_contractor( + &mut test_runner, + service.moon_work_component, + contractor_account, + contractor_public_key, + "beemdvp", + ); + + for id in 1..21 { + create_work_to_finish_work( + &mut test_runner, + client_account, + contractor_account, + "beemdvp", + &service.moon_work_resources, + &service.work_components, + client_public_key, + contractor_public_key, + id, + ); + } + + claim_contractor_compensation( + &mut test_runner, + &service.moon_work_resources, + service.moon_work_component, + contractor_account, + contractor_public_key, + ); + + promote_contractor( + &mut test_runner, + &service, + &promotion_service, + contractor_account, + contractor_public_key, + 20, + 3, + ); + + let receipt = promote_contractor( + &mut test_runner, + &service, + &promotion_service, + contractor_account, + contractor_public_key, + 20, + 3, + ); + + receipt.expect_commit_failure(); + assert_error_message(&receipt, "already promoted"); +} + +#[test] +fn test_create_moonwork_service_promotion_service_promote_contractor_remove_expired_promotions() { + // Setup the environment + let mut store = TypedInMemorySubstateStore::with_bootstrap(); + let mut test_runner = TestRunner::new(false, &mut store); + + let (client_public_key, _private_key, client_account) = test_runner.new_account(); + + let service = create_moon_work_service_with_work_category(&mut test_runner); + + let promotion_service = create_promotion_service( + &mut test_runner, + &service.moon_work_resources, + service.service_admin_component_address, + service.moon_work_component, + service.service_admin_public_key, + ); + + register_as_client( + &mut test_runner, + service.moon_work_component, + client_account, + client_public_key, + "slysmik", + ); + + let (contractor_public_key, _private_key, contractor_account) = test_runner.new_account(); + + register_as_contractor( + &mut test_runner, + service.moon_work_component, + contractor_account, + contractor_public_key, + "beemdvp", + ); + + let (second_contractor_public_key, _private_key, second_contractor_account) = + test_runner.new_account(); + + register_as_contractor( + &mut test_runner, + service.moon_work_component, + second_contractor_account, + second_contractor_public_key, + "second", + ); + + for id in 1..21 { + create_work_to_finish_work( + &mut test_runner, + client_account, + contractor_account, + "beemdvp", + &service.moon_work_resources, + &service.work_components, + client_public_key, + contractor_public_key, + id, + ); + } + + for id in 20..31 { + create_work_to_finish_work( + &mut test_runner, + client_account, + contractor_account, + "second", + &service.moon_work_resources, + &service.work_components, + client_public_key, + second_contractor_public_key, + id, + ); + } + + claim_contractor_compensation( + &mut test_runner, + &service.moon_work_resources, + service.moon_work_component, + contractor_account, + contractor_public_key, + ); + + promote_contractor( + &mut test_runner, + &service, + &promotion_service, + contractor_account, + contractor_public_key, + 20, + 3, + ); + + promote_contractor( + &mut test_runner, + &service, + &promotion_service, + second_contractor_account, + second_contractor_public_key, + 20, + 3, + ); + + test_runner.set_current_epoch(481); + + let receipt = remove_expired_promotions(&mut test_runner, &service, &promotion_service); + + receipt.expect_commit_success(); + + assert_eq!( + get_total_supply(&store, promotion_service.contractor_promotion), + Decimal::zero() + ); +} + +#[test] +fn test_create_moonwork_service_promotion_service_promote_contractor_fails_if_not_enough_work_completed( +) { + // Setup the environment + let mut store = TypedInMemorySubstateStore::with_bootstrap(); + let mut test_runner = TestRunner::new(false, &mut store); + + let (client_public_key, _private_key, client_account) = test_runner.new_account(); + + let service = create_moon_work_service_with_work_category(&mut test_runner); + + let promotion_service = create_promotion_service( + &mut test_runner, + &service.moon_work_resources, + service.service_admin_component_address, + service.moon_work_component, + service.service_admin_public_key, + ); + + register_as_client( + &mut test_runner, + service.moon_work_component, + client_account, + client_public_key, + "slysmik", + ); + + let (contractor_public_key, _private_key, contractor_account) = test_runner.new_account(); + + register_as_contractor( + &mut test_runner, + service.moon_work_component, + contractor_account, + contractor_public_key, + "beemdvp", + ); + + for id in 1..11 { + create_work_to_finish_work( + &mut test_runner, + client_account, + contractor_account, + "beemdvp", + &service.moon_work_resources, + &service.work_components, + client_public_key, + contractor_public_key, + id, + ); + } + + claim_contractor_compensation( + &mut test_runner, + &service.moon_work_resources, + service.moon_work_component, + contractor_account, + contractor_public_key, + ); + + let receipt = promote_contractor( + &mut test_runner, + &service, + &promotion_service, + contractor_account, + contractor_public_key, + 10, + 2, + ); + + assert_error_message(&receipt, "not enough work"); + + receipt.expect_commit_failure(); +} diff --git a/6-nfts-for-financial-applications/moonwork/tests/manifests.rs b/6-nfts-for-financial-applications/moonwork/tests/manifests.rs new file mode 100644 index 000000000..2cec58199 --- /dev/null +++ b/6-nfts-for-financial-applications/moonwork/tests/manifests.rs @@ -0,0 +1,928 @@ +use ::moonwork::dispute::*; +use radix_engine::ledger::*; +use radix_engine::model::ResourceManager; +use radix_engine::transaction::CommitResult; +use radix_engine::transaction::TransactionReceipt; +use radix_engine::types::*; +use scrypto::core::NetworkDefinition; +use scrypto::resource::Bucket; +use scrypto::resource::NonFungibleData; +use scrypto::resource::Proof; +use scrypto::*; +use scrypto_unit::*; +use transaction::builder::ManifestBuilder; + +pub fn claim_client_work_refund( + test_runner: &mut TestRunner, + service: &MoonWorkService, + client_account: ComponentAddress, + client_public_key: EcdsaSecp256k1PublicKey, +) -> radix_engine::transaction::TransactionReceipt { + let manifest = ManifestBuilder::new(&NetworkDefinition::simulator()) + .create_proof_from_account(service.moon_work_resources.client_badge, client_account) + .pop_from_auth_zone(|builder, proof| { + builder + .call_method( + service.moon_work_component, + "claim_client_work_refund", + args!(Proof(proof)), + ) + .call_method( + client_account, + "deposit_batch", + args!(Expression::entire_worktop()), + ) + }) + .build(); + let receipt = + test_runner.execute_manifest_ignoring_fee(manifest, vec![client_public_key.into()]); + receipt +} + +pub fn remove_expired_promotions( + test_runner: &mut TestRunner, + service: &MoonWorkService, + promotion_service: &PromotionService, +) -> TransactionReceipt { + let manifest = ManifestBuilder::new(&NetworkDefinition::simulator()) + .create_proof_from_account( + service.moon_work_resources.service_admin_badge, + service.service_admin_component_address, + ) + .call_method( + promotion_service.component, + "remove_expired_promotions", + args!(), + ) + .build(); + test_runner + .execute_manifest_ignoring_fee(manifest, vec![service.service_admin_public_key.into()]) +} + +pub fn promote_contractor( + test_runner: &mut TestRunner, + service: &MoonWorkService, + promotion_service: &PromotionService, + contractor_account: ComponentAddress, + contractor_public_key: EcdsaSecp256k1PublicKey, + work_resource_amount: u64, + accolade_resource_amount: u64, +) -> radix_engine::transaction::TransactionReceipt { + let manifest = ManifestBuilder::new(&NetworkDefinition::simulator()) + .create_proof_from_account_by_amount( + work_resource_amount.into(), + service.moon_work_resources.completed_work_resource, + contractor_account, + ) + .pop_from_auth_zone(|builder, work_completed_proof| { + builder + .create_proof_from_account_by_amount( + accolade_resource_amount.into(), + service.moon_work_resources.contractor_accolade_resource, + contractor_account, + ) + .pop_from_auth_zone(|builder, accolades_proof| { + builder + .create_proof_from_account( + service.moon_work_resources.contractor_badge, + contractor_account, + ) + .create_proof_from_auth_zone_by_amount( + dec!(1), + service.moon_work_resources.contractor_badge, + |builder, proof| { + builder + .call_method( + promotion_service.component, + "promote_contractor", + args!( + Proof(work_completed_proof), + Proof(accolades_proof), + Proof(proof) + ), + ) + .drop_all_proofs() + }, + ) + }) + }) + .build(); + let receipt = + test_runner.execute_manifest_ignoring_fee(manifest, vec![contractor_public_key.into()]); + receipt +} + +pub struct PromotionService { + pub component: ComponentAddress, + pub contractor_promotion: ResourceAddress, +} + +pub fn create_promotion_service( + test_runner: &mut TestRunner, + resources: &MoonWorkResource, + account_component: ComponentAddress, + component: ComponentAddress, + public_key: EcdsaSecp256k1PublicKey, +) -> PromotionService { + let manifest = ManifestBuilder::new(&NetworkDefinition::simulator()) + .create_proof_from_account(resources.service_admin_badge, account_component) + .call_method(component, "create_promotion_service", args!()) + .build(); + let receipt = test_runner.execute_manifest_ignoring_fee(manifest, vec![public_key.into()]); + + receipt.expect_commit_success(); + + let result = receipt.expect_commit(); + + PromotionService { + component: result.entity_changes.new_component_addresses[0], + contractor_promotion: result.entity_changes.new_resource_addresses[0], + } +} + +pub fn create_work_to_complete_dispute( + test_runner: &mut TestRunner, + service: &MoonWorkService, + client_account: ComponentAddress, + client_public_key: EcdsaSecp256k1PublicKey, + contractor_account: ComponentAddress, + contractor_public_key: EcdsaSecp256k1PublicKey, + last_created_work_id: u64, +) -> radix_engine::transaction::TransactionReceipt { + let (participant_client_public_key, _private_key, participant_client_account) = + test_runner.new_account(); + let (participant_contractor_public_key, _private_key, participant_contractor_account) = + test_runner.new_account(); + register_as_client( + test_runner, + service.moon_work_component, + participant_client_account, + participant_client_public_key, + "bar", + ); + register_as_contractor( + test_runner, + service.moon_work_component, + participant_contractor_account, + participant_contractor_public_key, + "foo", + ); + create_work_to_finish_work( + test_runner, + participant_client_account, + participant_contractor_account, + "foo", + &service.moon_work_resources, + &service.work_components, + participant_client_public_key, + participant_contractor_public_key, + last_created_work_id + 1, + ); + claim_contractor_compensation( + test_runner, + &service.moon_work_resources, + service.moon_work_component, + participant_contractor_account, + participant_contractor_public_key, + ); + create_new_work( + test_runner, + client_account, + client_public_key, + service.moon_work_resources.client_badge, + &service.work_components, + ); + let work_id = NonFungibleId::from_u64(last_created_work_id + 2); + request_work( + test_runner, + contractor_account, + contractor_public_key, + service.moon_work_resources.contractor_badge, + service.work_components.work_component, + work_id.clone(), + ); + accept_contractor_for_work( + test_runner, + service.work_components.work_component, + service.moon_work_resources.client_badge, + client_account, + client_public_key, + service.work_components.work_resource, + NonFungibleId::from_bytes("beemdvp".as_bytes().to_vec()), + work_id.clone(), + ); + create_new_dispute( + test_runner, + service.moon_work_resources.client_badge, + client_account, + client_public_key, + &service.work_components, + work_id.clone(), + ); + let dispute_id = NonFungibleId::from_u64(1); + join_and_decide_dispute( + test_runner, + participant_client_account, + participant_client_public_key, + service.moon_work_resources.client_badge, + &service, + dispute_id.clone(), + DisputeSide::Contractor, + ); + join_and_decide_dispute( + test_runner, + participant_contractor_account, + participant_contractor_public_key, + service.moon_work_resources.contractor_badge, + &service, + dispute_id.clone(), + DisputeSide::Contractor, + ); + let receipt = complete_dispute( + test_runner, + &service, + client_account, + client_public_key, + dispute_id, + ); + + receipt +} + +pub fn cancel_dispute( + test_runner: &mut TestRunner, + service: &MoonWorkService, + badge: ResourceAddress, + contractor_account: ComponentAddress, + contractor_public_key: EcdsaSecp256k1PublicKey, + dispute_id: NonFungibleId, +) -> radix_engine::transaction::TransactionReceipt { + let manifest = ManifestBuilder::new(&NetworkDefinition::simulator()) + .create_proof_from_account(badge, contractor_account) + .pop_from_auth_zone(|builder, proof| { + builder + .call_method( + service.work_components.dispute_component, + "cancel_dispute", + args!(dispute_id, Proof(proof)), + ) + .drop_all_proofs() + }) + .build(); + let receipt = + test_runner.execute_manifest_ignoring_fee(manifest, vec![contractor_public_key.into()]); + receipt +} + +pub fn complete_dispute_as_admin( + test_runner: &mut TestRunner, + service: &MoonWorkService, + dispute_id: NonFungibleId, + dispute_side: DisputeSide, +) -> radix_engine::transaction::TransactionReceipt { + let manifest = ManifestBuilder::new(&NetworkDefinition::simulator()) + .create_proof_from_account( + service.moon_work_resources.service_admin_badge, + service.service_admin_component_address, + ) + .call_method( + service.work_components.dispute_component, + "complete_dispute_as_admin", + args!(dispute_id, dispute_side), + ) + .drop_all_proofs() + .build(); + let receipt = test_runner + .execute_manifest_ignoring_fee(manifest, vec![service.service_admin_public_key.into()]); + receipt +} + +pub fn remove_work( + test_runner: &mut TestRunner, + service: &MoonWorkService, + account_component: ComponentAddress, + public_key: EcdsaSecp256k1PublicKey, + work_id: NonFungibleId, +) -> radix_engine::transaction::TransactionReceipt { + let manifest = ManifestBuilder::new(&NetworkDefinition::simulator()) + .create_proof_from_account_by_ids( + &BTreeSet::from_iter([work_id]), + service.work_components.work_resource, + account_component, + ) + .pop_from_auth_zone(|builder, work| { + builder + .call_method( + service.work_components.work_component, + "remove_work", + args!(Proof(work)), + ) + .drop_all_proofs() + }) + .build(); + let receipt = test_runner.execute_manifest_ignoring_fee(manifest, vec![public_key.into()]); + receipt +} + +pub fn complete_dispute( + test_runner: &mut TestRunner, + service: &MoonWorkService, + account: ComponentAddress, + public_key: EcdsaSecp256k1PublicKey, + dispute_id: NonFungibleId, +) -> radix_engine::transaction::TransactionReceipt { + let manifest = ManifestBuilder::new(&NetworkDefinition::simulator()) + .create_proof_from_account(service.moon_work_resources.client_badge, account) + .pop_from_auth_zone(|builder, proof| { + builder + .call_method( + service.work_components.dispute_component, + "complete_dispute", + args!(dispute_id, Proof(proof)), + ) + .drop_all_proofs() + }) + .build(); + let receipt = test_runner.execute_manifest_ignoring_fee(manifest, vec![public_key.into()]); + receipt +} + +pub fn join_and_decide_dispute( + test_runner: &mut TestRunner, + account: ComponentAddress, + public_key: EcdsaSecp256k1PublicKey, + badge: ResourceAddress, + service: &MoonWorkService, + dispute_id: NonFungibleId, + dispute_side: DisputeSide, +) -> TransactionReceipt { + let manifest = ManifestBuilder::new(&NetworkDefinition::simulator()) + .create_proof_from_account(badge, account) + .pop_from_auth_zone(|builder, proof| { + builder + .call_method( + service.work_components.dispute_component, + "join_and_decide_dispute", + args!(dispute_id, dispute_side, Proof(proof)), + ) + .drop_all_proofs() + }) + .build(); + + test_runner.execute_manifest_ignoring_fee(manifest, vec![public_key.into()]) +} + +pub struct MoonWorkService { + pub service_admin_public_key: EcdsaSecp256k1PublicKey, + pub service_admin_component_address: ComponentAddress, + pub moon_work_resources: MoonWorkResource, + pub moon_work_component: ComponentAddress, + pub work_components: WorkComponents, +} + +pub fn create_moon_work_service_with_work_category( + test_runner: &mut TestRunner, +) -> MoonWorkService { + let (service_admin_public_key, _private_key, service_admin_component_address) = + test_runner.new_account(); + let (moonwork_component, moonwork_resources) = create_moonwork_service( + test_runner, + service_admin_component_address, + service_admin_public_key, + ); + register_as_client( + test_runner, + moonwork_component, + service_admin_component_address, + service_admin_public_key, + "admin", + ); + let create_new_work_receipt = create_new_category( + test_runner, + moonwork_resources.service_admin_badge, + service_admin_component_address, + service_admin_public_key, + moonwork_component, + ); + let work_components = get_work_components(create_new_work_receipt.expect_commit()); + + MoonWorkService { + service_admin_public_key, + service_admin_component_address, + moon_work_resources: moonwork_resources, + moon_work_component: moonwork_component, + work_components, + } +} + +pub fn create_work_to_finish_work( + test_runner: &mut TestRunner, + client_account: ComponentAddress, + contractor_account: ComponentAddress, + contractor_username: &str, + moon_work_resources: &MoonWorkResource, + work_components: &WorkComponents, + client_public_key: EcdsaSecp256k1PublicKey, + contractor_public_key: EcdsaSecp256k1PublicKey, + id: u64, +) { + create_new_work( + test_runner, + client_account, + client_public_key, + moon_work_resources.client_badge, + work_components, + ); + request_work( + test_runner, + contractor_account, + contractor_public_key, + moon_work_resources.contractor_badge, + work_components.work_component, + NonFungibleId::from_u64(id), + ); + accept_contractor_for_work( + test_runner, + work_components.work_component, + moon_work_resources.client_badge, + client_account, + client_public_key, + work_components.work_resource, + NonFungibleId::from_bytes(contractor_username.as_bytes().to_vec()), + NonFungibleId::from_u64(id), + ); + finish_work( + test_runner, + moon_work_resources, + client_account, + client_public_key, + contractor_account, + contractor_public_key, + NonFungibleId::from_u64(id), + work_components, + ); +} + +pub fn submit_document( + test_runner: &mut TestRunner, + badge: ResourceAddress, + account: ComponentAddress, + public_key: EcdsaSecp256k1PublicKey, + work_components: &WorkComponents, + dispute_id: NonFungibleId, + dispute_document_title: &str, + dispute_document_url: &str, +) -> TransactionReceipt { + let manifest = ManifestBuilder::new(&NetworkDefinition::simulator()) + .create_proof_from_account(badge, account) + .pop_from_auth_zone(|builder, client_proof| { + builder + .call_method( + work_components.dispute_component, + "submit_document", + args!( + dispute_id, + dispute_document_title, + dispute_document_url, + Proof(client_proof) + ), + ) + .drop_all_proofs() + }) + .build(); + + test_runner.execute_manifest_ignoring_fee(manifest, vec![public_key.into()]) +} + +pub fn create_new_dispute( + test_runner: &mut TestRunner, + badge: ResourceAddress, + account: ComponentAddress, + public_key: EcdsaSecp256k1PublicKey, + work_components: &WorkComponents, + work_id: NonFungibleId, +) -> TransactionReceipt { + let manifest = ManifestBuilder::new(&NetworkDefinition::simulator()) + .create_proof_from_account(badge, account) + .pop_from_auth_zone(|builder, client_proof| { + builder + .call_method( + work_components.dispute_component, + "create_new_dispute", + args!(work_id, work_components.work_resource, Proof(client_proof)), + ) + .drop_all_proofs() + }) + .build(); + + test_runner.execute_manifest_ignoring_fee(manifest, vec![public_key.into()]) +} + +pub fn claim_contractor_compensation( + test_runner: &mut TestRunner, + resources: &MoonWorkResource, + moonwork_component: ComponentAddress, + contractor_account: ComponentAddress, + contractor_public_key: EcdsaSecp256k1PublicKey, +) -> TransactionReceipt { + let manifest = ManifestBuilder::new(&NetworkDefinition::simulator()) + .create_proof_from_account(resources.contractor_badge, contractor_account) + .pop_from_auth_zone(|builder, contractor_proof| { + builder + .call_method( + moonwork_component, + "claim_contractor_compensation", + args!(Proof(contractor_proof)), + ) + .call_method( + contractor_account, + "deposit_batch", + args!(Expression::entire_worktop()), + ) + }) + .build(); + test_runner.execute_manifest_ignoring_fee(manifest, vec![contractor_public_key.into()]) +} + +pub fn finish_work( + test_runner: &mut TestRunner, + resources: &MoonWorkResource, + client_account: ComponentAddress, + client_public_key: EcdsaSecp256k1PublicKey, + contractor_account: ComponentAddress, + contractor_public_key: EcdsaSecp256k1PublicKey, + work_id: NonFungibleId, + work_components: &WorkComponents, +) -> TransactionReceipt { + let manifest = ManifestBuilder::new(&NetworkDefinition::simulator()) + .create_proof_from_account(resources.client_badge, client_account) + .pop_from_auth_zone(|builder, client_proof| { + builder + .create_proof_from_account(resources.contractor_badge, contractor_account) + .pop_from_auth_zone(|builder, contractor_proof| { + builder + .create_proof_from_account_by_ids( + &BTreeSet::from_iter([work_id]), + work_components.work_resource, + client_account, + ) + .pop_from_auth_zone(|builder, work_proof| { + builder.call_method( + work_components.work_component, + "finish_work", + args!( + Proof(work_proof), + Proof(client_proof), + Proof(contractor_proof) + ), + ) + }) + }) + .drop_all_proofs() + }) + .build(); + + test_runner.execute_manifest_ignoring_fee( + manifest, + vec![client_public_key.into(), contractor_public_key.into()], + ) +} + +pub fn accept_contractor_for_work( + test_runner: &mut TestRunner, + work_component: ComponentAddress, + client_resource: ResourceAddress, + client_account: ComponentAddress, + public_key: EcdsaSecp256k1PublicKey, + work_resource: ResourceAddress, + contractor: NonFungibleId, + work_id: NonFungibleId, +) -> TransactionReceipt { + let manifest = ManifestBuilder::new(&NetworkDefinition::simulator()) + .create_proof_from_account(client_resource, client_account) + .pop_from_auth_zone(|builder, client_proof| { + builder + .create_proof_from_account_by_ids( + &BTreeSet::from_iter([work_id]), + work_resource, + client_account, + ) + .pop_from_auth_zone(|builder, work_id| { + builder.call_method( + work_component, + "accept_contractor_for_work", + args!(Proof(work_id), contractor, Proof(client_proof)), + ) + }) + .drop_all_proofs() + }) + .build(); + test_runner.execute_manifest_ignoring_fee(manifest, vec![public_key.into()]) +} + +pub fn request_work( + test_runner: &mut TestRunner, + account_component: ComponentAddress, + public_key: EcdsaSecp256k1PublicKey, + contractor_resource: ResourceAddress, + work_component: ComponentAddress, + work_id: NonFungibleId, +) -> TransactionReceipt { + let manifest = ManifestBuilder::new(&NetworkDefinition::simulator()) + .withdraw_from_account_by_amount(dec!(1), RADIX_TOKEN, account_component) + .create_proof_from_account(contractor_resource, account_component) + .pop_from_auth_zone(|builder, proof_id| { + builder + .call_method( + work_component, + "request_work", + args!(work_id, Proof(proof_id)), + ) + .call_method( + account_component, + "deposit_batch", + args!(Expression::entire_worktop()), + ) + }) + .build(); + test_runner.execute_manifest_ignoring_fee(manifest, vec![public_key.into()]) +} + +pub fn create_new_work( + test_runner: &mut TestRunner, + account_component: ComponentAddress, + public_key: EcdsaSecp256k1PublicKey, + client_resource: ResourceAddress, + work_components: &WorkComponents, +) -> TransactionReceipt { + let manifest = ManifestBuilder::new(&NetworkDefinition::simulator()) + .withdraw_from_account_by_amount(dec!(1), RADIX_TOKEN, account_component) + .create_proof_from_account(client_resource, account_component) + .pop_from_auth_zone(|builder, proof_id| { + builder + .take_from_worktop(RADIX_TOKEN, |builder, bucket_id| { + builder.call_method( + work_components.work_component, + "create_new_work", + args!( + dec!(1), + "Develop a dex", + "Create a multi pair decentralised exchange.", + vec!["rustlang", "scrypto"], + Bucket(bucket_id), + Proof(proof_id) + ), + ) + }) + .call_method( + account_component, + "deposit_batch", + args!(Expression::entire_worktop()), + ) + }) + .build(); + test_runner.execute_manifest_ignoring_fee(manifest, vec![public_key.into()]) +} + +pub fn create_new_category( + test_runner: &mut TestRunner, + service_admin_badge: ResourceAddress, + account_component: ComponentAddress, + public_key: EcdsaSecp256k1PublicKey, + component: ComponentAddress, +) -> TransactionReceipt { + let manifest = ManifestBuilder::new(&NetworkDefinition::simulator()) + .create_proof_from_account(service_admin_badge, account_component) + .call_method(component, "create_new_category", args!("Development & IT")) + .drop_all_proofs() + .build(); + + test_runner.execute_manifest_ignoring_fee(manifest, vec![public_key.into()]) +} + +pub struct WorkComponents { + pub work_component: ComponentAddress, + pub work_resource: ResourceAddress, + pub dispute_component: ComponentAddress, + pub dispute_resource: ResourceAddress, + pub dispute_document_resource: ResourceAddress, +} + +pub fn get_work_components(result: &CommitResult) -> WorkComponents { + let work_component = result.entity_changes.new_component_addresses[0]; + let dispute_component = result.entity_changes.new_component_addresses[1]; + let work_resource = result.entity_changes.new_resource_addresses[0]; + let dispute_resource = result.entity_changes.new_resource_addresses[1]; + let dispute_document_resource = result.entity_changes.new_resource_addresses[2]; + + WorkComponents { + work_component, + work_resource, + dispute_component, + dispute_resource, + dispute_document_resource, + } +} + +pub fn get_account_balance( + test_runner: &mut TestRunner, + account_component: ComponentAddress, + public_key: EcdsaSecp256k1PublicKey, + resource: ResourceAddress, +) -> Decimal { + let manifest = ManifestBuilder::new(&NetworkDefinition::simulator()) + .call_method(account_component, "balance", args!(resource)) + .call_method( + account_component, + "deposit_batch", + args!(Expression::entire_worktop()), + ) + .build(); + let receipt = test_runner.execute_manifest_ignoring_fee(manifest, vec![public_key.into()]); + + receipt.output::(1) +} + +pub struct MoonWorkResource { + pub service_admin_badge: ResourceAddress, + pub contractor_badge: ResourceAddress, + pub client_badge: ResourceAddress, + pub completed_work_resource: ResourceAddress, + pub disputed_outcome_resource: ResourceAddress, + pub contractor_accolade_resource: ResourceAddress, +} + +pub fn get_moonwork_resources(commit_result: &CommitResult) -> MoonWorkResource { + let service_admin_badge = commit_result.entity_changes.new_resource_addresses[1]; + let contractor_badge = commit_result.entity_changes.new_resource_addresses[2]; + let client_badge = commit_result.entity_changes.new_resource_addresses[3]; + let completed_work_resource = commit_result.entity_changes.new_resource_addresses[4]; + let disputed_outcome_resource = commit_result.entity_changes.new_resource_addresses[5]; + let contractor_accolade_resource = commit_result.entity_changes.new_resource_addresses[6]; + + MoonWorkResource { + service_admin_badge, + contractor_badge, + client_badge, + completed_work_resource, + disputed_outcome_resource, + contractor_accolade_resource, + } +} + +pub fn register_as_contractor( + test_runner: &mut TestRunner, + component: ComponentAddress, + account_component: ComponentAddress, + public_key: EcdsaSecp256k1PublicKey, + username: &str, +) -> TransactionReceipt { + let manifest = ManifestBuilder::new(&NetworkDefinition::simulator()) + .call_method(component, "register_as_contractor", args!(username)) + .call_method( + account_component, + "deposit_batch", + args!(Expression::entire_worktop()), + ) + .build(); + test_runner.execute_manifest_ignoring_fee(manifest, vec![public_key.into()]) +} + +pub fn register_as_client( + test_runner: &mut TestRunner, + component: ComponentAddress, + account_component: ComponentAddress, + public_key: EcdsaSecp256k1PublicKey, + username: &str, +) -> TransactionReceipt { + let manifest = ManifestBuilder::new(&NetworkDefinition::simulator()) + .call_method(component, "register_as_client", args!(username)) + .call_method( + account_component, + "deposit_batch", + args!(Expression::entire_worktop()), + ) + .build(); + test_runner.execute_manifest_ignoring_fee(manifest, vec![public_key.into()]) +} + +pub fn create_moonwork_service<'a>( + test_runner: &'a mut TestRunner, + account_component: ComponentAddress, + public_key: EcdsaSecp256k1PublicKey, +) -> (ComponentAddress, MoonWorkResource) { + let package_address = test_runner.compile_and_publish(this_package!()); + let service_fee = dec!(1); + let minimum_work_payment = dec!(1); + let dispute_participant_criteria = ParticipantCriteria { + participant_limit: 1, + contractor: ContractorCriteria { jobs_completed: 1 }, + client: ClientCriteria { jobs_paid_out: 1 }, + }; + + let manifest = ManifestBuilder::new(&NetworkDefinition::simulator()) + .call_function( + package_address, + "MoonWorkService", + "create", + args!( + service_fee, + minimum_work_payment, + dispute_participant_criteria + ), + ) + .call_method( + account_component, + "deposit_batch", + args!(Expression::entire_worktop()), + ) + .build(); + let service_receipt = + test_runner.execute_manifest_ignoring_fee(manifest, vec![public_key.into()]); + + service_receipt.expect_commit_success(); + + let resources = get_moonwork_resources(service_receipt.expect_commit()); + let service_component = service_receipt + .expect_commit() + .entity_changes + .new_component_addresses[0]; + (service_component, resources) +} + +pub fn get_non_fungible_data( + store: &TypedInMemorySubstateStore, + resource: ResourceAddress, + id: NonFungibleId, +) -> T { + let nft_wrapper: Option = store + .get_substate(&SubstateId::NonFungible(resource, id)) + .map(|s| s.substate) + .map(|s| s.into()); + let nft = nft_wrapper.unwrap().0.unwrap(); + // there is currently a bug in the NonFungibleData decode method where whats supposed to be an + // empty struct `Struct()` is decoding as `Struct(Vec(), Vec(), Map(), Map())` + // So this is a workaround to make things work for now + let mutable_data = match T::mutable_data_schema().matches(&Value::Struct { fields: vec![] }) { + true => { + ScryptoValue::from_value(Value::Struct { fields: vec![] }) + .unwrap() + .raw + } + false => nft.mutable_data(), + }; + let immutable_data = match T::immutable_data_schema().matches(&Value::Struct { fields: vec![] }) + { + true => { + ScryptoValue::from_value(Value::Struct { fields: vec![] }) + .unwrap() + .raw + } + false => nft.immutable_data(), + }; + T::decode(&immutable_data, &mutable_data).unwrap() +} + +pub fn does_non_fungible_exist( + store: &TypedInMemorySubstateStore, + resource: ResourceAddress, + id: NonFungibleId, +) -> bool { + let nft_wrapper: Option = store + .get_substate(&SubstateId::NonFungible(resource, id)) + .map(|s| s.substate) + .map(|s| s.into()); + + let unwrapped_substate = nft_wrapper.unwrap().0; + + match unwrapped_substate { + Some(_) => true, + None => false, + } +} + +pub fn get_total_supply( + store: &TypedInMemorySubstateStore, + resource_address: ResourceAddress, +) -> Decimal { + let resource_manager: ResourceManager = store + .get_substate(&SubstateId::ResourceManager(resource_address)) + .map(|s| s.substate) + .map(|s| s.into()) + .unwrap(); + + resource_manager.total_supply() +} + +pub fn assert_error_message( + receipt: &radix_engine::transaction::TransactionReceipt, + error_message: &str, +) { + let is_error_found = receipt + .execution + .application_logs + .iter() + .find(|(_, message)| message.contains(error_message)); + + assert!(is_error_found.is_some()); +} diff --git a/6-nfts-for-financial-applications/moonwork/transactions/1_create_moon_work_service.sh b/6-nfts-for-financial-applications/moonwork/transactions/1_create_moon_work_service.sh new file mode 100755 index 000000000..6efe60096 --- /dev/null +++ b/6-nfts-for-financial-applications/moonwork/transactions/1_create_moon_work_service.sh @@ -0,0 +1,43 @@ +#!/bin/bash + +export radix=resource_sim1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqzqu57yag + +# Create 4 accounts to test on +export op1=$(resim new-account) +export priv_key1=$(echo $op1 | sed -nr "s/Private key: ([[:alnum:]_]+)/\1/p") +export address1=$(echo $op1 | sed -nr "s/Account component address: ([[:alnum:]_]+)/\1/p") + +export op2=$(resim new-account) +export priv_key2=$(echo $op2 | sed -nr "s/Private key: ([[:alnum:]_]+)/\1/p") +export address2=$(echo $op2 | sed -nr "s/Account component address: ([[:alnum:]_]+)/\1/p") + +export op3=$(resim new-account) +export priv_key3=$(echo $op3 | sed -nr "s/Private key: ([[:alnum:]_]+)/\1/p") +export address3=$(echo $op3 | sed -nr "s/Account component address: ([[:alnum:]_]+)/\1/p") + +export op4=$(resim new-account) +export priv_key4=$(echo $op4 | sed -nr "s/Private key: ([[:alnum:]_]+)/\1/p") +export address4=$(echo $op4 | sed -nr "s/Account component address: ([[:alnum:]_]+)/\1/p") + +# Publish package address +export op1=$(resim publish .) +export package=$(echo $op1 | sed -nr "s/Success! New Package: ([[:alnum:]_]+)/\1/p") + +# Create Moon Work Service that has a fee of 5% +# Dispute requirements +# 1 participants allowed on each side (contractor/client) - 1 job completed by contractor - 1 job paid by client +export op2=$(resim run ./transactions/create_moonwork.rtm) +# Save the Moon Work component address +export moon_work_component=$(echo $op2 | sed -nr "s/.*Component: ([[:alnum:]_]+)/\1/p") +# Save the service admin badge +export service_admin_badge=$(echo $op2 | sed -nr "s/.*Resource: ([[:alnum:]_]+)/\1/p" | sed '2q;d') +# Save the contractor badge resource address +export contractor_badge=$(echo $op2 | sed -nr "s/.*Resource: ([[:alnum:]_]+)/\1/p" | sed '3q;d') +# Save the client badge resource address +export client_badge=$(echo $op2 | sed -nr "s/.*Resource: ([[:alnum:]_]+)/\1/p" | sed '4q;d') +# Save the work badge resource address +export completed_work_resource=$(echo $op2 | sed -nr "s/.*Resource: ([[:alnum:]_]+)/\1/p" | sed '5q;d') +# Save the DisputedOutcome NFT resource +export dispute_outcome_resource=$(echo $op2 | sed -nr "s/.*Resource: ([[:alnum:]_]+)/\1/p" | sed '6q;d') +# Save the ContractorAccolades NFT resource +export contractor_accolade_resource=$(echo $op2 | sed -nr "s/.*Resource: ([[:alnum:]_]+)/\1/p" | sed '7q;d') diff --git a/6-nfts-for-financial-applications/moonwork/transactions/2_register_contractors.sh b/6-nfts-for-financial-applications/moonwork/transactions/2_register_contractors.sh new file mode 100755 index 000000000..237abc7f8 --- /dev/null +++ b/6-nfts-for-financial-applications/moonwork/transactions/2_register_contractors.sh @@ -0,0 +1,15 @@ +#!/bin/bash + +# Create a contractor named beemdpv +resim set-default-account $address1 $priv_key1 +export contractor=beemdvp +export account=$address1 +resim run ./transactions/register_as_contractor.rtm + +# create another contractor named wylie +resim set-default-account $address3 $priv_key3 +export contractor=wylie +export account=$address3 +resim run ./transactions/register_as_contractor.rtm + +resim set-default-account $address1 $priv_key1 diff --git a/6-nfts-for-financial-applications/moonwork/transactions/3_register_client.sh b/6-nfts-for-financial-applications/moonwork/transactions/3_register_client.sh new file mode 100644 index 000000000..886a8f5c7 --- /dev/null +++ b/6-nfts-for-financial-applications/moonwork/transactions/3_register_client.sh @@ -0,0 +1,13 @@ +#!/bin/bash + +resim set-default-account $address2 $priv_key2 +export account=$address2 +export client=slys +resim run ./transactions/register_as_client.rtm + +resim set-default-account $address4 $priv_key4 +export account=$address4 +export client=dan +resim run ./transactions/register_as_client.rtm + +resim set-default-account $address1 $priv_key1 diff --git a/6-nfts-for-financial-applications/moonwork/transactions/4_create_work_categories.sh b/6-nfts-for-financial-applications/moonwork/transactions/4_create_work_categories.sh new file mode 100644 index 000000000..ce25e5bdf --- /dev/null +++ b/6-nfts-for-financial-applications/moonwork/transactions/4_create_work_categories.sh @@ -0,0 +1,34 @@ +#!/bin/bash + +resim set-default-account $address1 $priv_key1 +export account=$address1 + +export category="Development & IT" +export op1=$(resim run ./transactions/create_work_category.rtm) +export development_it_component=$(echo $op1 | sed -nr "s/.*Component: ([[:alnum:]_]+)/\1/p" | sed '1q;d') +export development_it_dispute_component=$(echo $op1 | sed -nr "s/.*Component: ([[:alnum:]_]+)/\1/p" | sed '2q;d') +export development_it_resource=$(echo $op1 | sed -nr "s/.*Resource: ([[:alnum:]_]+)/\1/p" | sed '1q;d') + +export category="Accounting & Finance" +export op1=$(resim run ./transactions/create_work_category.rtm) +export accounting_and_finance_component=$(echo $op1 | sed -nr "s/.*Component: ([[:alnum:]_]+)/\1/p" | sed '1q;d') +export accounting_and_finance_dispute_component=$(echo $op1 | sed -nr "s/.*Component: ([[:alnum:]_]+)/\1/p" | sed '2q;d') +export accounting_and_finance_resource=$(echo $op1 | sed -nr "s/.*Resource: ([[:alnum:]_]+)/\1/p" | sed '1q;d') + +export category="Digital Marketing" +export op1=$(resim run ./transactions/create_work_category.rtm) +export digital_marketing_component=$(echo $op1 | sed -nr "s/.*Component: ([[:alnum:]_]+)/\1/p" | sed '1q;d') +export digital_marketing_dispute_component=$(echo $op1 | sed -nr "s/.*Component: ([[:alnum:]_]+)/\1/p" | sed '2q;d') +export digital_marketing_resource=$(echo $op1 | sed -nr "s/.*Resource: ([[:alnum:]_]+)/\1/p" | sed '1q;d') + +export category="Graphics Design" +export op1=$(resim run ./transactions/create_work_category.rtm) +export graphics_design_component=$(echo $op1 | sed -nr "s/.*Component: ([[:alnum:]_]+)/\1/p" | sed '1q;d') +export graphics_design_dispute_component=$(echo $op1 | sed -nr "s/.*Component: ([[:alnum:]_]+)/\1/p" | sed '2q;d') +export graphics_design_resource=$(echo $op1 | sed -nr "s/.*Resource: ([[:alnum:]_]+)/\1/p" | sed '1q;d') + +export category="Disputed Category" +export op1=$(resim run ./transactions/create_work_category.rtm) +export disputed_work_component=$(echo $op1 | sed -nr "s/.*Component: ([[:alnum:]_]+)/\1/p" | sed '1q;d') +export disputed_work_dispute_component=$(echo $op1 | sed -nr "s/.*Component: ([[:alnum:]_]+)/\1/p" | sed '2q;d') +export disputed_work_resource=$(echo $op1 | sed -nr "s/.*Resource: ([[:alnum:]_]+)/\1/p" | sed '1q;d') diff --git a/6-nfts-for-financial-applications/moonwork/transactions/5_create_new_job.sh b/6-nfts-for-financial-applications/moonwork/transactions/5_create_new_job.sh new file mode 100644 index 000000000..f8d58939a --- /dev/null +++ b/6-nfts-for-financial-applications/moonwork/transactions/5_create_new_job.sh @@ -0,0 +1,79 @@ +#!/bin/bash + +resim set-default-account $address2 $priv_key2 +export account=$address2 + +export work_component=$development_it_component +export total_compensation='1' +export work_title='Dex' +export work_description='Create a DEX in Scrypto' +export skills_required='"scrypto", "rustlang"' +for i in {1..5}; do resim run ./transactions/create_new_work.rtm; done + +export work_component=$accounting_and_finance_component +export total_compensation='1' +export work_title='Yearly personal tax' +export work_description='Need my taxes done for end of year' +export skills_required='"accounting", "personal tax"' +for i in {1..5}; do resim run ./transactions/create_new_work.rtm; done + +export work_component=$digital_marketing_component +export total_compensation='1' +export work_title='Socials Marketing' +export work_description='Grow twitter from 195k to 500k' +export skills_required='"twitter", "social engagement"' +for i in {1..5}; do resim run ./transactions/create_new_work.rtm; done + +export work_component=$graphics_design_component +export total_compensation='1' +export work_title='DEX Logo' +export work_description='Create logo for dex' +export skills_required='"logo design", "creativity"' +for i in {1..5}; do resim run ./transactions/create_new_work.rtm; done + +export work_component=$disputed_work_component +export total_compensation='1' +export work_title='Example title to dispute' +export work_description='Example description of disputed work' +export skills_required='"logo design", "creativity"' +for i in {1..5}; do resim run ./transactions/create_new_work.rtm; done + +resim set-default-account $address4 $priv_key4 +export account=$address4 + +export work_component=$development_it_component +export total_compensation='1' +export work_title='Dex' +export work_description='Create a DEX in Scrypto' +export skills_required='"scrypto", "rustlang"' +for i in {1..5}; do resim run ./transactions/create_new_work.rtm; done + +export work_component=$accounting_and_finance_component +export total_compensation='1' +export work_title='Yearly personal tax' +export work_description='Need my taxes done for end of year' +export skills_required='"accounting", "personal tax"' +for i in {1..5}; do resim run ./transactions/create_new_work.rtm; done + +export work_component=$digital_marketing_component +export total_compensation='1' +export work_title='Socials Marketing' +export work_description='Grow twitter from 195k to 500k' +export skills_required='"twitter", "social engagement"' +for i in {1..5}; do resim run ./transactions/create_new_work.rtm; done + +export work_component=$graphics_design_component +export total_compensation='1' +export work_title='DEX Logo' +export work_description='Create logo for dex' +export skills_required='"logo design", "creativity"' +for i in {1..5}; do resim run ./transactions/create_new_work.rtm; done + +export work_component=$disputed_work_component +export total_compensation='1' +export work_title='Example title to dispute' +export work_description='Example description of disputed work' +export skills_required='"logo design", "creativity"' +for i in {1..5}; do resim run ./transactions/create_new_work.rtm; done + +resim set-default-account $address1 $priv_key1 diff --git a/6-nfts-for-financial-applications/moonwork/transactions/6_request_work.sh b/6-nfts-for-financial-applications/moonwork/transactions/6_request_work.sh new file mode 100644 index 000000000..40c47c129 --- /dev/null +++ b/6-nfts-for-financial-applications/moonwork/transactions/6_request_work.sh @@ -0,0 +1,182 @@ +#!/bin/bash + +resim set-default-account $address1 $priv_key1 + +export account=$address1 +export work_component=$development_it_component + +export job_id=0a0100000000000000 +resim run ./transactions/request_work.rtm + +export job_id=0a0200000000000000 +resim run ./transactions/request_work.rtm + +export job_id=0a0300000000000000 +resim run ./transactions/request_work.rtm + +export job_id=0a0400000000000000 +resim run ./transactions/request_work.rtm + +export job_id=0a0500000000000000 +resim run ./transactions/request_work.rtm + +export work_component=$accounting_and_finance_component +export account=$address1 + +export job_id=0a0100000000000000 +resim run ./transactions/request_work.rtm + +export job_id=0a0200000000000000 +resim run ./transactions/request_work.rtm + +export job_id=0a0300000000000000 +resim run ./transactions/request_work.rtm + +export job_id=0a0400000000000000 +resim run ./transactions/request_work.rtm + +export job_id=0a0500000000000000 +resim run ./transactions/request_work.rtm + +export work_component=$digital_marketing_component +export account=$address1 + +export job_id=0a0100000000000000 +resim run ./transactions/request_work.rtm + +export job_id=0a0200000000000000 +resim run ./transactions/request_work.rtm + +export job_id=0a0300000000000000 +resim run ./transactions/request_work.rtm + +export job_id=0a0400000000000000 +resim run ./transactions/request_work.rtm + +export job_id=0a0500000000000000 +resim run ./transactions/request_work.rtm + +export work_component=$graphics_design_component +export account=$address1 + +export job_id=0a0100000000000000 +resim run ./transactions/request_work.rtm + +export job_id=0a0200000000000000 +resim run ./transactions/request_work.rtm + +export job_id=0a0300000000000000 +resim run ./transactions/request_work.rtm + +export job_id=0a0400000000000000 +resim run ./transactions/request_work.rtm + +export job_id=0a0500000000000000 +resim run ./transactions/request_work.rtm + +export work_component=$disputed_work_component +export account=$address1 + +export job_id=0a0100000000000000 +resim run ./transactions/request_work.rtm + +export job_id=0a0200000000000000 +resim run ./transactions/request_work.rtm + +export job_id=0a0300000000000000 +resim run ./transactions/request_work.rtm + +export job_id=0a0400000000000000 +resim run ./transactions/request_work.rtm + +export job_id=0a0500000000000000 +resim run ./transactions/request_work.rtm + +resim set-default-account $address3 $priv_key3 +export account=$address3 + +export work_component=$development_it_component + +export job_id=0a0600000000000000 +resim run ./transactions/request_work.rtm + +export job_id=0a0700000000000000 +resim run ./transactions/request_work.rtm + +export job_id=0a0800000000000000 +resim run ./transactions/request_work.rtm + +export job_id=0a0900000000000000 +resim run ./transactions/request_work.rtm + +export job_id=0a0a00000000000000 +resim run ./transactions/request_work.rtm + +export work_component=$accounting_and_finance_component + +export job_id=0a0600000000000000 +resim run ./transactions/request_work.rtm + +export job_id=0a0700000000000000 +resim run ./transactions/request_work.rtm + +export job_id=0a0800000000000000 +resim run ./transactions/request_work.rtm + +export job_id=0a0900000000000000 +resim run ./transactions/request_work.rtm + +export job_id=0a0a00000000000000 +resim run ./transactions/request_work.rtm + +export work_component=$digital_marketing_component + +export job_id=0a0600000000000000 +resim run ./transactions/request_work.rtm + +export job_id=0a0700000000000000 +resim run ./transactions/request_work.rtm + +export job_id=0a0800000000000000 +resim run ./transactions/request_work.rtm + +export job_id=0a0900000000000000 +resim run ./transactions/request_work.rtm + +export job_id=0a0a00000000000000 +resim run ./transactions/request_work.rtm + +export work_component=$graphics_design_component + +export job_id=0a0600000000000000 +resim run ./transactions/request_work.rtm + +export job_id=0a0700000000000000 +resim run ./transactions/request_work.rtm + +export job_id=0a0800000000000000 +resim run ./transactions/request_work.rtm + +export job_id=0a0900000000000000 +resim run ./transactions/request_work.rtm + +export job_id=0a0a00000000000000 +resim run ./transactions/request_work.rtm + +export work_component=$disputed_work_component + +export job_id=0a0600000000000000 +resim run ./transactions/request_work.rtm + +export job_id=0a0700000000000000 +resim run ./transactions/request_work.rtm + +export job_id=0a0800000000000000 +resim run ./transactions/request_work.rtm + +export job_id=0a0900000000000000 +resim run ./transactions/request_work.rtm + +export job_id=0a0a00000000000000 +resim run ./transactions/request_work.rtm + diff --git a/6-nfts-for-financial-applications/moonwork/transactions/7_accept_contractor_for_work.sh b/6-nfts-for-financial-applications/moonwork/transactions/7_accept_contractor_for_work.sh new file mode 100644 index 000000000..7d921ac68 --- /dev/null +++ b/6-nfts-for-financial-applications/moonwork/transactions/7_accept_contractor_for_work.sh @@ -0,0 +1,193 @@ +#!/bin/bash + +resim set-default-account $address2 $priv_key2 +export account=$address2 + +export contractor_id=3007070000006265656d647670 + +export work_component=$development_it_component +export work_resource=$development_it_resource + +export job_id=0a0100000000000000 +resim run ./transactions/accept_contractor_for_work.rtm + +export job_id=0a0200000000000000 +resim run ./transactions/accept_contractor_for_work.rtm + +export job_id=0a0300000000000000 +resim run ./transactions/accept_contractor_for_work.rtm + +export job_id=0a0400000000000000 +resim run ./transactions/accept_contractor_for_work.rtm + +export job_id=0a0500000000000000 +resim run ./transactions/accept_contractor_for_work.rtm + +export work_component=$accounting_and_finance_component +export work_resource=$accounting_and_finance_resource + +export job_id=0a0100000000000000 +resim run ./transactions/accept_contractor_for_work.rtm + +export job_id=0a0200000000000000 +resim run ./transactions/accept_contractor_for_work.rtm + +export job_id=0a0300000000000000 +resim run ./transactions/accept_contractor_for_work.rtm + +export job_id=0a0400000000000000 +resim run ./transactions/accept_contractor_for_work.rtm + +export job_id=0a0500000000000000 +resim run ./transactions/accept_contractor_for_work.rtm + + +export work_component=$digital_marketing_component +export work_resource=$digital_marketing_resource + +export job_id=0a0100000000000000 +resim run ./transactions/accept_contractor_for_work.rtm + +export job_id=0a0200000000000000 +resim run ./transactions/accept_contractor_for_work.rtm + +export job_id=0a0300000000000000 +resim run ./transactions/accept_contractor_for_work.rtm + +export job_id=0a0400000000000000 +resim run ./transactions/accept_contractor_for_work.rtm + +export job_id=0a0500000000000000 +resim run ./transactions/accept_contractor_for_work.rtm + +export work_component=$graphics_design_component +export work_resource=$graphics_design_resource + +export job_id=0a0100000000000000 +resim run ./transactions/accept_contractor_for_work.rtm + +export job_id=0a0200000000000000 +resim run ./transactions/accept_contractor_for_work.rtm + +export job_id=0a0300000000000000 +resim run ./transactions/accept_contractor_for_work.rtm + +export job_id=0a0400000000000000 +resim run ./transactions/accept_contractor_for_work.rtm + +export job_id=0a0500000000000000 +resim run ./transactions/accept_contractor_for_work.rtm + +export work_component=$disputed_work_component +export work_resource=$disputed_work_resource + +export job_id=0a0100000000000000 +resim run ./transactions/accept_contractor_for_work.rtm + +export job_id=0a0200000000000000 +resim run ./transactions/accept_contractor_for_work.rtm + +export job_id=0a0300000000000000 +resim run ./transactions/accept_contractor_for_work.rtm + +export job_id=0a0400000000000000 +resim run ./transactions/accept_contractor_for_work.rtm + +export job_id=0a0500000000000000 +resim run ./transactions/accept_contractor_for_work.rtm + +resim set-default-account $address4 $priv_key4 +export account=$address4 +export contractor_id=30070500000077796c6965 + +export work_component=$development_it_component +export work_resource=$development_it_resource + +export job_id=0a0600000000000000 +resim run ./transactions/accept_contractor_for_work.rtm + +export job_id=0a0700000000000000 +resim run ./transactions/accept_contractor_for_work.rtm + +export job_id=0a0800000000000000 +resim run ./transactions/accept_contractor_for_work.rtm + +export job_id=0a0900000000000000 +resim run ./transactions/accept_contractor_for_work.rtm + +export job_id=0a0a00000000000000 +resim run ./transactions/accept_contractor_for_work.rtm + +export work_component=$accounting_and_finance_component +export work_resource=$accounting_and_finance_resource + +export job_id=0a0600000000000000 +resim run ./transactions/accept_contractor_for_work.rtm + +export job_id=0a0700000000000000 +resim run ./transactions/accept_contractor_for_work.rtm + +export job_id=0a0800000000000000 +resim run ./transactions/accept_contractor_for_work.rtm + +export job_id=0a0900000000000000 +resim run ./transactions/accept_contractor_for_work.rtm + +export job_id=0a0a00000000000000 +resim run ./transactions/accept_contractor_for_work.rtm + +export work_component=$digital_marketing_component +export work_resource=$digital_marketing_resource + +export job_id=0a0600000000000000 +resim run ./transactions/accept_contractor_for_work.rtm + +export job_id=0a0700000000000000 +resim run ./transactions/accept_contractor_for_work.rtm + +export job_id=0a0800000000000000 +resim run ./transactions/accept_contractor_for_work.rtm + +export job_id=0a0900000000000000 +resim run ./transactions/accept_contractor_for_work.rtm + +export job_id=0a0a00000000000000 +resim run ./transactions/accept_contractor_for_work.rtm + +export work_component=$graphics_design_component +export work_resource=$graphics_design_resource + +export job_id=0a0600000000000000 +resim run ./transactions/accept_contractor_for_work.rtm + +export job_id=0a0700000000000000 +resim run ./transactions/accept_contractor_for_work.rtm + +export job_id=0a0800000000000000 +resim run ./transactions/accept_contractor_for_work.rtm + +export job_id=0a0900000000000000 +resim run ./transactions/accept_contractor_for_work.rtm + +export job_id=0a0a00000000000000 +resim run ./transactions/accept_contractor_for_work.rtm + +export work_component=$disputed_work_component +export work_resource=$disputed_work_resource + +export job_id=0a0600000000000000 +resim run ./transactions/accept_contractor_for_work.rtm + +export job_id=0a0700000000000000 +resim run ./transactions/accept_contractor_for_work.rtm + +export job_id=0a0800000000000000 +resim run ./transactions/accept_contractor_for_work.rtm + +export job_id=0a0900000000000000 +resim run ./transactions/accept_contractor_for_work.rtm + +export job_id=0a0a00000000000000 +resim run ./transactions/accept_contractor_for_work.rtm + +resim set-default-account $address1 $priv_key1 diff --git a/6-nfts-for-financial-applications/moonwork/transactions/8_finish_work.sh b/6-nfts-for-financial-applications/moonwork/transactions/8_finish_work.sh new file mode 100644 index 000000000..a107c42a7 --- /dev/null +++ b/6-nfts-for-financial-applications/moonwork/transactions/8_finish_work.sh @@ -0,0 +1,155 @@ +#!/bin/bash + +export client=$address2 +export contractor=$address1 + +export work_component=$development_it_component +export work_resource=$development_it_resource + +export work_id=0a0100000000000000 +resim run ./transactions/finish_work.rtm --signing-keys $priv_key1,$priv_key2 + +export work_id=0a0200000000000000 +resim run ./transactions/finish_work.rtm --signing-keys $priv_key1,$priv_key2 + +export work_id=0a0300000000000000 +resim run ./transactions/finish_work.rtm --signing-keys $priv_key1,$priv_key2 + +export work_id=0a0400000000000000 +resim run ./transactions/finish_work.rtm --signing-keys $priv_key1,$priv_key2 + +export work_id=0a0500000000000000 +resim run ./transactions/finish_work.rtm --signing-keys $priv_key1,$priv_key2 + +export work_component=$accounting_and_finance_component +export work_resource=$accounting_and_finance_resource + +export work_id=0a0100000000000000 +resim run ./transactions/finish_work.rtm --signing-keys $priv_key1,$priv_key2 + +export work_id=0a0200000000000000 +resim run ./transactions/finish_work.rtm --signing-keys $priv_key1,$priv_key2 + +export work_id=0a0300000000000000 +resim run ./transactions/finish_work.rtm --signing-keys $priv_key1,$priv_key2 + +export work_id=0a0400000000000000 +resim run ./transactions/finish_work.rtm --signing-keys $priv_key1,$priv_key2 + +export work_id=0a0500000000000000 +resim run ./transactions/finish_work.rtm --signing-keys $priv_key1,$priv_key2 + + +export work_component=$digital_marketing_component +export work_resource=$digital_marketing_resource + +export work_id=0a0100000000000000 +resim run ./transactions/finish_work.rtm --signing-keys $priv_key1,$priv_key2 + +export work_id=0a0200000000000000 +resim run ./transactions/finish_work.rtm --signing-keys $priv_key1,$priv_key2 + +export work_id=0a0300000000000000 +resim run ./transactions/finish_work.rtm --signing-keys $priv_key1,$priv_key2 + +export work_id=0a0400000000000000 +resim run ./transactions/finish_work.rtm --signing-keys $priv_key1,$priv_key2 + +export work_id=0a0500000000000000 +resim run ./transactions/finish_work.rtm --signing-keys $priv_key1,$priv_key2 + + +export work_component=$graphics_design_component +export work_resource=$graphics_design_resource + +export work_id=0a0100000000000000 +resim run ./transactions/finish_work.rtm --signing-keys $priv_key1,$priv_key2 + +export work_id=0a0200000000000000 +resim run ./transactions/finish_work.rtm --signing-keys $priv_key1,$priv_key2 + +export work_id=0a0300000000000000 +resim run ./transactions/finish_work.rtm --signing-keys $priv_key1,$priv_key2 + +export work_id=0a0400000000000000 +resim run ./transactions/finish_work.rtm --signing-keys $priv_key1,$priv_key2 + +export work_id=0a0500000000000000 +resim run ./transactions/finish_work.rtm --signing-keys $priv_key1,$priv_key2 + +export client=$address4 +export contractor=$address3 + +export work_component=$development_it_component +export work_resource=$development_it_resource + +export work_id=0a0600000000000000 +resim run ./transactions/finish_work.rtm --signing-keys $priv_key3,$priv_key4 + +export work_id=0a0700000000000000 +resim run ./transactions/finish_work.rtm --signing-keys $priv_key3,$priv_key4 + +export work_id=0a0800000000000000 +resim run ./transactions/finish_work.rtm --signing-keys $priv_key3,$priv_key4 + +export work_id=0a0900000000000000 +resim run ./transactions/finish_work.rtm --signing-keys $priv_key3,$priv_key4 + +export work_id=0a0a00000000000000 +resim run ./transactions/finish_work.rtm --signing-keys $priv_key3,$priv_key4 + +export work_component=$accounting_and_finance_component +export work_resource=$accounting_and_finance_resource + +export work_id=0a0600000000000000 +resim run ./transactions/finish_work.rtm --signing-keys $priv_key3,$priv_key4 + +export work_id=0a0700000000000000 +resim run ./transactions/finish_work.rtm --signing-keys $priv_key3,$priv_key4 + +export work_id=0a0800000000000000 +resim run ./transactions/finish_work.rtm --signing-keys $priv_key3,$priv_key4 + +export work_id=0a0900000000000000 +resim run ./transactions/finish_work.rtm --signing-keys $priv_key3,$priv_key4 + +export work_id=0a0a00000000000000 +resim run ./transactions/finish_work.rtm --signing-keys $priv_key3,$priv_key4 + + +export work_component=$digital_marketing_component +export work_resource=$digital_marketing_resource + +export work_id=0a0600000000000000 +resim run ./transactions/finish_work.rtm --signing-keys $priv_key3,$priv_key4 + +export work_id=0a0700000000000000 +resim run ./transactions/finish_work.rtm --signing-keys $priv_key3,$priv_key4 + +export work_id=0a0800000000000000 +resim run ./transactions/finish_work.rtm --signing-keys $priv_key3,$priv_key4 + +export work_id=0a0900000000000000 +resim run ./transactions/finish_work.rtm --signing-keys $priv_key3,$priv_key4 + +export work_id=0a0a00000000000000 +resim run ./transactions/finish_work.rtm --signing-keys $priv_key3,$priv_key4 + +export work_component=$graphics_design_component +export work_resource=$graphics_design_resource + +export work_id=0a0600000000000000 +resim run ./transactions/finish_work.rtm --signing-keys $priv_key3,$priv_key4 + +export work_id=0a0700000000000000 +resim run ./transactions/finish_work.rtm --signing-keys $priv_key3,$priv_key4 + +export work_id=0a0800000000000000 +resim run ./transactions/finish_work.rtm --signing-keys $priv_key3,$priv_key4 + +export work_id=0a0900000000000000 +resim run ./transactions/finish_work.rtm --signing-keys $priv_key3,$priv_key4 + +export work_id=0a0a00000000000000 +resim run ./transactions/finish_work.rtm --signing-keys $priv_key3,$priv_key4 + diff --git a/6-nfts-for-financial-applications/moonwork/transactions/9_claim_compensation.sh b/6-nfts-for-financial-applications/moonwork/transactions/9_claim_compensation.sh new file mode 100644 index 000000000..a30936a91 --- /dev/null +++ b/6-nfts-for-financial-applications/moonwork/transactions/9_claim_compensation.sh @@ -0,0 +1,7 @@ +resim set-default-account $address1 $priv_key1 +export account=$address1 +resim run ./transactions/claim_contractor_compensation.rtm + +resim set-default-account $address3 $priv_key3 +export account=$address3 +resim run ./transactions/claim_contractor_compensation.rtm diff --git a/6-nfts-for-financial-applications/moonwork/transactions/accept_contractor_for_work.rtm b/6-nfts-for-financial-applications/moonwork/transactions/accept_contractor_for_work.rtm new file mode 100644 index 000000000..5630e29d1 --- /dev/null +++ b/6-nfts-for-financial-applications/moonwork/transactions/accept_contractor_for_work.rtm @@ -0,0 +1,7 @@ +CALL_METHOD ComponentAddress("system_sim1qsqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqs9fh54n") "lock_fee" Decimal("100"); +CALL_METHOD ComponentAddress("${account}") "create_proof" ResourceAddress("${work_resource}"); +CREATE_PROOF_FROM_AUTH_ZONE_BY_IDS Set(NonFungibleId("${job_id}")) ResourceAddress("${work_resource}") Proof("work_proof"); +CALL_METHOD ComponentAddress("${account}") "create_proof_by_amount" Decimal("1") ResourceAddress("${client_badge}"); +POP_FROM_AUTH_ZONE Proof("client_proof"); +CALL_METHOD ComponentAddress("${work_component}") "accept_contractor_for_work" Proof("work_proof") NonFungibleId("${contractor_id}") Proof("client_proof"); +DROP_ALL_PROOFS; diff --git a/6-nfts-for-financial-applications/moonwork/transactions/claim_client_work_refund.rtm b/6-nfts-for-financial-applications/moonwork/transactions/claim_client_work_refund.rtm new file mode 100644 index 000000000..9c32bb750 --- /dev/null +++ b/6-nfts-for-financial-applications/moonwork/transactions/claim_client_work_refund.rtm @@ -0,0 +1,5 @@ +CALL_METHOD ComponentAddress("system_sim1qsqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqs9fh54n") "lock_fee" Decimal("100"); +CALL_METHOD ComponentAddress("${account}") "create_proof_by_amount" Decimal("1") ResourceAddress("${client_badge}"); +POP_FROM_AUTH_ZONE Proof("client_proof"); +CALL_METHOD ComponentAddress("${moon_work_component}") "claim_client_work_refund" Proof("client_proof"); +CALL_METHOD ComponentAddress("${account}") "deposit_batch" Expression("ENTIRE_WORKTOP"); diff --git a/6-nfts-for-financial-applications/moonwork/transactions/claim_contractor_compensation.rtm b/6-nfts-for-financial-applications/moonwork/transactions/claim_contractor_compensation.rtm new file mode 100644 index 000000000..06d4319ae --- /dev/null +++ b/6-nfts-for-financial-applications/moonwork/transactions/claim_contractor_compensation.rtm @@ -0,0 +1,5 @@ +CALL_METHOD ComponentAddress("system_sim1qsqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqs9fh54n") "lock_fee" Decimal("100"); +CALL_METHOD ComponentAddress("${account}") "create_proof_by_amount" Decimal("1") ResourceAddress("${contractor_badge}"); +POP_FROM_AUTH_ZONE Proof("contractor_proof"); +CALL_METHOD ComponentAddress("${moon_work_component}") "claim_contractor_compensation" Proof("contractor_proof"); +CALL_METHOD ComponentAddress("${account}") "deposit_batch" Expression("ENTIRE_WORKTOP"); diff --git a/6-nfts-for-financial-applications/moonwork/transactions/create_moonwork.rtm b/6-nfts-for-financial-applications/moonwork/transactions/create_moonwork.rtm new file mode 100644 index 000000000..823892c7f --- /dev/null +++ b/6-nfts-for-financial-applications/moonwork/transactions/create_moonwork.rtm @@ -0,0 +1,3 @@ +CALL_METHOD ComponentAddress("system_sim1qsqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqs9fh54n") "lock_fee" Decimal("100"); +CALL_FUNCTION PackageAddress("${package}") "MoonWorkService" "create" Decimal("5") Decimal("1") Struct(1u64,Struct(1u64),Struct(5u64)); +CALL_METHOD ComponentAddress("${address1}") "deposit_batch" Expression("ENTIRE_WORKTOP"); diff --git a/6-nfts-for-financial-applications/moonwork/transactions/create_new_work.rtm b/6-nfts-for-financial-applications/moonwork/transactions/create_new_work.rtm new file mode 100644 index 000000000..54262b59e --- /dev/null +++ b/6-nfts-for-financial-applications/moonwork/transactions/create_new_work.rtm @@ -0,0 +1,7 @@ +CALL_METHOD ComponentAddress("system_sim1qsqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqs9fh54n") "lock_fee" Decimal("100"); +CALL_METHOD ComponentAddress("${account}") "withdraw_by_amount" Decimal("${total_compensation}") ResourceAddress("${radix}"); +TAKE_FROM_WORKTOP ResourceAddress("${radix}") Bucket("xrd_bucket"); +CALL_METHOD ComponentAddress("${account}") "create_proof_by_amount" Decimal("1") ResourceAddress("${client_badge}"); +POP_FROM_AUTH_ZONE Proof("client_proof"); +CALL_METHOD ComponentAddress("${work_component}") "create_new_work" Decimal("${total_compensation}") "${work_title}" "${work_description}" Vec(${skills_required}) Bucket("xrd_bucket") Proof("client_proof"); +CALL_METHOD ComponentAddress("${account}") "deposit_batch" Expression("ENTIRE_WORKTOP"); diff --git a/6-nfts-for-financial-applications/moonwork/transactions/create_work_category.rtm b/6-nfts-for-financial-applications/moonwork/transactions/create_work_category.rtm new file mode 100644 index 000000000..f8fe88482 --- /dev/null +++ b/6-nfts-for-financial-applications/moonwork/transactions/create_work_category.rtm @@ -0,0 +1,4 @@ +CALL_METHOD ComponentAddress("system_sim1qsqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqs9fh54n") "lock_fee" Decimal("100"); +CALL_METHOD ComponentAddress("${account}") "create_proof_by_amount" Decimal("1") ResourceAddress("${service_admin_badge}"); +CALL_METHOD ComponentAddress("${moon_work_component}") "create_new_category" "${category}"; +DROP_ALL_PROOFS; diff --git a/6-nfts-for-financial-applications/moonwork/transactions/dispute/1_create_new_dispute.sh b/6-nfts-for-financial-applications/moonwork/transactions/dispute/1_create_new_dispute.sh new file mode 100644 index 000000000..a8f7c9b33 --- /dev/null +++ b/6-nfts-for-financial-applications/moonwork/transactions/dispute/1_create_new_dispute.sh @@ -0,0 +1,7 @@ +resim set-default-account $address2 $priv_key2 + +export work_id=0a0100000000000000 +export dispute_work_resource=$disputed_work_resource +export account=$address2 +export badge=$client_badge +resim run ./transactions/dispute/create_new_dispute.rtm diff --git a/6-nfts-for-financial-applications/moonwork/transactions/dispute/2_submit_document.sh b/6-nfts-for-financial-applications/moonwork/transactions/dispute/2_submit_document.sh new file mode 100644 index 000000000..7d977e4e5 --- /dev/null +++ b/6-nfts-for-financial-applications/moonwork/transactions/dispute/2_submit_document.sh @@ -0,0 +1,14 @@ +resim set-default-account $address2 $priv_key2 + +export dispute_id=0a0100000000000000 +export account=$address2 +export badge=$client_badge +resim run ./transactions/dispute/submit_document.rtm + +resim set-default-account $address1 $priv_key1 + +export dispute_id=0a0100000000000000 +export account=$address1 +export badge=$contractor_badge +resim run ./transactions/dispute/submit_document.rtm + diff --git a/6-nfts-for-financial-applications/moonwork/transactions/dispute/3_join_and_decide_dispute.sh b/6-nfts-for-financial-applications/moonwork/transactions/dispute/3_join_and_decide_dispute.sh new file mode 100644 index 000000000..c44c6d603 --- /dev/null +++ b/6-nfts-for-financial-applications/moonwork/transactions/dispute/3_join_and_decide_dispute.sh @@ -0,0 +1,15 @@ +resim set-default-account $address3 $priv_key3 +export dispute_id=0a0100000000000000 +export account=$address3 +export badge=$contractor_badge +export side=Contractor +resim run ./transactions/dispute/join_and_decide_dispute.rtm + +resim set-default-account $address4 $priv_key4 +export dispute_id=0a0100000000000000 +export account=$address4 +export badge=$client_badge +export side=Contractor +resim run ./transactions/dispute/join_and_decide_dispute.rtm + +resim set-default-account $address1 $priv_key1 diff --git a/6-nfts-for-financial-applications/moonwork/transactions/dispute/4_complete_dispute.sh b/6-nfts-for-financial-applications/moonwork/transactions/dispute/4_complete_dispute.sh new file mode 100644 index 000000000..01b5bd577 --- /dev/null +++ b/6-nfts-for-financial-applications/moonwork/transactions/dispute/4_complete_dispute.sh @@ -0,0 +1,11 @@ +resim set-default-account $address1 $priv_key1 + +export dispute_id=0a0100000000000000 +export account=$address1 +export badge=$contractor_badge +resim run ./transactions/dispute/complete_dispute.rtm +resim run ./transactions/claim_contractor_compensation.rtm + +resim set-default-account $address2 $priv_key2 +export account=$address2 +resim run ./transactions/claim_client_work_refund.rtm diff --git a/6-nfts-for-financial-applications/moonwork/transactions/dispute/complete_dispute.rtm b/6-nfts-for-financial-applications/moonwork/transactions/dispute/complete_dispute.rtm new file mode 100644 index 000000000..a1d0e70e7 --- /dev/null +++ b/6-nfts-for-financial-applications/moonwork/transactions/dispute/complete_dispute.rtm @@ -0,0 +1,6 @@ +CALL_METHOD ComponentAddress("system_sim1qsqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqs9fh54n") "lock_fee" Decimal("100"); +CALL_METHOD ComponentAddress("${account}") "create_proof_by_amount" Decimal("1") ResourceAddress("${badge}"); +POP_FROM_AUTH_ZONE Proof("badge"); +CALL_METHOD ComponentAddress("${disputed_work_dispute_component}") "complete_dispute" NonFungibleId("${dispute_id}") Proof("badge"); +DROP_ALL_PROOFS; + diff --git a/6-nfts-for-financial-applications/moonwork/transactions/dispute/create_new_dispute.rtm b/6-nfts-for-financial-applications/moonwork/transactions/dispute/create_new_dispute.rtm new file mode 100644 index 000000000..efc6a268f --- /dev/null +++ b/6-nfts-for-financial-applications/moonwork/transactions/dispute/create_new_dispute.rtm @@ -0,0 +1,5 @@ +CALL_METHOD ComponentAddress("system_sim1qsqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqs9fh54n") "lock_fee" Decimal("100"); +CALL_METHOD ComponentAddress("${account}") "create_proof_by_amount" Decimal("1") ResourceAddress("${badge}"); +POP_FROM_AUTH_ZONE Proof("badge"); +CALL_METHOD ComponentAddress("${disputed_work_dispute_component}") "create_new_dispute" NonFungibleId("${work_id}") ResourceAddress("${dispute_work_resource}") Proof("badge"); +DROP_ALL_PROOFS; diff --git a/6-nfts-for-financial-applications/moonwork/transactions/dispute/join_and_decide_dispute.rtm b/6-nfts-for-financial-applications/moonwork/transactions/dispute/join_and_decide_dispute.rtm new file mode 100644 index 000000000..faef88410 --- /dev/null +++ b/6-nfts-for-financial-applications/moonwork/transactions/dispute/join_and_decide_dispute.rtm @@ -0,0 +1,6 @@ +CALL_METHOD ComponentAddress("system_sim1qsqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqs9fh54n") "lock_fee" Decimal("100"); +CALL_METHOD ComponentAddress("${account}") "create_proof_by_amount" Decimal("1") ResourceAddress("${badge}"); +POP_FROM_AUTH_ZONE Proof("badge"); +CALL_METHOD ComponentAddress("${disputed_work_dispute_component}") "join_and_decide_dispute" NonFungibleId("${dispute_id}") Enum("${side}") Proof("badge"); +DROP_ALL_PROOFS; + diff --git a/6-nfts-for-financial-applications/moonwork/transactions/dispute/run_full_flow.sh b/6-nfts-for-financial-applications/moonwork/transactions/dispute/run_full_flow.sh new file mode 100644 index 000000000..8540a7356 --- /dev/null +++ b/6-nfts-for-financial-applications/moonwork/transactions/dispute/run_full_flow.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +# Creates a dispute between client and contractor +source ./transactions/dispute/1_create_new_dispute.sh +# Submits a document nft by client and contractor +source ./transactions/dispute/2_submit_document.sh +# Participant that is eligble joins and decides on the dispute in favour of contractor +source ./transactions/dispute/3_join_and_decide_dispute.sh +# Contractor completes the dispute and both client and contractor claim their dispute outcome NFT +source ./transactions/dispute/4_complete_dispute.sh diff --git a/6-nfts-for-financial-applications/moonwork/transactions/dispute/submit_document.rtm b/6-nfts-for-financial-applications/moonwork/transactions/dispute/submit_document.rtm new file mode 100644 index 000000000..7288febec --- /dev/null +++ b/6-nfts-for-financial-applications/moonwork/transactions/dispute/submit_document.rtm @@ -0,0 +1,5 @@ +CALL_METHOD ComponentAddress("system_sim1qsqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqs9fh54n") "lock_fee" Decimal("100"); +CALL_METHOD ComponentAddress("${account}") "create_proof_by_amount" Decimal("1") ResourceAddress("${badge}"); +POP_FROM_AUTH_ZONE Proof("badge"); +CALL_METHOD ComponentAddress("${disputed_work_dispute_component}") "submit_document" NonFungibleId("${dispute_id}") "signed contract" "https://example.com/doc.pdf" Proof("badge"); +DROP_ALL_PROOFS; diff --git a/6-nfts-for-financial-applications/moonwork/transactions/finish_work.rtm b/6-nfts-for-financial-applications/moonwork/transactions/finish_work.rtm new file mode 100644 index 000000000..18428fc5a --- /dev/null +++ b/6-nfts-for-financial-applications/moonwork/transactions/finish_work.rtm @@ -0,0 +1,9 @@ +CALL_METHOD ComponentAddress("system_sim1qsqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqs9fh54n") "lock_fee" Decimal("100"); +CALL_METHOD ComponentAddress("${client}") "create_proof_by_amount" Decimal("1") ResourceAddress("${client_badge}"); +POP_FROM_AUTH_ZONE Proof("client_proof"); +CALL_METHOD ComponentAddress("${contractor}") "create_proof_by_amount" Decimal("1") ResourceAddress("${contractor_badge}"); +POP_FROM_AUTH_ZONE Proof("contractor_proof"); +CALL_METHOD ComponentAddress("${client}") "create_proof" ResourceAddress("${work_resource}"); +CREATE_PROOF_FROM_AUTH_ZONE_BY_IDS Set(NonFungibleId("${work_id}")) ResourceAddress("${work_resource}") Proof("work_proof"); +CALL_METHOD ComponentAddress("${work_component}") "finish_work" Proof("work_proof") Proof("client_proof") Proof("contractor_proof"); +DROP_ALL_PROOFS; diff --git a/6-nfts-for-financial-applications/moonwork/transactions/promotion/1_create_promotion_service.sh b/6-nfts-for-financial-applications/moonwork/transactions/promotion/1_create_promotion_service.sh new file mode 100644 index 000000000..9acc94ca1 --- /dev/null +++ b/6-nfts-for-financial-applications/moonwork/transactions/promotion/1_create_promotion_service.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +resim set-default-account $address1 $priv_key1 +export account=$address1 +export op1=$(resim run ./transactions/promotion/create_promotion_service.rtm) +# promotion component address +export promotion_component=$(echo $op1 | sed -nr "s/.*Component: ([[:alnum:]_]+)/\1/p") +# promotion resource which is stored in the vault +export promotion_resource=$(echo $op1 | sed -nr "s/.*Resource: ([[:alnum:]_]+)/\1/p" | sed '1q;d') diff --git a/6-nfts-for-financial-applications/moonwork/transactions/promotion/2_promote_contractor.sh b/6-nfts-for-financial-applications/moonwork/transactions/promotion/2_promote_contractor.sh new file mode 100644 index 000000000..b23fbb6f7 --- /dev/null +++ b/6-nfts-for-financial-applications/moonwork/transactions/promotion/2_promote_contractor.sh @@ -0,0 +1,5 @@ +#!/bin/bash + +resim set-default-account $address1 $priv_key1 +export account=$address1 +resim run ./transactions/promotion/promote_contractor.rtm diff --git a/6-nfts-for-financial-applications/moonwork/transactions/promotion/3_remove_expired_promotions.sh b/6-nfts-for-financial-applications/moonwork/transactions/promotion/3_remove_expired_promotions.sh new file mode 100644 index 000000000..7fd34af5c --- /dev/null +++ b/6-nfts-for-financial-applications/moonwork/transactions/promotion/3_remove_expired_promotions.sh @@ -0,0 +1,6 @@ +resim set-default-account $address1 $priv_key1 +export account=$address1 + +resim set-current-epoch 481 + +resim run ./transactions/promotion/remove_expired_promotions.rtm diff --git a/6-nfts-for-financial-applications/moonwork/transactions/promotion/create_promotion_service.rtm b/6-nfts-for-financial-applications/moonwork/transactions/promotion/create_promotion_service.rtm new file mode 100644 index 000000000..fe4e84c45 --- /dev/null +++ b/6-nfts-for-financial-applications/moonwork/transactions/promotion/create_promotion_service.rtm @@ -0,0 +1,4 @@ +CALL_METHOD ComponentAddress("system_sim1qsqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqs9fh54n") "lock_fee" Decimal("100"); +CALL_METHOD ComponentAddress("${account}") "create_proof_by_amount" Decimal("1") ResourceAddress("${service_admin_badge}"); +CALL_METHOD ComponentAddress("${moon_work_component}") "create_promotion_service"; +DROP_ALL_PROOFS; diff --git a/6-nfts-for-financial-applications/moonwork/transactions/promotion/promote_contractor.rtm b/6-nfts-for-financial-applications/moonwork/transactions/promotion/promote_contractor.rtm new file mode 100644 index 000000000..b544d2f47 --- /dev/null +++ b/6-nfts-for-financial-applications/moonwork/transactions/promotion/promote_contractor.rtm @@ -0,0 +1,9 @@ +CALL_METHOD ComponentAddress("system_sim1qsqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqs9fh54n") "lock_fee" Decimal("100"); +CALL_METHOD ComponentAddress("${account}") "create_proof_by_amount" Decimal("20") ResourceAddress("${completed_work_resource}"); +POP_FROM_AUTH_ZONE Proof("work_completed"); +CALL_METHOD ComponentAddress("${account}") "create_proof_by_amount" Decimal("3") ResourceAddress("${contractor_accolade_resource}"); +POP_FROM_AUTH_ZONE Proof("accolades"); +CALL_METHOD ComponentAddress("${account}") "create_proof_by_amount" Decimal("1") ResourceAddress("${contractor_badge}"); +POP_FROM_AUTH_ZONE Proof("contractor_proof"); +CALL_METHOD ComponentAddress("${promotion_component}") "promote_contractor" Proof("work_completed") Proof("accolades") Proof("contractor_proof"); +DROP_ALL_PROOFS; diff --git a/6-nfts-for-financial-applications/moonwork/transactions/promotion/remove_expired_promotions.rtm b/6-nfts-for-financial-applications/moonwork/transactions/promotion/remove_expired_promotions.rtm new file mode 100644 index 000000000..80506aaa0 --- /dev/null +++ b/6-nfts-for-financial-applications/moonwork/transactions/promotion/remove_expired_promotions.rtm @@ -0,0 +1,4 @@ +CALL_METHOD ComponentAddress("system_sim1qsqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqs9fh54n") "lock_fee" Decimal("100"); +CALL_METHOD ComponentAddress("${account}") "create_proof_by_amount" Decimal("1") ResourceAddress("${service_admin_badge}"); +CALL_METHOD ComponentAddress("${promotion_component}") "remove_expired_promotions"; +DROP_ALL_PROOFS; diff --git a/6-nfts-for-financial-applications/moonwork/transactions/promotion/run_full_flow.sh b/6-nfts-for-financial-applications/moonwork/transactions/promotion/run_full_flow.sh new file mode 100644 index 000000000..f7c0eb8b1 --- /dev/null +++ b/6-nfts-for-financial-applications/moonwork/transactions/promotion/run_full_flow.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +# Creates a promotion service from MoonWork service +source ./transactions/promotion/1_create_promotion_service.sh +# Promotes a contractor +source ./transactions/promotion/2_promote_contractor.sh +# Passes some time (epoch) which expires the promotion, remove expired promotion, ending the contractor's promotion +source ./transactions/promotion/3_remove_expired_promotions.sh + diff --git a/6-nfts-for-financial-applications/moonwork/transactions/register_as_client.rtm b/6-nfts-for-financial-applications/moonwork/transactions/register_as_client.rtm new file mode 100644 index 000000000..8b3e47595 --- /dev/null +++ b/6-nfts-for-financial-applications/moonwork/transactions/register_as_client.rtm @@ -0,0 +1,3 @@ +CALL_METHOD ComponentAddress("system_sim1qsqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqs9fh54n") "lock_fee" Decimal("100"); +CALL_METHOD ComponentAddress("${moon_work_component}") "register_as_client" "${client}"; +CALL_METHOD ComponentAddress("${account}") "deposit_batch" Expression("ENTIRE_WORKTOP"); diff --git a/6-nfts-for-financial-applications/moonwork/transactions/register_as_contractor.rtm b/6-nfts-for-financial-applications/moonwork/transactions/register_as_contractor.rtm new file mode 100644 index 000000000..cb8d0efce --- /dev/null +++ b/6-nfts-for-financial-applications/moonwork/transactions/register_as_contractor.rtm @@ -0,0 +1,3 @@ +CALL_METHOD ComponentAddress("system_sim1qsqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqs9fh54n") "lock_fee" Decimal("100"); +CALL_METHOD ComponentAddress("${moon_work_component}") "register_as_contractor" "${contractor}"; +CALL_METHOD ComponentAddress("${account}") "deposit_batch" Expression("ENTIRE_WORKTOP"); diff --git a/6-nfts-for-financial-applications/moonwork/transactions/request_work.rtm b/6-nfts-for-financial-applications/moonwork/transactions/request_work.rtm new file mode 100644 index 000000000..85244df95 --- /dev/null +++ b/6-nfts-for-financial-applications/moonwork/transactions/request_work.rtm @@ -0,0 +1,5 @@ +CALL_METHOD ComponentAddress("system_sim1qsqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqs9fh54n") "lock_fee" Decimal("100"); +CALL_METHOD ComponentAddress("${account}") "create_proof_by_amount" Decimal("1") ResourceAddress("${contractor_badge}"); +POP_FROM_AUTH_ZONE Proof("contractor_proof"); +CALL_METHOD ComponentAddress("${work_component}") "request_work" NonFungibleId("${job_id}") Proof("contractor_proof"); +CALL_METHOD ComponentAddress("${account}") "deposit_batch" Expression("ENTIRE_WORKTOP"); diff --git a/6-nfts-for-financial-applications/moonwork/transactions/run_full_flow.sh b/6-nfts-for-financial-applications/moonwork/transactions/run_full_flow.sh new file mode 100644 index 000000000..3c157f6fe --- /dev/null +++ b/6-nfts-for-financial-applications/moonwork/transactions/run_full_flow.sh @@ -0,0 +1,20 @@ +#!/bin/bash + +# Creates the MoonWork service +source ./transactions/1_create_moon_work_service.sh +# Registers 2 contractors +source ./transactions/2_register_contractors.sh +# Registers 2 clients +source ./transactions/3_register_client.sh +# Creates a bunch of work categories, including one category specifically for handling disputes flow +source ./transactions/4_create_work_categories.sh +# Client creating a bunch of new work +source ./transactions/5_create_new_job.sh +# Contractor requesting to get a lot of work +source ./transactions/6_request_work.sh +# Client accepting contractor for all work +source ./transactions/7_accept_contractor_for_work.sh +# Client & contractor agree to finishing work +source ./transactions/8_finish_work.sh +# Contractor claims all compensation from work thats been completed +source ./transactions/9_claim_compensation.sh