1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
#![no_std]

use gear_lib_old::non_fungible_token::token::{TokenId, TokenMetadata};
use gmeta::{InOut, Metadata, Out};
use gstd::{prelude::*, ActorId};

pub struct ContractMetadata;

impl Metadata for ContractMetadata {
    type Init = InOut<InitNFTPixelboard, Result<(), NFTPixelboardError>>;
    type Handle = InOut<NFTPixelboardAction, Result<NFTPixelboardEvent, NFTPixelboardError>>;
    type Reply = ();
    type Others = ();
    type Signal = ();
    type State = Out<NFTPixelboardState>;
}

#[derive(Default, Encode, Decode, TypeInfo)]
#[codec(crate = gstd::codec)]
#[scale_info(crate = gstd::scale_info)]
pub struct NFTPixelboardState {
    pub owner: ActorId,
    pub block_side_length: BlockSideLength,
    pub pixel_price: u128,
    pub resolution: Resolution,
    pub commission_percentage: u8,
    pub painting: Vec<Color>,

    pub rectangles_by_token_ids: Vec<(TokenId, Rectangle)>,
    pub tokens_by_rectangles: Vec<(Rectangle, TokenInfo)>,

    pub ft_program: ActorId,
    pub nft_program: ActorId,

    pub txs: Vec<(ActorId, (TransactionId, NFTPixelboardAction))>,
    pub tx_id: TransactionId,
}
/// The maximum price that can be set to a pixel.
///
/// This number is calculated to avoid an overflow and precisely calculate a
/// resale commission. Here's an explanation.
///
/// The maximum number of pixels that a canvas can contain is
/// [`BlockSideLength::MAX`]² = 2³². So the maximum price that each pixel can
/// have is [`u128::MAX`] / [`BlockSideLength::MAX`]² = 2⁹⁶.
///
/// To calculate the commission, the number can be multiplied by 100, so, to
/// avoid an overflow, the number must be divided by 100. Hence 2⁹⁶ / 100.
pub const MAX_PIXEL_PRICE: u128 = 2u128.pow(96) / 100;

/// A block side length.
///
/// It's also used to store pixel [`Coordinates`], [`Resolution`] of a canvas,
/// and NFT [`Rectangle`]s.
pub type BlockSideLength = u16;
/// A pixel color.
pub type Color = u8;
/// A transaction id for tracking transactions in the fungible token contract.
pub type TransactionId = u64;

/// Coordinates of the corners of an NFT rectangle on a canvas.
#[derive(Decode, Encode, TypeInfo, PartialEq, Eq, PartialOrd, Ord, Clone, Copy, Debug, Default)]
#[codec(crate = gstd::codec)]
#[scale_info(crate = gstd::scale_info)]
pub struct Rectangle {
    pub top_left_corner: Coordinates,
    pub bottom_right_corner: Coordinates,
}

impl Rectangle {
    pub fn width(&self) -> BlockSideLength {
        self.bottom_right_corner.x - self.top_left_corner.x
    }

    pub fn height(&self) -> BlockSideLength {
        self.bottom_right_corner.y - self.top_left_corner.y
    }
}

impl
    From<(
        (BlockSideLength, BlockSideLength),
        (BlockSideLength, BlockSideLength),
    )> for Rectangle
{
    fn from(
        rectangle: (
            (BlockSideLength, BlockSideLength),
            (BlockSideLength, BlockSideLength),
        ),
    ) -> Self {
        Self {
            top_left_corner: rectangle.0.into(),
            bottom_right_corner: rectangle.1.into(),
        }
    }
}

/// Coordinates of some pixel on a canvas.
#[derive(Decode, Encode, TypeInfo, PartialEq, Eq, PartialOrd, Ord, Clone, Copy, Debug, Default)]
#[codec(crate = gstd::codec)]
#[scale_info(crate = gstd::scale_info)]
pub struct Coordinates {
    pub x: BlockSideLength,
    pub y: BlockSideLength,
}

impl From<(BlockSideLength, BlockSideLength)> for Coordinates {
    fn from((x, y): (BlockSideLength, BlockSideLength)) -> Self {
        Self { x, y }
    }
}

/// A resolution of a canvas.
#[derive(Decode, Encode, Default, Clone, Copy, TypeInfo, Debug, PartialEq, Eq)]
#[codec(crate = gstd::codec)]
#[scale_info(crate = gstd::scale_info)]
pub struct Resolution {
    pub width: BlockSideLength,
    pub height: BlockSideLength,
}

impl From<(BlockSideLength, BlockSideLength)> for Resolution {
    fn from((width, height): (BlockSideLength, BlockSideLength)) -> Self {
        Self { width, height }
    }
}

/// An NFT with its [`Rectangle`] and [`TokenInfo`].
#[derive(Decode, Encode, TypeInfo, Clone, Copy, Debug, Default, PartialEq, Eq)]
#[codec(crate = gstd::codec)]
#[scale_info(crate = gstd::scale_info)]
pub struct Token(pub Rectangle, pub TokenInfo);

