/
internal.rs
235 lines (206 loc) · 9.61 KB
/
internal.rs
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
use crate::*;
use near_sdk::{CryptoHash};
use std::mem::size_of;
//convert the royalty percentage and amount to pay into a payout
pub(crate) fn royalty_to_payout(royalty_percentage: u128, amount_to_pay: u128) -> U128 {
U128(amount_to_pay.saturating_mul(royalty_percentage).saturating_div(10000))
}
//calculate how many bytes the account ID is taking up
pub(crate) fn bytes_for_approved_account_id(account_id: &AccountId) -> u128 {
// The extra 4 bytes are coming from Borsh serialization to store the length of the string.
account_id.as_str().len() as u128 + 4 + size_of::<u128>() as u128
}
//refund the storage taken up by passed in approved account IDs and send the funds to the passed in account ID.
pub(crate) fn refund_approved_account_ids_iter<'a, I>(
account_id: AccountId,
approved_account_ids: I, //the approved account IDs must be passed in as an iterator
) -> Promise where I: Iterator<Item = &'a AccountId> {
//get the storage total by going through and summing all the bytes for each approved account IDs
let storage_released = approved_account_ids.map(bytes_for_approved_account_id).sum();
//transfer the account the storage that is released
Promise::new(account_id).transfer(env::storage_byte_cost().saturating_mul(storage_released))
}
//refund a map of approved account IDs and send the funds to the passed in account ID
pub(crate) fn refund_approved_account_ids(
account_id: AccountId,
approved_account_ids: &HashMap<AccountId, u32>,
) -> Promise {
//call the refund_approved_account_ids_iter with the approved account IDs as keys
refund_approved_account_ids_iter(account_id, approved_account_ids.keys())
}
//used to generate a unique prefix in our storage collections (this is to avoid data collisions)
pub(crate) fn hash_account_id(account_id: &AccountId) -> CryptoHash {
//get the default hash
let mut hash = CryptoHash::default();
//we hash the account ID and return it
hash.copy_from_slice(&env::sha256(account_id.as_bytes()));
hash
}
//used to make sure the user attached exactly 1 yoctoNEAR
pub(crate) fn assert_one_yocto() {
assert_eq!(
env::attached_deposit(),
NearToken::from_yoctonear(1),
"Requires attached deposit of exactly 1 yoctoNEAR",
)
}
//Assert that the user has attached at least 1 yoctoNEAR (for security reasons and to pay for storage)
pub(crate) fn assert_at_least_one_yocto() {
assert!(
env::attached_deposit() >= NearToken::from_yoctonear(1),
"Requires attached deposit of at least 1 yoctoNEAR",
)
}
//refund the initial deposit based on the amount of storage that was used up
pub(crate) fn refund_deposit(storage_used: u128) {
//get how much it would cost to store the information
let required_cost = env::storage_byte_cost().saturating_mul(storage_used);
//get the attached deposit
let attached_deposit = env::attached_deposit();
//make sure that the attached deposit is greater than or equal to the required cost
assert!(
required_cost.le(&attached_deposit),
"Must attach {} to cover storage",
required_cost,
);
//get the refund amount from the attached deposit - required cost
let refund = attached_deposit.saturating_sub(required_cost);
//if the refund is greater than 1 yocto NEAR, we refund the predecessor that amount
if refund.gt(&ONE_YOCTONEAR) {
Promise::new(env::predecessor_account_id()).transfer(refund);
}
}
impl Contract {
//add a token to the set of tokens an owner has
pub(crate) fn internal_add_token_to_owner(
&mut self,
account_id: &AccountId,
token_id: &TokenId,
) {
//get the set of tokens for the given account
let mut tokens_set = self.tokens_per_owner.get(account_id).unwrap_or_else(|| {
//if the account doesn't have any tokens, we create a new unordered set
UnorderedSet::new(
StorageKey::TokenPerOwnerInner {
//we get a new unique prefix for the collection
account_id_hash: hash_account_id(&account_id),
},
)
});
//we insert the token ID into the set
tokens_set.insert(token_id);
//we insert that set for the given account ID.
self.tokens_per_owner.insert(account_id, &tokens_set);
}
//remove a token from an owner (internal method and can't be called directly via CLI).
pub(crate) fn internal_remove_token_from_owner(
&mut self,
account_id: &AccountId,
token_id: &TokenId,
) {
//we get the set of tokens that the owner has
let mut tokens_set = self
.tokens_per_owner
.get(account_id)
//if there is no set of tokens for the owner, we panic with the following message:
.expect("Token should be owned by the sender");
//we remove the the token_id from the set of tokens
tokens_set.remove(token_id);
//if the token set is now empty, we remove the owner from the tokens_per_owner collection
if tokens_set.is_empty() {
self.tokens_per_owner.remove(account_id);
} else {
//if the token set is not empty, we simply insert it back for the account ID.
self.tokens_per_owner.insert(account_id, &tokens_set);
}
}
//transfers the NFT to the receiver_id (internal method and can't be called directly via CLI).
pub(crate) fn internal_transfer(
&mut self,
sender_id: &AccountId,
receiver_id: &AccountId,
token_id: &TokenId,
//we introduce an approval ID so that people with that approval ID can transfer the token
approval_id: Option<u32>,
memo: Option<String>,
) -> Token {
//get the token object by passing in the token_id
let token = self.tokens_by_id.get(token_id).expect("No token");
//if the sender doesn't equal the owner, we check if the sender is in the approval list
if sender_id != &token.owner_id {
//if the token's approved account IDs doesn't contain the sender, we panic
if !token.approved_account_ids.contains_key(sender_id) {
env::panic_str("Unauthorized");
}
// If they included an approval_id, check if the sender's actual approval_id is the same as the one included
if let Some(enforced_approval_id) = approval_id {
//get the actual approval ID
let actual_approval_id = token
.approved_account_ids
.get(sender_id)
//if the sender isn't in the map, we panic
.expect("Sender is not approved account");
//make sure that the actual approval ID is the same as the one provided
assert_eq!(
actual_approval_id, &enforced_approval_id,
"The actual approval_id {} is different from the given approval_id {}",
actual_approval_id, enforced_approval_id,
);
}
}
//we make sure that the sender isn't sending the token to themselves
assert_ne!(
&token.owner_id, receiver_id,
"The token owner and the receiver should be different"
);
//we remove the token from it's current owner's set
self.internal_remove_token_from_owner(&token.owner_id, token_id);
//we then add the token to the receiver_id's set
self.internal_add_token_to_owner(receiver_id, token_id);
//we create a new token struct
let new_token = Token {
owner_id: receiver_id.clone(),
//reset the approval account IDs
approved_account_ids: Default::default(),
next_approval_id: token.next_approval_id,
//we copy over the royalties from the previous token
royalty: token.royalty.clone(),
};
//insert that new token into the tokens_by_id, replacing the old entry
self.tokens_by_id.insert(token_id, &new_token);
//if there was some memo attached, we log it.
if let Some(memo) = memo.as_ref() {
env::log_str(&format!("Memo: {}", memo).to_string());
}
// Default the authorized ID to be None for the logs.
let mut authorized_id = None;
//if the approval ID was provided, set the authorized ID equal to the sender
if approval_id.is_some() {
authorized_id = Some(sender_id.to_string());
}
// Construct the transfer log as per the events standard.
let nft_transfer_log: EventLog = EventLog {
// Standard name ("nep171").
standard: NFT_STANDARD_NAME.to_string(),
// Version of the standard ("nft-1.0.0").
version: NFT_METADATA_SPEC.to_string(),
// The data related with the event stored in a vector.
event: EventLogVariant::NftTransfer(vec![NftTransferLog {
// The optional authorized account ID to transfer the token on behalf of the old owner.
authorized_id,
// The old owner's account ID.
old_owner_id: token.owner_id.to_string(),
// The account ID of the new owner of the token.
new_owner_id: receiver_id.to_string(),
// A vector containing the token IDs as strings.
token_ids: vec![token_id.to_string()],
// An optional memo to include.
memo,
}]),
};
// Log the serialized json.
env::log_str(&nft_transfer_log.to_string());
//return the previous token object that was transferred.
token
}
}