-
Notifications
You must be signed in to change notification settings - Fork 0
/
AENSWrapping.aes
681 lines (590 loc) · 34.4 KB
/
AENSWrapping.aes
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
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
@compiler >= 7
include "./interfaces/IAEX141.aes"
include "./interfaces/IAEX141NFTReceiver.aes"
include "./interfaces/IAENSWrapping.aes"
include "Pair.aes"
main contract AENSWrapping : IAEX141, IAENSWrapping =
datatype metadata_type = URL | OBJECT_ID | MAP
datatype metadata = MetadataIdentifier(string) | MetadataMap(map(string, string))
record meta_info =
{ name: string
, symbol: string
, base_url: option(string)
, metadata_type : metadata_type }
record nft_data =
{ id: int
, owner: address
, owner_config: option(config)
, names: list(string)
, expiration_height: int }
record config =
{ reward: int
, reward_block_window: int
, emergency_reward: int
, emergency_reward_block_window: int
, can_receive_from_others: bool
, burnable_if_empty: bool }
record state =
{ owner: address
, meta_info: meta_info
, token_to_owner: map(int, address)
, owner_to_tokens: map(address, Set.set(int))
, name_to_token: map(string, int)
, token_to_name_expiration: map(int, Chain.ttl)
, token_to_config: map(int, config)
, account_to_config: map(address, config)
, account_to_reward_pool: map(address, int)
, max_name_ttl: Chain.ttl
, balances: map(address, int)
, approvals: map(int, address)
, operators: map(address, map(address, bool))
, metadata: map(int, metadata)
, total_supply: int
, counter: int }
datatype event
= Transfer(address, address, int)
| Approval(address, address, int, string)
| ApprovalForAll(address, address, string)
// extension "mintable"
| Mint(address, int)
// extension "burnable"
| Burn(address, int)
// AENS Wrapping
// name, nft_id, owner, new_ttl
| NameWrap(string, int, address, int)
// name, nft_id, current_owner, recipient
| NameUnwrap(string, int, address, address)
// name, nft_id, new_ttl, caller
| NameExtend(string, int, int, address)
// name, nft_id_old, nft_id_new
| NameTransfer(string, int, int)
// nft_id, caller, reward
| Reward(int, address, int)
stateful entrypoint init() =
{ owner = Contract.creator,
meta_info = { name = "Wrapped AENS", symbol = "WAENS", base_url = None, metadata_type = MAP },
token_to_owner = {},
owner_to_tokens = {},
name_to_token = {},
token_to_name_expiration = {},
token_to_config = {},
account_to_config = {},
account_to_reward_pool = {},
max_name_ttl = RelativeTTL(180_000),
balances = {},
approvals = {},
operators = {},
metadata = {},
total_supply = 0,
counter = 1 }
entrypoint aex141_extensions() : list(string) =
["mintable", "burnable"]
entrypoint meta_info() : meta_info =
state.meta_info
entrypoint metadata(token_id: int) : option(metadata) =
Map.lookup(token_id, state.metadata)
entrypoint total_supply() : int =
state.total_supply
entrypoint balance(owner: address) : option(int) =
Map.lookup(owner, state.balances)
entrypoint owner(token_id: int) : option(address) =
Map.lookup(token_id, state.token_to_owner)
stateful entrypoint transfer(to: address, token_id: int, data: option(string)) =
let from = require_authorized(token_id)
require(from != to, "SENDER_MUST_NOT_BE_RECEIVER")
__remove_approval(token_id)
put(state{ balances[from] @balance1 = balance1 - 1
, balances[to = 0] @balance2 = balance2 + 1
, token_to_owner[token_id] = to
, owner_to_tokens[from] @from_tokens = Set.delete(token_id, from_tokens)
, owner_to_tokens[to = Set.new()] @to_tokens = Set.insert(token_id, to_tokens)
, token_to_config @token_config = Map.delete(token_id, token_config) })
switch(invoke_nft_receiver(Some(from), to, token_id, data))
(true, false) => abort("SAFE_TRANSFER_FAILED")
_ => Chain.event(Transfer(from, to, token_id))
stateful entrypoint transfer_to_contract(token_id: int) =
let to = Call.caller
require(Address.is_contract(to), "CALLER_MUST_BE_A_CONTRACT")
let from = require_authorized(token_id)
require(from != to, "SENDER_MUST_NOT_BE_RECEIVER")
__remove_approval(token_id)
put(state{ balances[from] @balance1 = balance1 - 1
, balances[to = 0] @balance2 = balance2 + 1
, token_to_owner[token_id] = to
, owner_to_tokens[from] @from_tokens = Set.delete(token_id, from_tokens)
, owner_to_tokens[to = Set.new()] @to_tokens = Set.insert(token_id, to_tokens) })
Chain.event(Transfer(from, to, token_id))
stateful entrypoint approve(approved: address, token_id: int, enabled: bool) =
require_authorized(token_id)
if(enabled)
put(state{approvals[token_id] = approved})
else
__remove_approval(token_id)
Chain.event(Approval(Call.caller, approved, token_id, bool_to_string(enabled)))
stateful entrypoint approve_all(operator: address, enabled: bool) =
put(state{operators @ ops = ops{[Call.caller = {}] @ op = op{[operator] = enabled}}})
Chain.event(ApprovalForAll(Call.caller, operator, bool_to_string(enabled)))
entrypoint get_approved(token_id: int) : option(address) =
Map.lookup(token_id, state.approvals)
entrypoint is_approved(token_id: int, a: address) : bool =
switch(Map.lookup(token_id, state.approvals))
None => false
Some(o) => o == a
entrypoint is_approved_for_all(owner: address, operator: address) : bool =
switch(Map.lookup(owner, state.operators))
None => false
Some(ops) =>
switch(Map.lookup(operator, ops))
None => false
Some(v) => v
// mintable extension
stateful entrypoint mint(to: address, metadata: option(metadata), data: option(string)) : int =
switch(metadata)
None =>
let token_id = __mint(to, data)
// init the name expiration to sync for the NFT
let name_expiration_height = to_fixed_ttl(state.max_name_ttl)
put(state{ token_to_name_expiration[token_id] @name_expiration = FixedTTL(name_expiration_height) })
put(state{ metadata[token_id] @nft_metadata = MetadataMap({}) })
token_id
Some(_) => abort("MINTING_WITH_METADATA_NOT_ALLOWED")
// burnable extension
stateful entrypoint burn(token_id: int) =
__burn_single(token_id)
// AENSWrapping
/// @notice returns the account address of the real owner
/// @param name the name to lookup
/// @return real owner
entrypoint resolve_owner(name: string) : option(address) =
switch(Map.lookup(name, state.name_to_token))
Some(nft_id) =>
Map.lookup(nft_id, state.token_to_owner)
None => None
/// @notice returns the nft id where the AENS name is wrapped into
/// @param name the name to lookup
/// @return nft_id
entrypoint resolve_nft_id(name: string) : option(int) =
Map.lookup(name, state.name_to_token)
/// @notice returns the nft id where the AENS name is wrapped into as well as the real owner of the name
/// @param name the name to lookup
/// @return nft_id and owner
entrypoint resolve_nft_id_and_owner(name: string) : option(int * address) =
switch(resolve_nft_id(name))
Some(nft_id) =>
let Some(owner) = Map.lookup(nft_id, state.token_to_owner)
Some((nft_id, owner))
None => None
/// @notice returns nft data (id, owner and the list of names wrapped into the nft and the expiration_height)
/// @param nft_id the NFT id
/// @return nft_data
entrypoint get_nft_data(nft_id: int) : option(nft_data) =
switch(owner(nft_id))
None => None
Some(owner) =>
let Some(MetadataMap(nft_metadata_map)) = Map.lookup(nft_id, state.metadata)
let names = List.map(Pair.fst, Map.to_list(nft_metadata_map))
let Some(FixedTTL(expiration_height)) = Map.lookup(nft_id, state.token_to_name_expiration)
let config = get_nft_config(nft_id, owner)
Some({ id = nft_id, owner = owner, owner_config = config, names = names, expiration_height = expiration_height })
/// @notice returns the expiration height of names that are wrapped into the provided nft id
/// @param nft_id the NFT id
/// @return the (fixed) height where all names wrapped into the NFT will expire OR None if the NFT does not exist
entrypoint get_expiration_by_nft_id(nft_id: int) : option(int) =
switch(Map.lookup(nft_id, state.token_to_name_expiration))
None => None
Some(v) =>
let FixedTTL(height) = v
Some(height)
/// @notice returns the global config for an account
/// @param account the account address to lookup the config for
/// @return the global config for an account OR None if not set
entrypoint get_global_config(account: address) : option(config) =
Map.lookup(account, state.account_to_config)
/// @notice returns the balance included in the reward pool
/// @param account the account where the reward pool balance should be looked up for
/// @return the reward pool balance
entrypoint get_reward_pool(account: address) : int =
state.account_to_reward_pool[account = 0]
/// @notice wraps AENS names into a fresh minted NFT, adds NFT metadata, extends all names
/// @param names_delegation_sigs a map (key = AENS name, value = delegation signature)
/// @return the NFT id
stateful entrypoint wrap_and_mint(names_delegation_sigs: map(string, signature)) : int =
let token_id = __mint(Call.caller, None)
let names_delegation_sigs_list: list(string * signature) = Map.to_list(names_delegation_sigs)
let name_expiration_height = to_fixed_ttl(state.max_name_ttl)
List.foreach(names_delegation_sigs_list, (val) => __claim_and_assign(val, token_id, name_expiration_height))
put(state{ token_to_name_expiration[token_id] @name_expiration = FixedTTL(name_expiration_height) })
token_id
/// @notice wraps a single AENS name into an existing NFT, adds NFT metadata, updates expiry of name to match expiry of already wrapped names
/// @param nft_id the id of the NFT to wrap the AENS name into
/// @param name the AENS name to wrap
/// @param delegation_sig the delegation signature for the name
stateful entrypoint wrap_single(nft_id: int, name: string, delegation_sig: signature) =
require_authorized(nft_id)
let expiration_height = require_not_expired(nft_id)
__claim_and_assign((name, delegation_sig), nft_id, expiration_height)
/// @notice wraps multiple AENS names into an existing NFT, adds NFT metadata, updates expiry of names to match expiry of already wrapped names
/// @param nft_id the id of the NFT to wrap the AENS name into
/// @param names_delegation_sigs a map (key = AENS name, value = delegation signature)
stateful entrypoint wrap_multiple(nft_id: int, names_delegation_sigs: map(string, signature)) =
require_authorized(nft_id)
let expiration_height = require_not_expired(nft_id)
let names_delegation_sigs_list: list(string * signature) = Map.to_list(names_delegation_sigs)
List.foreach(names_delegation_sigs_list, (val) => __claim_and_assign(val, nft_id, expiration_height))
/// @notice adds / replaces a pointer of the AENS name while keeping existing pointers
/// @param nft_id the id of the NFT where the AENS name is wrapped into
/// @param name the AENS name where the pointer will be added / replaced
/// @param pointer_key the key of the pointer
/// @param pointer_value the object to point to (account, channel, contract, oracle)
stateful entrypoint add_pointer(nft_id: int, name: string, pointer_key: string, pointer_value: AENS.pointee) =
// TODO
()
/// @notice adds / replaces a set of pointers of the AENS name
/// @param nft_id the id of the NFT where the AENS name is wrapped into
/// @param name the AENS name where the pointer will be added / replaced
/// @param pointers a map of pointers to set
/// @param keep_existing a bool indicating whether to keep existing pointers or not
stateful entrypoint add_pointers(nft_id: int, name: string, pointers: map(string, AENS.pointee), keep_existing: bool) =
// TODO
()
/// @notice removes a pointer of the AENS name
/// @param nft_id the id of the NFT where the AENS name is wrapped into
/// @param name the AENS name where the pointer will be removed
/// @param pointer_key the key of the pointer
stateful entrypoint remove_pointer(nft_id: int, name: string, pointer_key: string) =
// TODO
()
/// @notice removes multiple pointers of the AENS name
/// @param nft_id the id of the NFT where the AENS name is wrapped into
/// @param name the AENS name where the pointers will be removed
/// @param pointer_keys a set of pointer keys
stateful entrypoint remove_pointers(nft_id: int, name: string, pointer_keys: Set.set(string)) =
// TODO
()
/// @notice removes all pointers of the AENS name
/// @param nft_id the id of the NFT where the AENS name is wrapped into
/// @param name the AENS name where the pointers will be removed
stateful entrypoint remove_all_pointers(nft_id: int, name: string) =
// TODO
()
/// @notice revokes a single AENS name wrapped in the NFT, removes metadata
/// @param nft_id the id of the NFT where the AENS name is wrapped into
/// @param name the AENS name to revoke
stateful entrypoint revoke_single(nft_id: int, name: string) =
// TODO
()
/// @notice revokes multiple AENS names wrapped in the NFT, removes metadata
/// @param nft_id the id of the NFT where the AENS name is wrapped into
/// @param names the AENS names to revoke
stateful entrypoint revoke_multiple(nft_id: int, names: Set.set(string)) =
// TODO
()
/// @notice revokes all AENS names wrapped in the NFT, removes metadata and burns the NFT
/// @param nft_id the id of the NFT where the AENS name is wrapped into
stateful entrypoint revoke_all(nft_id: int) =
// TODO
()
/// @notice transfers a single AENS name to another NFT by updating metadata of both NFTs, updates expiry of name to match expiry of already wrapped names
/// @param nft_id_old the id of the NFT that currently wraps the AENS name
/// @param nft_id_new the id of the NFT that will wrap the AENS name in the future
/// @param name the AENS name to transfer
stateful entrypoint transfer_single(nft_id_old: int, nft_id_new: int, name: string) =
let current_owner = require_authorized(nft_id_old)
let target_owner = require_exists(nft_id_new)
require_not_expired(nft_id_old)
let new_expiration_height = require_not_expired(nft_id_new)
if(current_owner != target_owner)
require_can_receive_name(nft_id_new, target_owner)
__transfer_single_common(nft_id_old, nft_id_new, name, new_expiration_height)
/// @notice transfers multiple AENS names to another NFT by updating metadata of both NFTs, updates expiry of names to match expiry of already wrapped names
/// @param nft_id_old the id of the NFT that currently wraps the AENS name
/// @param nft_id_new the id of the NFT that will wrap the AENS name in the future
/// @param names the AENS names to transfer
stateful entrypoint transfer_multiple(nft_id_old: int, nft_id_new: int, names: Set.set(string)) =
let current_owner = require_authorized(nft_id_old)
let target_owner = require_exists(nft_id_new)
require_not_expired(nft_id_old)
let new_expiration_height = require_not_expired(nft_id_new)
if(current_owner != target_owner)
require_can_receive_name(nft_id_new, target_owner)
List.foreach(Set.to_list(names), (n) => __transfer_single_common(nft_id_old, nft_id_new, n, new_expiration_height))
/// @notice transfers a single AENS name to another NFT by updating metadata of both NFTs, updates expiry of names to match expiry of already wrapped names, burns the old NFT
/// @param nft_id_old the id of the NFT that currently wraps the AENS name
/// @param nft_id_new the id of the NFT that will wrap the AENS name in the future
stateful entrypoint transfer_all(nft_id_old: int, nft_id_new: int) =
require_authorized(nft_id_old)
require_not_expired(nft_id_old)
require_exists(nft_id_new)
let nft_metadata_map = require_names_wrapped(nft_id_old)
require(Map.lookup(nft_id_new, state.token_to_owner) != None, "RECIPIENT_TOKEN_NOT_EXISTS")
let new_expiration_height = require_not_expired(nft_id_new)
List.foreach(Map.to_list(nft_metadata_map), (val) => __transfer_single_common(nft_id_old, nft_id_new, Pair.fst(val), new_expiration_height))
/// @notice transfers a single AENS name to another NFT by requiring explicit authorization of the recipient via delegation signature. can be used if can_receive_from_others is false
/// @param nft_id_old the id of the NFT that currently wraps the AENS name
/// @param nft_id_new the id of the NFT that will wrap the AENS name in the future
/// @param name the AENS name to transfer
/// @param delegation_sig the delegation signature for the name provided by the owner of nft_id_new
stateful entrypoint transfer_single_authorized(nft_id_old: int, nft_id_new: int, name: string, delegation_sig: signature) =
require_authorized(nft_id_old)
require_not_expired(nft_id_old)
let target_owner = require_exists(nft_id_new)
let new_expiration_height = require_not_expired(nft_id_new)
__transfer_single_authorized(nft_id_old, nft_id_new, name, new_expiration_height, target_owner, delegation_sig)
/// @notice transfers multiple AENS names to another NFT by requiring explicit authorization of the recipient via delegation signatures. can be used if can_receive_from_others is false
/// @param nft_id_old the id of the NFT that currently wraps the AENS name
/// @param nft_id_new the id of the NFT that will wrap the AENS name in the future
/// @param names_delegation_sigs a map (key = AENS name, value = delegation signature)
stateful entrypoint transfer_multiple_authorized(nft_id_old: int, nft_id_new: int, names_delegation_sigs: map(string, signature)) =
let current_owner = require_authorized(nft_id_old)
let target_owner = require_exists(nft_id_new)
require_not_expired(nft_id_old)
let new_expiration_height = require_not_expired(nft_id_new)
List.foreach(Map.to_list(names_delegation_sigs), (val) => __transfer_single_authorized(nft_id_old, nft_id_new, Pair.fst(val), new_expiration_height, target_owner, Pair.snd(val)))
/// @notice transfers the AENS name back to the owner or to another defined recipient, updates metadata
/// @param nft_id the id of the NFT that currently wraps the AENS name
/// @param name the AENS name to transfer
/// @param recipient the address that should receive the AENS name
stateful entrypoint unwrap_single(nft_id: int, name: string, recipient: option(address)) =
let current_owner = require_authorized(nft_id)
require_not_expired(nft_id)
__unwrap(nft_id, name, current_owner, recipient)
/// @notice transfers the AENS names back to the owner or to another defined recipient, updates metadata
/// @param nft_id the id of the NFT that currently wraps the AENS name
/// @param names the AENS names to transfer
/// @param recipient the address that should receive the AENS name
stateful entrypoint unwrap_multiple(nft_id: int, names: Set.set(string), recipient: option(address)) =
let current_owner = require_authorized(nft_id)
require_not_expired(nft_id)
List.foreach(Set.to_list(names), (val) => __unwrap(nft_id, val, current_owner, recipient))
/// @notice transfers all AENS names back to the owner or to another defined recipient, updates metadata, burns the NFT
/// @param nft_id the id of the NFT that currently wraps the AENS name
/// @param recipient the address that should receive the AENS name
stateful entrypoint unwrap_all(nft_id: int, recipient: option(address)) =
let current_owner = require_authorized(nft_id)
require_not_expired(nft_id)
let nft_metadata_map = require_names_wrapped(nft_id)
List.foreach(Map.to_list(nft_metadata_map), (val) => __unwrap(nft_id, Pair.fst(val), current_owner, recipient))
/// @notice caller sets global config for NFTs owned by the caller
/// @param config the global config
stateful entrypoint set_global_config(config: config) =
put(state{ account_to_config[Call.caller] @cfg = config })
/// @notice caller removes global config for NFTs owned by the caller
stateful entrypoint remove_global_config() =
put(state{ account_to_config @ val = Map.delete(Call.caller, val) })
/// @notice caller sets NFT specific config
/// @param nft_id the id of the NFT to set the config
/// @param config the nft specific config
stateful entrypoint set_nft_config(nft_id: int, config: config) =
require_authorized(nft_id)
put(state{ token_to_config[nft_id] @cfg = config })
/// @notice caller removes NFT specific config
/// @param nft_id the id of the NFT to remove the config from
stateful entrypoint remove_nft_config(nft_id: int) =
require_authorized(nft_id)
put(state{ token_to_config @val = Map.delete(nft_id, val) })
/// @notice caller deposits AE to his reward pool
stateful payable entrypoint deposit_to_reward_pool() =
require(Call.value > 0, "DEPOSIT_VALUE_MISSING")
put(state{ account_to_reward_pool[Call.caller = 0] @reward_pool = reward_pool + Call.value })
/// @notice caller withdraws all AE or a specific amount from his reward pool
/// @param amount the optional amount of AE to withdraw
stateful entrypoint withdraw_from_reward_pool(amount: option(int)) =
let reward_pool = require_ae_in_reward_pool()
switch(amount)
None =>
put(state{ account_to_reward_pool @val = Map.delete(Call.caller, val) })
Chain.spend(Call.caller, reward_pool)
Some(value) =>
require(reward_pool >= value, "INSUFFICIENT_BALANCE_IN_POOL")
put(state{ account_to_reward_pool[Call.caller] @reward_pool = reward_pool - value })
Chain.spend(Call.caller, value)
/// @notice calculates the reward based on the expiration date of the names wrapped in an NFT, considering the global config (or the NFT specific config if set) and the amount of Æ deposited by the NFT owner
/// @param nft_id the id of the NFT to extend
/// @return the estimated reward
entrypoint estimate_reward(nft_id: int) =
let owner = require_exists(nft_id)
let expiration_height = require_not_expired(nft_id)
switch(get_nft_config(nft_id, owner))
None => 0
Some(config) => get_possible_reward(owner, config, expiration_height)
/// @notice extends all AENS names wrapped in the NFT without getting a reward
/// @param nft_id the id of the NFT that wraps the AENS names to extend
stateful entrypoint extend_all(nft_id: int) =
require_exists(nft_id)
require_not_expired(nft_id)
let nft_metadata_map = require_names_wrapped(nft_id)
let new_expiration_height = to_fixed_ttl(state.max_name_ttl)
List.foreach(Map.to_list(nft_metadata_map), (val) => __extend(nft_id, Pair.fst(val), new_expiration_height))
put(state{ token_to_name_expiration[nft_id] @name_expiration = FixedTTL(new_expiration_height) })
/// @notice extends all AENS names wrapped in the NFT and distributes a reward to the caller
/// @param nft_id the id of the NFT that wraps the AENS names to extend
stateful entrypoint extend_all_for_reward(nft_id: int) =
let owner = require_exists(nft_id)
let expiration_height = require_not_expired(nft_id)
let nft_metadata_map = require_names_wrapped(nft_id)
let reward =
switch(get_nft_config(nft_id, owner))
None => 0
Some(config) => get_possible_reward(owner, config, expiration_height)
// TODO => shall we allow 0 reward?!
let new_expiration_height = to_fixed_ttl(state.max_name_ttl)
List.foreach(Map.to_list(nft_metadata_map), (val) => __extend(nft_id, Pair.fst(val), new_expiration_height))
put(state{ token_to_name_expiration[nft_id] @name_expiration = FixedTTL(new_expiration_height) })
put(state{ account_to_reward_pool[owner = 0] @reward_pool = reward_pool - reward })
Chain.spend(Call.caller, reward)
Chain.event(Reward(nft_id, Call.caller, reward))
/// @notice transfers a set of NFTs to the desired recipient
/// @param recipient the address to become new owner of the NFTs
/// @param nft_ids the ids of the NFTs to transfer
stateful entrypoint transfer_multiple_nfts(recipient: address, nft_ids: Set.set(int)) =
// TODO
()
/// @notice burns a set of NFTs (only possible if AENS names are expired)
/// @param nft_ids the ids of the NFTs to burn
stateful entrypoint burn_multiple_nfts(nft_ids: Set.set(int)) =
List.foreach(Set.to_list(nft_ids), (id) => __burn_single(id))
// external helpers for AEX-141
entrypoint get_owned_tokens(owner: address) : list(int) =
Set.to_list(Map.lookup_default(owner, state.owner_to_tokens, Set.new()))
// internal helper functions
function require_expired_if_wrapped(nft_id: int) =
let Some(MetadataMap(nft_metadata_map)) = Map.lookup(nft_id, state.metadata)
if(Map.size(nft_metadata_map) > 0)
let Some(FixedTTL(expiration_height)) = Map.lookup(nft_id, state.token_to_name_expiration)
require(Chain.block_height > expiration_height, "WRAPPED_NAMES_NOT_EXPIRED")
function require_can_receive_name(target_nft_id: int, target_owner: address) =
switch(get_nft_config(target_nft_id, target_owner))
None => abort("RECEIVING_NAME_NOT_ALLOWED")
Some(cfg) => require(cfg.can_receive_from_others, "RECEIVING_NAME_NOT_ALLOWED")
function require_ae_in_reward_pool() : int =
let reward_pool = Map.lookup_default(Call.caller, state.account_to_reward_pool, 0)
require(reward_pool > 0, "NO_AE_IN_REWARD_POOL")
reward_pool
function require_names_wrapped(nft_id: int) : map(string, string) =
let Some(MetadataMap(nft_metadata_map)) = Map.lookup(nft_id, state.metadata)
require(Map.size(nft_metadata_map) > 0, "NO_NAMES_WRAPPED")
nft_metadata_map
function require_name_wrapped(nft_id: int, name: string) : map(string, string) =
let Some(MetadataMap(nft_metadata_map)) = Map.lookup(nft_id, state.metadata)
require(Map.member(name, nft_metadata_map), "NAME_NOT_WRAPPED")
nft_metadata_map
function require_not_expired(nft_id: int) : int =
let Some(FixedTTL(expiration_height)) = Map.lookup(nft_id, state.token_to_name_expiration)
require(expiration_height >= Chain.block_height, "NAMES_IN_NFT_EXPIRED")
expiration_height
function require_caller_is_receipient(recipient: address) =
require(Call.caller == recipient, "CALLER_MUST_BE_RECIPIENT")
function require_exists(token_id: int) : address =
switch(owner(token_id))
None => abort("TOKEN_NOT_EXISTS")
Some(v) => v
function require_authorized(token_id: int) : address =
let owner = require_exists(token_id)
require(Call.caller == owner || is_approved(token_id, Call.caller) || is_approved_for_all(owner, Call.caller), "ONLY_OWNER_APPROVED_OR_OPERATOR_CALL_ALLOWED")
owner
stateful function __extend(nft_id: int, name: string, new_expiration_height: int) =
AENS.update(Contract.address, name, Some(FixedTTL(new_expiration_height)), None, None)
Chain.event(NameExtend(name, nft_id, new_expiration_height, Call.caller))
stateful function __transfer_single_authorized(nft_id_old: int, nft_id_new: int, name: string, new_expiration_height: int, owner_target_nft: address, delegation_sig: signature) =
AENS.transfer(Contract.address, owner_target_nft, name)
AENS.transfer(owner_target_nft, Contract.address, name, signature = delegation_sig)
__transfer_single_common(nft_id_old, nft_id_new, name, new_expiration_height)
stateful function __transfer_single_common(nft_id_old: int, nft_id_new: int, name: string, new_expiration_height: int) =
require_name_wrapped(nft_id_old, name)
// update state of old nft
let Some(MetadataMap(metadata_map_old_nft)) = Map.lookup(nft_id_old, state.metadata)
put(state{ metadata[nft_id_old] @nft_metadata = MetadataMap(Map.delete(name, metadata_map_old_nft)) })
// update state of new nft
let Some(MetadataMap(metadata_map_new_nft)) = Map.lookup(nft_id_new, state.metadata)
let updated_map_new_nft = metadata_map_new_nft{[name] @ n = ""}
put(state{ metadata[nft_id_new] @nft_metadata = MetadataMap(updated_map_new_nft) })
// update the ttl of the transfered name and keep in sync with expiration height of new nft
__update_name_ttl(name, FixedTTL(new_expiration_height))
// update map for name resolving
put(state{ name_to_token[name] @token_id = nft_id_new })
Chain.event(NameTransfer(name, nft_id_old, nft_id_new))
stateful function __unwrap(nft_id: int, name: string, current_owner, recipient: option(address)) =
let nft_metadata_map = require_name_wrapped(nft_id, name)
put(state{ metadata[nft_id] @nft_metadata = MetadataMap(Map.delete(name, nft_metadata_map)) })
put(state{ name_to_token @ val = Map.delete(name, val) })
let new_owner =
switch(recipient)
None => current_owner
Some(v) => v
AENS.transfer(Contract.address, new_owner, name)
Chain.event(NameUnwrap(name, nft_id, current_owner, new_owner))
stateful function __claim_and_assign(name_delegation_sig: string * signature, token_id: int, expiration_height: int) =
let name = Pair.fst(name_delegation_sig)
AENS.transfer(Call.caller, Contract.address, name, signature = Pair.snd(name_delegation_sig))
let MetadataMap(nft_metadata_map) = Map.lookup_default(token_id, state.metadata, MetadataMap({}))
// adding name to the MetadataMap (value is irrelevant)
let updated_map = nft_metadata_map{[name] @ n = ""}
put(state{ metadata[token_id] @nft_metadata = MetadataMap(updated_map) })
put(state{ name_to_token[name] @t = token_id })
__update_name_ttl(name, FixedTTL(expiration_height))
Chain.event(NameWrap(name, token_id, Call.caller, expiration_height))
/// @notice updates TTL of an AENS name
/// @param name the name to update
/// @param new_ttl the optional ttl to set
stateful function __update_name_ttl(name: string, new_ttl: Chain.ttl) =
AENS.update(Contract.address, name, Some(new_ttl), None, None)
stateful function __mint(to: address, data: option(string)) : int =
require_caller_is_receipient(to)
let token_id = state.counter
put(state{ counter = state.counter + 1
, total_supply = state.total_supply + 1
, balances[to = 0] @balance = balance + 1
, token_to_owner[token_id] = to
, owner_to_tokens[to = Set.new()] @owners_tokens = Set.insert(token_id, owners_tokens)})
switch(invoke_nft_receiver(None, to, token_id, data))
(true, false) => abort("SAFE_TRANSFER_FAILED")
_ => Chain.event(Mint(to, token_id))
token_id
stateful function __remove_approval(token_id: int) =
if(Map.member(token_id, state.approvals))
put(state{ approvals = Map.delete(token_id, state.approvals) })
stateful function __burn_single(token_id: int) =
let owner = require_authorized(token_id)
require_expired_if_wrapped(token_id)
__remove_approval(token_id)
put(state{ balances[owner] @balance = balance - 1
, total_supply = state.total_supply - 1
, token_to_owner = Map.delete(token_id, state.token_to_owner)
, owner_to_tokens[owner] @owners_tokens = Set.delete(token_id, owners_tokens)
, token_to_name_expiration = Map.delete(token_id, state.token_to_name_expiration)
, token_to_config = Map.delete(token_id, state.token_to_config)
, metadata = Map.delete(token_id, state.metadata) })
Chain.event(Burn(owner, token_id))
function get_possible_reward(owner: address, owner_cfg : config, expiration_height: int) : int =
let delta = expiration_height - Chain.block_height
if(delta > owner_cfg.reward_block_window)
0
else
let expected_reward =
switch(delta > owner_cfg.emergency_reward_block_window)
true => owner_cfg.reward
false => owner_cfg.emergency_reward
let reward_pool = state.account_to_reward_pool[owner = 0]
if(reward_pool >= expected_reward)
expected_reward
else
reward_pool
function get_nft_config(nft_id: int, owner: address) : option(config) =
switch(Map.lookup(nft_id, state.token_to_config))
None => get_global_config(owner)
Some(cfg) => Some(cfg)
function to_fixed_ttl(relative_ttl: Chain.ttl) : int =
let RelativeTTL(ttl) = relative_ttl
Chain.block_height + ttl
function invoke_nft_receiver(from: option(address), to: address, token_id: int, data: option(string)) : (bool * bool) =
if(Address.is_contract(to))
let c = Address.to_contract(to)
switch(c.on_aex141_received(from, token_id, data, protected = true) : option(bool))
None => (true, false)
Some(val) => (true, val)
else
(false, false)
function bool_to_string(v: bool): string =
switch(v)
true => "true"
false => "false"