/// NFT info.
#[derive(Decode, Encode, TypeInfo, Clone, Copy, Debug, Default, PartialEq, Eq)]
#[codec(crate = gstd::codec)]
#[scale_info(crate = gstd::scale_info)]
pub struct TokenInfo {
    pub token_id: Option<TokenId>,
    pub owner: ActorId,
    /// If this field is [`None`], then this NFT isn't for sale, and vice versa.
    ///
    /// To calculate a price of the entire NFT, its area must be calculated and
    /// multiplied by `pixel_price`. The area can be calculated by multiplying a
    /// [width](`Rectangle::width`) & [height](`Rectangle::height`) from NFT
    /// [`Rectangle`]. NFT [`Rectangle`] can be obtained by
    /// [`token_info()`](../nft_pixelboard_state/metafns/fn.token_info.html) using `token_id` from this
    /// struct.
    pub pixel_price: Option<u128>,
}

/// Initializes the NFT pixelboard program.
///
/// # Requirements
/// * `owner` address mustn't be [`ActorId::zero()`].
/// * `block_side_length` must be more than 0.
/// * `pixel_price` mustn't be more than [`MAX_PIXEL_PRICE`].
/// * A [width](`Resolution#structfield.width`) &
/// [height](`Resolution#structfield.height`) (`resolution`) of a canvas must be
/// more than 0.
/// * Each side of `resolution` must be a multiple of `block_side_length`.
/// * `painting` length must equal a pixel count in a canvas (which can be
/// calculated by multiplying a [width](`Resolution#structfield.width`) &
/// [height](`Resolution#structfield.height`) from `resolution`).
/// * `commission_percentage` mustn't be more than 100.
/// * `ft_program` address mustn't be [`ActorId::zero()`].
/// * `nft_program` address mustn't be [`ActorId::zero()`].
#[derive(Decode, Encode, TypeInfo, Clone)]
#[codec(crate = gstd::codec)]
#[scale_info(crate = gstd::scale_info)]
pub struct InitNFTPixelboard {
    /// An address of a pixelboard owner to which minting fees and commissions
    /// on resales will be transferred.
    pub owner: ActorId,
    /// A block side length.
    ///
    /// To avoid a canvas clogging with one pixel NFTs, blocks are used instead
    /// of pixels to set NFT [`Rectangle`]s. This parameter is used to set a
    /// side length of these pixel blocks. If blocks aren't needed, then this
    /// parameter can be set to 1, so the block side length will equal a pixel.
    pub block_side_length: BlockSideLength,
    /// The price of a free pixel. It'll be used to calculate a minting price.
    pub pixel_price: u128,
    /// A canvas (pixelboard) [width](`Resolution#structfield.width`) &
    /// [height](`Resolution#structfield.height`).
    pub resolution: Resolution,
    /// A commission percentage that'll be included in each NFT resale.
    pub commission_percentage: u8,
    /// A painting that'll be displayed on the free territory of a pixelboard.
    pub painting: Vec<Color>,

    /// A FT program address.
    pub ft_program: ActorId,
    /// An NFT program address.
    pub nft_program: ActorId,
}

/// Sends a program info about what it should do.
#[derive(Decode, Encode, TypeInfo, Clone, PartialEq, Eq)]
#[codec(crate = gstd::codec)]
#[scale_info(crate = gstd::scale_info)]
pub enum NFTPixelboardAction {
    /// Mints one NFT on a pixelboard with given `token_metadata` & `painting`.
    ///
    /// Transfers a minted NFT to [`msg::source()`].
    ///
    /// # Requirements
    /// * `rectangle` coordinates mustn't be out of a canvas.
    /// * `rectangle` coordinates mustn't be mixed up or belong to wrong
    /// corners.
    /// * `rectangle` coordinates must observe a block layout. In other words,
    /// each `rectangle` coordinate must be a multiple of a block side length in
    /// the canvas. The block side length can be obtained by
    /// [`block_side_length()`](../nft_pixelboard_state/metafns/fn.block_side_length.html).
    /// * NFT `rectangle` mustn't collide with already minted one.
    /// * `painting` length must equal a pixel count in an NFT
    /// (which can be calculated by multiplying a [width](`Rectangle::width`) &
    /// [height](`Rectangle::height`) from `rectangle`).
    /// * [`msg::source()`] must have enough fungible tokens to buy all free
    /// pixels that `rectangle` will occupy. An enough number of tokens can be
    /// calculated by multiplying a `rectangle` area and the price of a free
    /// pixel. The area can be calculated by multiplying a
    /// [width](`Rectangle::width`) & [height](`Rectangle::height`) from
    /// `rectangle`. The price of a free pixel can be obtained by
    /// [`pixel_price()`](../nft_pixelboard_state/metafns/fn.pixel_price.html).
    ///
    /// On success, returns [`NFTPixelboardEvent::Minted`].
    ///
    /// [`msg::source()`]: gstd::msg::source
    Mint {
        rectangle: Rectangle,
        token_metadata: TokenMetadata,
        /// A painting that'll be displayed in a place of an NFT on a pixelboard
        /// after a successful minting.
        painting: Vec<Color>,
    },

