polymesh_ink/
lib.rs

1#![cfg_attr(not(feature = "std"), no_std, no_main)]
2
3extern crate alloc;
4
5mod macros;
6
7use alloc::collections::BTreeSet;
8#[cfg(not(feature = "as-library"))]
9use alloc::vec;
10
11use alloc::vec::Vec;
12
13pub use polymesh_api::ink::basic_types::{AssetId, IdentityId};
14pub use polymesh_api::ink::extension::PolymeshEnvironment;
15pub use polymesh_api::ink::Error as PolymeshInkError;
16pub use polymesh_api::polymesh::types::pallet_corporate_actions;
17pub use polymesh_api::polymesh::types::pallet_corporate_actions::CAId;
18pub use polymesh_api::polymesh::types::polymesh_contracts::Api as ContractRuntimeApi;
19pub use polymesh_api::polymesh::types::polymesh_primitives::asset::{
20    AssetName, AssetType, CheckpointId,
21};
22pub use polymesh_api::polymesh::types::polymesh_primitives::asset_metadata::{
23    AssetMetadataKey, AssetMetadataLocalKey, AssetMetadataName, AssetMetadataValue,
24};
25pub use polymesh_api::polymesh::types::polymesh_primitives::identity_id::{
26    PortfolioId, PortfolioKind, PortfolioName, PortfolioNumber,
27};
28pub use polymesh_api::polymesh::types::polymesh_primitives::nft::{NFTId, NFTs};
29pub use polymesh_api::polymesh::types::polymesh_primitives::portfolio::{Fund, FundDescription};
30pub use polymesh_api::polymesh::types::polymesh_primitives::settlement::{
31    InstructionId, Leg, SettlementType, VenueDetails, VenueId, VenueType,
32};
33pub use polymesh_api::polymesh::Api;
34
35pub const API_VERSION: ContractRuntimeApi = ContractRuntimeApi {
36    desc: *b"POLY",
37    major: 7,
38};
39
40/// Contract Errors.
41#[derive(Debug, scale::Encode, scale::Decode)]
42#[cfg_attr(feature = "std", derive(scale_info::TypeInfo))]
43pub enum PolymeshError {
44    /// Polymesh runtime error.
45    PolymeshRuntime(PolymeshInkError),
46    /// [`IdentityId`] not found for the contract caller. MultiSig's are not supported.
47    MissingIdentity,
48    /// No portfolio was found for the given [`PortfolioId`].
49    InvalidPortfolioAuthorization,
50    /// Ink! Delegate call error.
51    InkDelegateCallError {
52        selector: [u8; 4],
53        err: Option<InkEnvError>,
54    },
55    /// Not the NFT owner.
56    NotNftOwner,
57}
58
59/// Encodable `ink::env::Error`.
60#[derive(Debug, scale::Encode, scale::Decode)]
61#[cfg_attr(feature = "std", derive(scale_info::TypeInfo))]
62pub enum InkEnvError {
63    /// Error upon decoding an encoded value.
64    ScaleDecodeError,
65    /// The call to another contract has trapped.
66    CalleeTrapped,
67    /// The call to another contract has been reverted.
68    CalleeReverted,
69    /// The queried contract storage entry is missing.
70    KeyNotFound,
71    /// Transfer failed for other not further specified reason. Most probably
72    /// reserved or locked balance of the sender that was preventing the transfer.
73    TransferFailed,
74    /// Deprecated and no longer returned: Endowment is no longer required.
75    _EndowmentTooLow,
76    /// No code could be found at the supplied code hash.
77    CodeNotFound,
78    /// The account that was called is no contract, but a plain account.
79    NotCallable,
80    /// The call to `seal_debug_message` had no effect because debug message
81    /// recording was disabled.
82    LoggingDisabled,
83    /// ECDSA pubkey recovery failed. Most probably wrong recovery id or signature.
84    EcdsaRecoveryFailed,
85}
86
87impl PolymeshError {
88    pub fn from_delegate_error(err: ink::env::Error, selector: ink::env::call::Selector) -> Self {
89        use ink::env::Error::*;
90        Self::InkDelegateCallError {
91            selector: selector.to_bytes(),
92            err: match err {
93                Decode(_) => Some(InkEnvError::ScaleDecodeError),
94                CalleeTrapped => Some(InkEnvError::CalleeTrapped),
95                CalleeReverted => Some(InkEnvError::CalleeReverted),
96                KeyNotFound => Some(InkEnvError::KeyNotFound),
97                TransferFailed => Some(InkEnvError::TransferFailed),
98                _EndowmentTooLow => Some(InkEnvError::_EndowmentTooLow),
99                CodeNotFound => Some(InkEnvError::CodeNotFound),
100                NotCallable => Some(InkEnvError::NotCallable),
101                LoggingDisabled => Some(InkEnvError::LoggingDisabled),
102                EcdsaRecoveryFailed => Some(InkEnvError::EcdsaRecoveryFailed),
103                _ => None,
104            },
105        }
106    }
107}
108
109impl From<PolymeshInkError> for PolymeshError {
110    fn from(err: PolymeshInkError) -> Self {
111        Self::PolymeshRuntime(err)
112    }
113}
114
115/// The contract result type.
116pub type PolymeshResult<T> = core::result::Result<T, PolymeshError>;
117
118#[cfg(feature = "as-library")]
119pub type Hash = <PolymeshEnvironment as ink::env::Environment>::Hash;
120#[cfg(feature = "as-library")]
121pub type AccountId = <PolymeshEnvironment as ink::env::Environment>::AccountId;
122pub type Balance = <PolymeshEnvironment as ink::env::Environment>::Balance;
123pub type Timestamp = <PolymeshEnvironment as ink::env::Environment>::Timestamp;
124
125#[derive(Debug, scale::Encode, scale::Decode)]
126#[cfg_attr(feature = "std", derive(scale_info::TypeInfo))]
127pub struct DistributionSummary {
128    pub currency: AssetId,
129    pub per_share: Balance,
130    pub reclaimed: bool,
131    pub payment_at: Timestamp,
132    pub expires_at: Option<Timestamp>,
133}
134
135impl From<pallet_corporate_actions::distribution::Distribution> for DistributionSummary {
136    fn from(distribution: pallet_corporate_actions::distribution::Distribution) -> Self {
137        Self {
138            currency: distribution.currency,
139            per_share: distribution.per_share,
140            reclaimed: distribution.reclaimed,
141            payment_at: distribution.payment_at,
142            expires_at: distribution.expires_at,
143        }
144    }
145}
146
147#[derive(Debug, scale::Encode, scale::Decode)]
148#[cfg_attr(feature = "std", derive(scale_info::TypeInfo))]
149pub struct SimpleDividend {
150    pub asset_id: AssetId,
151    pub decl_date: Timestamp,
152    pub record_date: Timestamp,
153    pub portfolio: Option<PortfolioNumber>,
154    pub currency: AssetId,
155    pub per_share: Balance,
156    pub amount: Balance,
157    pub payment_at: Timestamp,
158    pub expires_at: Option<Timestamp>,
159}
160
161upgradable_api! {
162    mod polymesh_ink {
163        impl PolymeshInk {
164            /// Wrap the `system.remark` extrinsic.  Only useful for testing.
165            #[ink(message)]
166            pub fn system_remark(&self, remark: Vec<u8>) -> PolymeshResult<()> {
167                let api = Api::new();
168                api.call().system().remark(remark).submit()?;
169                Ok(())
170            }
171
172            /// Creates a portfolio with the given `name`.
173            #[ink(message)]
174            pub fn create_portfolio(&self, name: Vec<u8>) -> PolymeshResult<PortfolioId> {
175                let api = Api::new();
176
177                // Get the contract's did
178                let contracts_did = Self::get_our_did()?;
179                // Get the next portfolio number
180                let portfolio_number = api
181                    .query()
182                    .portfolio()
183                    .next_portfolio_number(contracts_did)?;
184
185                api.call()
186                    .portfolio()
187                    .create_portfolio(PortfolioName(name))
188                    .submit()?;
189
190                Ok(PortfolioId {
191                    did: contracts_did,
192                    kind: PortfolioKind::User(portfolio_number),
193                })
194            }
195
196            /// Accepts custody of a portfolio.
197            #[ink(message)]
198            pub fn accept_portfolio_custody(
199                &self,
200                auth_id: u64,
201                portfolio_kind: PortfolioKind
202            ) -> PolymeshResult<PortfolioId> {
203                let api = Api::new();
204
205                // Get the caller's identity.
206                let caller_did = Self::get_caller_did()?;
207                // Get the contract's did
208                let contracts_did = Self::get_our_did()?;
209
210                api.call()
211                    .portfolio()
212                    .accept_portfolio_custody(auth_id)
213                    .submit()?;
214
215                let portfolio_id = PortfolioId {
216                    did: caller_did,
217                    kind: portfolio_kind,
218                };
219
220                if !api
221                    .query()
222                    .portfolio()
223                    .portfolios_in_custody(contracts_did, portfolio_id)?
224                {
225                    return Err(PolymeshError::InvalidPortfolioAuthorization);
226                }
227
228                Ok(portfolio_id)
229            }
230
231            /// Quits custodianship of a portfolio returning control back to the owner.
232            #[ink(message)]
233            pub fn quit_portfolio_custody(&self, portfolio_id: PortfolioId) -> PolymeshResult<()> {
234                let api = Api::new();
235
236                api.call()
237                    .portfolio()
238                    .quit_portfolio_custody(portfolio_id)
239                    .submit()?;
240                Ok(())
241            }
242
243            /// Moves the given `funds` from `source_portfolio_id` to `destination_portfolio_id`.
244            #[ink(message)]
245            pub fn move_portfolio_funds(
246                &self,
247                source_portfolio_id: PortfolioId,
248                destination_portfolio_id: PortfolioId,
249                funds: Vec<Fund>
250            ) -> PolymeshResult<()> {
251                let api = Api::new();
252
253                api.call()
254                    .portfolio()
255                    .move_portfolio_funds(source_portfolio_id, destination_portfolio_id, funds)
256                    .submit()?;
257                Ok(())
258            }
259
260            /// Returns the [`Balance`] for the `asset_id` in the given `portfolio_id`.
261            #[ink(message)]
262            pub fn portfolio_asset_balances(
263                &self,
264                portfolio_id: PortfolioId,
265                asset_id: AssetId
266            ) -> PolymeshResult<Balance> {
267                let api = Api::new();
268
269                let balance = api
270                    .query()
271                    .portfolio()
272                    .portfolio_asset_balances(portfolio_id, asset_id)?;
273                Ok(balance)
274            }
275
276            /// Returns `true` if `portfolio_id` is in custody of `custodian_did`, otherwise returns `false`.
277            #[ink(message)]
278            pub fn check_portfolios_in_custody(
279                &self,
280                custodian_did: IdentityId,
281                portfolio_id: PortfolioId
282            ) -> PolymeshResult<bool> {
283                let api = Api::new();
284
285                let is_custodian = api.query()
286                    .portfolio()
287                    .portfolios_in_custody(custodian_did, portfolio_id)?;
288                Ok(is_custodian)
289            }
290
291            /// Creates a Settlement Venue.
292            #[ink(message)]
293            pub fn create_venue(
294                &self,
295                venue_details: VenueDetails,
296                venue_type: VenueType
297            ) -> PolymeshResult<VenueId> {
298                let api = Api::new();
299
300                // Get the next venue id.
301                let venue_id = api.query().settlement().venue_counter()?;
302
303                api.call()
304                    .settlement()
305                    .create_venue(venue_details, vec![], venue_type)
306                    .submit()?;
307                Ok(venue_id)
308            }
309
310            /// Creates and manually executes a settlement to transfer assets.
311            #[ink(message)]
312            pub fn settlement_execute(
313                &self,
314                venue_id: Option<VenueId>,
315                legs: Vec<Leg>,
316                portfolios: BTreeSet<PortfolioId>
317            ) -> PolymeshResult<()> {
318                let api = Api::new();
319
320                // Counts the number of each asset type
321                let (fungible, nfts, offchain) =
322                    legs.iter()
323                        .fold((0, 0, 0), |(fungible, nfts, offchain), leg| match leg {
324                            Leg::Fungible { .. } => (fungible + 1, nfts, offchain),
325                            Leg::NonFungible { .. } => (fungible, nfts + 1, offchain),
326                            Leg::OffChain { .. } => (fungible, nfts, offchain + 1),
327                        });
328
329                // Get the next instruction id.
330                let instruction_id = api
331                    .query()
332                    .settlement()
333                    .instruction_counter()?;
334
335                api.call()
336                    .settlement()
337                    .add_and_affirm_instruction(
338                        venue_id,
339                        SettlementType::SettleManual(0),
340                        None,
341                        None,
342                        legs,
343                        portfolios,
344                        None,
345                    )
346                    .submit()?;
347
348                api.call()
349                    .settlement()
350                    .execute_manual_instruction(instruction_id, None, fungible, nfts, offchain, None)
351                    .submit()?;
352                Ok(())
353            }
354
355            /// Issues `amount_to_issue` new tokens to `portfolio_kind`.
356            #[ink(message)]
357            pub fn asset_issue(
358                &self,
359                asset_id: AssetId,
360                amount_to_issue: Balance,
361                portfolio_kind: PortfolioKind
362            ) -> PolymeshResult<()> {
363                let api = Api::new();
364                api.call().asset().issue(asset_id, amount_to_issue, portfolio_kind).submit()?;
365                Ok(())
366            }
367
368            /// Redeems `amount_to_redeem` tokens from `portfolio_kind`.
369            #[ink(message)]
370            pub fn asset_redeem(
371                &self,
372                asset_id: AssetId,
373                amount_to_redeem: Balance,
374                portfolio_kind: PortfolioKind
375            ) -> PolymeshResult<()> {
376                let api = Api::new();
377                api.call().asset().redeem(asset_id, amount_to_redeem, portfolio_kind).submit()?;
378                Ok(())
379            }
380
381            /// Creates a new asset and issues `amount_to_issue` tokens of that asset to the default portfolio of the caller.
382            #[ink(message)]
383            pub fn asset_create_and_issue(
384                &self,
385                asset_name: AssetName,
386                asset_type: AssetType,
387                divisible: bool,
388                amount_to_issue: Option<Balance>
389            ) -> PolymeshResult<AssetId> {
390                let api = Api::new();
391
392                let asset_id = Self::get_our_next_asset_id()?;
393
394                api.call()
395                    .asset()
396                    .create_asset(asset_name, divisible, asset_type, vec![], None)
397                    .submit()?;
398
399                if let Some(amount_to_issue) = amount_to_issue {
400                    api.call()
401                        .asset()
402                        .issue(asset_id, amount_to_issue, PortfolioKind::Default)
403                        .submit()?;
404                }
405
406                Ok(asset_id)
407            }
408
409            /// Returns the `asset_id` [`Balance`] for the given `did`.
410            #[ink(message)]
411            pub fn asset_balance_of(
412                &self,
413                asset_id: AssetId,
414                did: IdentityId
415            ) -> PolymeshResult<Balance> {
416                let api = Api::new();
417                let balance = api.query().asset().balance_of(asset_id, did)?;
418                Ok(balance)
419            }
420
421            /// Returns the total supply of `asset_id`.
422            #[ink(message)]
423            pub fn asset_total_supply(
424                &self,
425                asset_id: AssetId
426            ) -> PolymeshResult<Balance> {
427                let api = Api::new();
428
429                let asset_details = api.query().asset().assets(asset_id)?;
430                let total_supply = asset_details
431                    .map(|asset| asset.total_supply)
432                    .unwrap_or_default();
433                Ok(total_supply)
434            }
435
436            /// Get corporate action distribution summary.
437            #[ink(message)]
438            pub fn distribution_summary(
439                &self,
440                ca_id: CAId
441            ) -> PolymeshResult<Option<DistributionSummary>> {
442                let api = Api::new();
443                let distribution = api.query().capital_distribution().distributions(ca_id)?;
444                Ok(distribution.map(|d| d.into()))
445            }
446
447            /// Claims dividends from a distribution.
448            #[ink(message)]
449            pub fn dividend_claim(
450                &self,
451                ca_id: CAId
452            ) -> PolymeshResult<()> {
453                let api = Api::new();
454                api.call().capital_distribution().claim(ca_id).submit()?;
455                Ok(())
456            }
457
458            /// Create a simple dividend distribution.
459            #[ink(message)]
460            pub fn create_dividend(
461                &self,
462                dividend: SimpleDividend
463            ) -> PolymeshResult<()> {
464                let api = Api::new();
465                // Corporate action args.
466                let ca_args = pallet_corporate_actions::InitiateCorporateActionArgs {
467                    asset_id: dividend.asset_id,
468                    kind: pallet_corporate_actions::CAKind::PredictableBenefit,
469                    decl_date: dividend.decl_date,
470                    record_date: Some(pallet_corporate_actions::RecordDateSpec::Scheduled(dividend.record_date)),
471                    details: pallet_corporate_actions::CADetails(vec![]),
472                    targets: None,
473                    default_withholding_tax: None,
474                    withholding_tax: None,
475                };
476                // Create corporate action & distribution.
477                api.call()
478                    .corporate_action()
479                    .initiate_corporate_action_and_distribute(
480                        ca_args,
481                        dividend.portfolio,
482                        dividend.currency,
483                        dividend.per_share,
484                        dividend.amount,
485                        dividend.payment_at,
486                        dividend.expires_at,
487                    )
488                    .submit()?;
489                Ok(())
490            }
491
492            /// Adds and affirms an instruction.
493            #[ink(message)]
494            pub fn add_and_affirm_instruction(
495                &self,
496                venue_id: Option<VenueId>,
497                legs: Vec<Leg>,
498                portfolios: BTreeSet<PortfolioId>
499            ) -> PolymeshResult<InstructionId> {
500                let api = Api::new();
501
502                let instruction_id = api
503                    .query()
504                    .settlement()
505                    .instruction_counter()?;
506
507                api.call()
508                    .settlement()
509                    .add_and_affirm_instruction(
510                        venue_id,
511                        SettlementType::SettleOnAffirmation,
512                        None,
513                        None,
514                        legs,
515                        portfolios,
516                        None,
517                    )
518                    .submit()?;
519                Ok(instruction_id)
520            }
521
522            /// Creates a portoflio owned by `portfolio_owner_id` and transfer its custody to the smart contract.
523            /// Returns the [`PortfolioId`] of the new portfolio.
524            #[ink(message)]
525            pub fn create_custody_portfolio(
526                &self,
527                portfolio_owner_id: IdentityId,
528                portfolio_name: PortfolioName
529            ) -> PolymeshResult<PortfolioId> {
530                let api = Api::new();
531
532                let portfolio_number = api
533                    .query()
534                    .portfolio()
535                    .next_portfolio_number(portfolio_owner_id)?;
536                let portfolio_id = PortfolioId {
537                    did: portfolio_owner_id,
538                    kind: PortfolioKind::User(portfolio_number),
539                };
540
541                api.call()
542                    .portfolio()
543                    .create_custody_portfolio(portfolio_owner_id, portfolio_name)
544                    .submit()?;
545                Ok(portfolio_id)
546            }
547
548            /// Returns the [`AssetMetadataLocalKey`] for the given `asset_id` and `asset_metadata_name`.
549            #[ink(message)]
550            pub fn asset_metadata_local_name_to_key(
551                &self,
552                asset_id: AssetId,
553                asset_metadata_name: AssetMetadataName
554            ) -> PolymeshResult<Option<AssetMetadataLocalKey>> {
555                Ok(Api::new()
556                    .query()
557                    .asset()
558                    .asset_metadata_local_name_to_key(asset_id, asset_metadata_name)?)
559            }
560
561            /// Returns the [`AssetMetadataValue`] for the given `asset_id` and `asset_metadata_key`.
562            #[ink(message)]
563            pub fn asset_metadata_value(
564                &self,
565                asset_id: AssetId,
566                asset_metadata_key: AssetMetadataKey
567            ) -> PolymeshResult<Option<AssetMetadataValue>> {
568                Ok(Api::new()
569                    .query()
570                    .asset()
571                    .asset_metadata_values(asset_id, asset_metadata_key)?)
572            }
573
574            /// Returns the [`PortfolioId`] that holds the NFT.
575            #[ink(message)]
576            pub fn nft_owner(
577              &self,
578              asset_id: AssetId,
579              nft: NFTId,
580            ) -> PolymeshResult<Option<PortfolioId>> {
581                let api = Api::new();
582                Ok(api.query().nft().nft_owner(asset_id, nft)?)
583            }
584
585            /// Returns `Ok` if `portfolio_id` holds all `nfts`. Otherwise, returns [`PolymeshError::NotNftOwner`].
586            #[ink(message)]
587            pub fn holds_nfts(
588              &self,
589              portfolio_id: PortfolioId,
590              asset_id: AssetId,
591              nfts: Vec<NFTId>,
592            ) -> PolymeshResult<()> {
593                let api = Api::new();
594                for nft_id in nfts {
595                    let nft_holder = api
596                        .query()
597                        .nft()
598                        .nft_owner(asset_id, nft_id)?
599                        .ok_or(PolymeshError::NotNftOwner)?;
600
601                    if nft_holder != portfolio_id {
602                        return Err(PolymeshError::NotNftOwner);
603                    }
604                }
605                Ok(())
606            }
607        }
608
609        // Non-upgradable api.
610        impl PolymeshInk {
611            /// Get the identity of the caller.
612            pub fn get_caller_did() -> PolymeshResult<IdentityId> {
613                Self::get_key_did(ink::env::caller::<PolymeshEnvironment>())
614            }
615
616            /// Get the identity of the contract.
617            pub fn get_our_did() -> PolymeshResult<IdentityId> {
618                Self::get_key_did(ink::env::account_id::<PolymeshEnvironment>())
619            }
620
621            /// Get the identity of a key.
622            pub fn get_key_did(acc: AccountId) -> PolymeshResult<IdentityId> {
623                let api = Api::new();
624                api.runtime()
625                    .get_key_did(acc)?
626                    .ok_or(PolymeshError::MissingIdentity)
627            }
628
629            /// Returns the next [`AssetId`] for the caller.
630            pub fn get_our_next_asset_id() -> PolymeshResult<AssetId> {
631                Self::get_next_asset_id(ink::env::account_id::<PolymeshEnvironment>())
632            }
633
634            /// Returns the next [`AssetId`] for the given `account_id`.
635            pub fn get_next_asset_id(account_id: AccountId) -> PolymeshResult<AssetId> {
636                let api = Api::new();
637                Ok(api.runtime().get_next_asset_id(account_id)?)
638            }
639        }
640    }
641}