    /// Buys an NFT minted on a pixelboard.
    ///
    /// Transfers a purchased NFT from a pixelboard program to
    /// [`msg::source()`].
    ///
    /// **Note:** If [`msg::source()`] has enough fungible tokens to pay a
    /// resale commission but not the entire NFT, then the commission will still
    /// be withdrawn from its account.
    ///
    /// # Requirements
    /// * An NFT must be minted on a pixelboard.
    /// * An NFT must be for sale. This can be found out by
    /// [`token_info()`]. See also the documentation of
    /// [`TokenInfo#structfield.pixel_price`].
    /// * [`msg::source()`] must have enough fungible tokens to buy all pixels
    /// that an NFT occupies. This can be found out by
    /// [`token_info()`]. See also the documentation of
    /// [`TokenInfo#structfield.pixel_price`].
    ///
    /// On success, returns [`NFTPixelboardEvent::Bought`].
    ///
    /// [`msg::source()`]: gstd::msg::source
    /// [`token_info()`]: ../nft_pixelboard_state/metafns/fn.token_info.html
    Buy(TokenId),

    /// Changes a sale state of an NFT minted on a pixelboard.
    ///
    /// There are 3 options of a sale state change:
    /// * Putting up for sale\
    /// If an NFT is **not** for sale, then assigning `pixel_price` to [`Some`]
    /// price will transfer it to a pixelboard program & put it up for sale.
    /// * Updating a pixel price\
    /// If an NFT is for sale, then assigning `pixel_price` to [`Some`] price
    /// will update its pixel price.
    /// * Removing from sale\
    /// Assigning the `pixel_price` to [`None`] will transfer an NFT back to its
    /// owner & remove an NFT from sale.
    ///
    /// **Note:** A commission is included in each NFT resale, so a seller
    /// will receive not all fungible tokens but tokens with a commission
    /// deduction. A commission percentage can be obtained by
    /// [`commission_percentage()`](../nft_pixelboard_state/metafns/fn.commission_percentage.html).
    ///
    /// # Requirements
    /// * An NFT must be minted on a pixelboard.
    /// * [`msg::source()`](gstd::msg::source) must be the owner of an NFT.
    /// * `pixel_price` mustn't be more than [`MAX_PIXEL_PRICE`].
    ///
    /// On success, returns [`NFTPixelboardEvent::SaleStateChanged`].
    ChangeSaleState {
        token_id: TokenId,
        /// A price of each pixel that an NFT occupies. To calculate a price of
        /// the entire NFT, see the documentation of
        /// [`TokenInfo#structfield.pixel_price`].
        pixel_price: Option<u128>,
    },

    /// Paints with `painting` an NFT minted on a pixelboard.
    ///
    /// # Requirements
    /// * An NFT must be minted on a pixelboard.
    /// * [`msg::source()`](gstd::msg::source) must be the owner of an NFT.
    /// * `painting` length must equal a pixel count in an NFT. The count can be
    /// calculated by multiplying a [width](`Rectangle::width`) &
    /// [height](`Rectangle::height`) from a rectangle of the NFT. The NFT
    /// rectangle can be obtained by [`token_info()`](../nft_pixelboard_state/metafns/fn.token_info.html).
    ///
    /// On success, returns [`NFTPixelboardEvent::Painted`].
    Paint {
        token_id: TokenId,
        painting: Vec<Color>,
    },
}

/// A result of processed [`NFTPixelboardAction`] in case of successfull execution.
#[derive(Decode, Encode, TypeInfo)]
#[codec(crate = gstd::codec)]
#[scale_info(crate = gstd::scale_info)]
pub enum NFTPixelboardEvent {
    /// Should be returned from [`NFTPixelboardAction::Mint`].
    Minted(TokenId),
    /// Should be returned from [`NFTPixelboardAction::Buy`].
    Bought(TokenId),
    /// Should be returned from [`NFTPixelboardAction::ChangeSaleState`].
    SaleStateChanged(TokenId),
    /// Should be returned from [`NFTPixelboardAction::Paint`].
    Painted(TokenId),
}

/// A result of processed [`NFTPixelboardAction`] in case of failure.
#[derive(Decode, Encode, TypeInfo)]
#[codec(crate = gstd::codec)]
#[scale_info(crate = gstd::scale_info)]
pub enum NFTPixelboardError {
    ZeroWidthOrHeight,
    ZeroAddress,
    ZeroBlockSideLength,
    WrongResolution,
    WrongCommissionPercentage,
    WrongPaintingLength,
    PixelPriceExceeded,
    NFTNotFoundById,
    NFTNotFountByRectangle,
    NFTIsNotOnSale,
    NotOwner,
    CoordinatesNotObserveBlockLayout,
    CoordinatesWithWrongCorners,
    CoordinatesOutOfCanvas,
    CoordinatesCollision,
    PreviousTxMustBeCompleted,
    NFTTransferFailed,
    FTokensTransferFailed,
    NFTMintFailed,
}