Account Name or Address / Txn Hash or Version / Block Height or Version

Object

Supervillain Labs

0 APT

Balance

sidekick


Code

The source code is plain text uploaded by the deployer, which can be different from the actual bytecode.

1module supervlabs::sidekick {
2    use apto_orm::orm_class;
3    use apto_orm::orm_creator;
4    use apto_orm::orm_module;
5    use apto_orm::orm_object;
6    use aptos_framework::object::{Self, Object, ConstructorRef};
7    use aptos_framework::timestamp;
8    use aptos_token_objects::property_map;
9    use aptos_token_objects::token;
10    use std::error;
11    use std::option::{Self, Option};
12    use std::signer;
13    use std::string::{Self, String};
14    use std::vector;
15
16    use aptos_framework::randomness;
17
18    use supervlabs::gacha_rounds;
19    use supervlabs::gacha_item;
20    use supervlabs::sidekick_capsule;
21    use supervlabs::random;
22    use supervlabs::gacha_supply;
23
24    const CLASS_NAME: vector<u8> = b"Sidekick";
25    const ENOT_SIDEKICK_OBJECT: u64 = 4;
26    const EINVALID_AMOUNT: u64 = 5;
27    const EUNABLE_TO_MINT_TARGET_SIDEKICK_DUE_TO_LIMITED_SUPPLY: u64 = 6;
28    const EINVALID_RANDBOX_NUMBER: u64 = 7;
29    const EREACH_TO_MAX_SUPPLY: u64 = 8;
30    const EINVALID_SIDEKICK_INDEX: u64 = 9;
31    const EDEPRECATED_FUNCTION: u64 = 10;
32
33    const SIDEKICK_DRAW_SUPPLY_INDEX: u64 = 0;
34
35    #[resource_group_member(group = aptos_framework::object::ObjectGroup)]
36    struct Sidekick has key, drop {
37        updated_at: u64,
38        salt: u64,
39    }
40
41    #[resource_group_member(group = aptos_framework::object::ObjectGroup)]
42    struct SidekickMintRequest has key, drop {
43        request_id: String,
44    }
45
46    fun init_module(package: &signer) {
47        let class_address = orm_class::update_class_as_collection<Sidekick>(
48            package,
49            string::utf8(b"SuperV Sidekicks"),
50            true, true, true, true, false, true, false,
51            string::utf8(b"https://public.vir.supervlabs.io/virweb/nft/sidekicks/collection.png"),
52            string::utf8(b"Sidekicks, as faithful allies of the Villains, stand by their side and help maximize the Villains' potential. They typically inhabit the wild before being captured by Villains. By establishing a deep connection, they are reborn as true Sidekicks."),
53            0,
54            true,
55            true,
56            @0x0,
57            100,
58            5,
59        );
60        orm_module::set<Sidekick>(
61            package,
62            signer::address_of(package),
63            class_address,
64        );
65        let pa = signer::address_of(package);
66        let orm_class_obj = object::address_to_object<orm_class::OrmClass>(class_address);
67        if (!gacha_rounds::exists_at(class_address)) {
68            let class_signer = orm_class::load_class_signer(package, orm_class_obj);
69            let grades = vector[
70                string::utf8(b"legendary"),
71                string::utf8(b"epic"),
72                string::utf8(b"rare"),
73                string::utf8(b"uncommon"),
74            ];
75            gacha_rounds::add(&class_signer, pa, grades, vector[
76                100000, 800000, 5000000, 30000000], 100000000); // 0.1%, 0.8%, 5%, 30%, etc.
77        };
78
79        if (!gacha_supply::exists_at(class_address) && orm_class::has_class_signer(orm_class_obj)) {
80            let class_signer = orm_class::load_class_signer(package, orm_class_obj);
81            let (current_round, _, _, _) = gacha_rounds::get_probabilities(class_address);
82            gacha_supply::init(&class_signer, vector[
83                // SIDEKICK_DRAW_SUPPLY_INDEX = 0
84                gacha_supply::new_supply(
85                    string::utf8(b"legendary_sidekick_draw"),
86                    string::utf8(b"legendary"), 0, 1000, current_round),
87            ]);
88        };
89    }
90
91    entry fun update_module(package_owner: &signer) {
92        let (orm_creator, orm_class) = orm_module::get<Sidekick>(@supervlabs);
93        let package = orm_creator::load_creator(package_owner, orm_creator);
94        init_module(&package);
95        let class_address = object::object_address(&orm_class);
96        gacha_rounds::update_probabilities(&package, class_address, 0,
97            vector[
98                string::utf8(b"legendary"),
99                string::utf8(b"epic"),
100                string::utf8(b"rare"),
101                string::utf8(b"uncommon"),
102            ],
103            vector[100000, 800000, 5000000, 30000000],
104            100000000
105        );
106    }
107
108    package fun create_object_by_name(
109        creator_signer: &signer,
110        group_prefix: String,
111        salt: u64,
112        to: Option<address>,
113        grade: String,
114        index: u64,
115        supply_index: u64,
116    ): (ConstructorRef, bool) {
117        let (_, orm_class) = orm_module::get<Sidekick>(@supervlabs);
118        let class_address = object::object_address(&orm_class);
119        let supply = gacha_supply::get(class_address, supply_index);
120        if (gacha_supply::check_run_out(&supply)) {
121            let g = gacha_supply::get_grade(&supply);
122            assert!(g != grade, error::invalid_state(EREACH_TO_MAX_SUPPLY));
123        };
124        let round_up = false;
125        let key = group_prefix;
126        string::append(&mut key, string::utf8(b"/"));
127        string::append(&mut key, grade);
128        let (group, start_index, end_index, _) = gacha_item::get_item_group(key);
129        assert!(index >= start_index, error::invalid_argument(EINVALID_SIDEKICK_INDEX));
130        assert!(index < end_index, error::invalid_argument(EINVALID_SIDEKICK_INDEX));
131        if (grade == gacha_supply::get_grade(&supply)) round_up = true;
132        let (name, uri, description, _, property_keys, property_types, property_values)
133            = gacha_item::load_item_data(creator_signer, group, index);
134        let ref = token::create(
135            creator_signer,
136            string::utf8(b"SuperV Sidekicks"),
137            description,
138            name, // format: "{ItemName} #{count}"
139            option::none(),
140            uri,
141        );
142        let object_signer = orm_object::init<Sidekick>(creator_signer, &ref, orm_class);
143        orm_object::init_properties(&ref,
144            property_keys,
145            property_types,
146            property_values,
147        );
148        let updated_at = timestamp::now_seconds();
149        move_to<Sidekick>(&object_signer, Sidekick {
150            updated_at: updated_at, salt: salt
151        });
152        if (option::is_some(&to)) {
153            let destination = option::extract<address>(&mut to);
154            orm_object::transfer_initially(&ref, destination);
155        };
156        if (round_up) {
157            gacha_supply::increase(class_address, supply_index, 1);
158        };
159        (ref, round_up)
160    }
161
162    package fun create_object(
163        creator_signer: &signer,
164        group_prefix: String,
165        salt: u64,
166        to: Option<address>,
167        grades: vector<String>,
168        numerators: vector<u64>,
169        denominator: u64,
170    ): (ConstructorRef, bool) {
171        let (_, orm_class) = orm_module::get<Sidekick>(@supervlabs);
172        let class_address = object::object_address(&orm_class);
173        let supply = gacha_supply::get(class_address, SIDEKICK_DRAW_SUPPLY_INDEX);
174        if (gacha_supply::check_run_out(&supply)) {
175            let index = gacha_supply::get_add_info(&supply);
176            let numerator = vector::borrow_mut(&mut numerators, index);
177            *numerator = 0;
178        };
179
180        let roll1 = randomness::u64_range(0, denominator);
181        let round_up = false;
182        let (group, start_index, end_index) = (string::utf8(b""), 0, 0);
183        let i = 0;
184        let upto = 0;
185        let len = vector::length(&grades);
186        while (i < len) {
187            let grade = vector::borrow(&grades, i);
188            let numerator = vector::borrow(&numerators, i);
189            upto = upto + *numerator;
190            if (roll1 < upto) {
191                let key = group_prefix;
192                string::append(&mut key, string::utf8(b"/"));
193                string::append(&mut key, *grade);
194                (group, start_index, end_index, _) = gacha_item::get_item_group(key);
195                if (i == gacha_supply::get_add_info(&supply)) round_up = true;
196                break
197            };
198            i = i + 1;
199        };
200        if (i == len) {
201            let key = group_prefix;
202            string::append(&mut key, string::utf8(b"/common"));
203            (group, start_index, end_index, _) = gacha_item::get_item_group(key);
204        };
205
206        let roll2 = randomness::u64_range(start_index, end_index);
207        let (name, uri, description, _, property_keys, property_types, property_values)
208            = gacha_item::load_item_data(creator_signer, group, roll2);
209        let ref = token::create(
210            creator_signer,
211            string::utf8(b"SuperV Sidekicks"),
212            description,
213            name, // format: "{ItemName} #{count}"
214            option::none(),
215            uri,
216        );
217        let object_signer = orm_object::init<Sidekick>(creator_signer, &ref, orm_class);
218
219        orm_object::init_properties(&ref,
220            property_keys,
221            property_types,
222            property_values,
223        );
224
225        let updated_at = timestamp::now_seconds();
226        move_to<Sidekick>(&object_signer, Sidekick {
227            updated_at: updated_at, salt: salt
228        });
229
230        random::store_roll_u64(&object_signer, roll1, 0, denominator);
231        random::store_roll_u64(&object_signer, roll2, start_index, end_index);
232
233        if (option::is_some(&to)) {
234            let destination = option::extract<address>(&mut to);
235            orm_object::transfer_initially(&ref, destination);
236        };
237
238        if (round_up) {
239            gacha_supply::increase(class_address, SIDEKICK_DRAW_SUPPLY_INDEX, 1);
240        };
241        (ref, round_up)
242    }
243
244    fun update_object<T: key>(
245        package_owner: &signer,
246        object: Object<T>,
247    ) acquires Sidekick {
248        let object_address = object::object_address(&object);
249        assert!(
250            exists<Sidekick>(object_address),
251            error::invalid_argument(ENOT_SIDEKICK_OBJECT),
252        );
253        let _object_signer = orm_object::load_signer(package_owner, object);
254        let user_data = borrow_global_mut<Sidekick>(object_address);
255        user_data.updated_at = timestamp::now_seconds();
256    }
257
258    public fun delete_object<T: key>(
259        package_owner: &signer,
260        object: Object<T>,
261    ) acquires Sidekick {
262        let object_address = object::object_address(&object);
263        assert!(
264          exists<Sidekick>(object_address),
265          error::invalid_argument(ENOT_SIDEKICK_OBJECT),
266        );
267        move_from<Sidekick>(object_address);
268        let object_signer = orm_object::delete_and_load_signer(package_owner, object, string::utf8(b"release"));
269        random::clear_log(&object_signer);
270        gacha_rounds::clear_log(&object_signer);
271    }
272
273    public fun delete_for_fusion<T: key>(
274        package_owner: &signer,
275        object: Object<T>,
276    ) acquires Sidekick {
277        let object_address = object::object_address(&object);
278        assert!(
279          exists<Sidekick>(object_address),
280          error::invalid_argument(ENOT_SIDEKICK_OBJECT),
281        );
282        move_from<Sidekick>(object_address);
283        let object_signer = orm_object::load_signer(package_owner, object);
284        random::clear_log(&object_signer);
285        gacha_rounds::clear_log(&object_signer);
286        orm_object::delete(package_owner, object, string::utf8(b"fusion"));
287    }
288
289    #[randomness]
290    entry fun create(
291        package_owner: &signer,
292        sidekick_capsule: address,
293        salt: u64,
294    ) {
295        // burn sidekick_capsule
296        if (sidekick_capsule != @0x0) {
297            let sidekick_capsule_obj = object::address_to_object<sidekick_capsule::SidekickCapsule>(sidekick_capsule);
298            sidekick_capsule::delete_object(package_owner, sidekick_capsule_obj);
299        };
300        let (orm_creator, orm_class) = orm_module::get<Sidekick>(@supervlabs);
301        let class_address = object::object_address(&orm_class);
302        let creator_signer = orm_creator::load_creator(package_owner, orm_creator);
303        let (current_round, grades, numerators, denominator) = gacha_rounds::get_probabilities(class_address);
304
305        let (ref, round_up) = create_object(
306            &creator_signer, string::utf8(b"sidekick"), salt, option::none(),
307            grades, numerators, denominator,
308        );
309        gacha_rounds::set_round_log(class_address, &ref, current_round);
310        if (round_up) {
311            gacha_rounds::round_up(&creator_signer, class_address);
312        };
313    }
314
315    #[randomness]
316    entry fun create_to(
317        package_owner: &signer,
318        sidekick_capsule: address,
319        salt: u64,
320        to: address,
321    ) {
322        // burn sidekick_capsule
323        if (sidekick_capsule != @0x0) {
324            let sidekick_capsule_obj = object::address_to_object<sidekick_capsule::SidekickCapsule>(sidekick_capsule);
325            sidekick_capsule::delete_object(package_owner, sidekick_capsule_obj);
326        };
327        let (orm_creator, orm_class) = orm_module::get<Sidekick>(@supervlabs);
328        let class_address = object::object_address(&orm_class);
329        let creator_signer = orm_creator::load_creator(package_owner, orm_creator);
330        let (current_round, grades, numerators, denominator) = gacha_rounds::get_probabilities(class_address);
331
332        let (ref, round_up) = create_object(
333            &creator_signer, string::utf8(b"sidekick"), salt, option::some(to),
334            grades, numerators, denominator,
335        );
336        gacha_rounds::set_round_log(class_address, &ref, current_round);
337        if (round_up) {
338            gacha_rounds::round_up(&creator_signer, class_address);
339        };
340    }
341
342    fun draw_sidekicks_internal(
343        package_owner: &signer,
344        sidekick_capsules: vector<address>,
345        salt: u64,
346        amount: u64,
347        to: address,
348    ): vector<ConstructorRef> {
349        assert!(amount > 0, error::invalid_argument(EINVALID_AMOUNT));
350        let calsules_len = vector::length(&sidekick_capsules);
351        if (calsules_len > 0) {
352            assert!(amount == calsules_len, error::invalid_argument(EINVALID_AMOUNT));
353        };
354        // burn all sidekick_capsules
355        vector::for_each_ref(&sidekick_capsules, |sidekick_capsule| {
356            if (*sidekick_capsule != @0x0) {
357                let sidekick_capsule_obj =
358                    object::address_to_object<sidekick_capsule::SidekickCapsule>(*sidekick_capsule);
359                sidekick_capsule::delete_object(package_owner, sidekick_capsule_obj);
360            };
361        });
362
363        let (orm_creator, orm_class) = orm_module::get<Sidekick>(@supervlabs);
364        let class_address = object::object_address(&orm_class);
365        let creator_signer = orm_creator::load_creator(package_owner, orm_creator);
366
367        let current_round: u64 = 0;
368        let grades: vector<String> = vector::empty();
369        let numerators: vector<u64> = vector::empty();
370        let denominator: u64 = 100000000;
371        let load_prob: bool = true;
372        let i = 0;
373        let r: vector<ConstructorRef> = vector::empty();
374        while (i < amount) {
375            if (load_prob) {
376                (current_round, grades, numerators, denominator)
377                    = gacha_rounds::get_probabilities(class_address);
378                load_prob = false;
379            };
380            let (ref, round_up) = create_object(
381                &creator_signer, string::utf8(b"sidekick"), salt + i, option::some(to),
382                grades, numerators, denominator,
383            );
384            gacha_rounds::set_round_log(class_address, &ref, current_round);
385            if (round_up) {
386                gacha_rounds::round_up(&creator_signer, class_address);
387                load_prob = true;
388            };
389            i = i+1;
390            vector::push_back(&mut r, ref);
391        };
392        r
393    }
394
395    #[randomness]
396    entry fun draw_sidekicks(
397        package_owner: &signer,
398        sidekick_capsules: vector<address>,
399        salt: u64,
400        amount: u64,
401        to: address,
402    ) {
403        draw_sidekicks_internal(package_owner, sidekick_capsules, salt, amount, to);
404    }
405
406    entry fun update(
407        package_owner: &signer,
408        object: address,
409    ) acquires Sidekick {
410        let obj = object::address_to_object<Sidekick>(object);
411        update_object(package_owner, obj);
412    }
413
414    entry fun delete(
415        package_owner: &signer,
416        object: address,
417    ) acquires Sidekick {
418        let obj = object::address_to_object<Sidekick>(object);
419        delete_object(package_owner, obj);
420    }
421
422    entry fun batch_delete(
423        package_owner: &signer,
424        objects: vector<address>,
425    ) acquires Sidekick {
426        vector::enumerate_ref(&objects, |_i, obj_addr| {
427            let obj = object::address_to_object<Sidekick>(*obj_addr);
428            delete_object(package_owner, obj);
429        });
430    }
431
432    #[view]
433    public fun get(object: address): (
434        string::String,
435        string::String,
436        string::String,
437        address,
438        u64,
439        u64,
440    ) acquires Sidekick {
441        let o = object::address_to_object<Sidekick>(object);
442        let user_data = borrow_global<Sidekick>(object);
443        (
444            token::name(o),
445            token::uri(o),
446            token::description(o),
447            property_map::read_address(&o, &string::utf8(b"sidekick_capsule")),
448            user_data.updated_at,
449            user_data.salt,
450        )
451    }
452
453    #[view]
454    public fun exists_at(object: address): bool {
455        exists<Sidekick>(object)
456    }
457
458    #[view]
459    public fun replay_to_create_object(_object: address): (
460        string::String, u64, u64, u64, u64, u64, u64, u64, u64, string::String, u64, u64,
461    ) {
462        abort(error::invalid_argument(EDEPRECATED_FUNCTION))
463    }
464
465    #[test_only]
466    public fun update_limited_supply(package: &signer, max: u64, max_from_game: u64) {
467        init_module(package);
468        let (_, orm_class) = orm_module::get<Sidekick>(@supervlabs);
469        let class_address = object::object_address(&orm_class);
470        let class_signer = orm_class::load_class_signer(package, orm_class);
471        let (current_round, _, _, _) = gacha_rounds::get_probabilities(class_address);
472        gacha_supply::init(&class_signer, vector[
473            // SIDEKICK_DRAW_SUPPLY_INDEX = 0
474            gacha_supply::new_supply(string::utf8(b"legendary_sidekick_draw"), string::utf8(b"legendary"), 0, max, current_round),
475            // SIDEKICK_FROM_GAME_SUPPLY_INDEX = 1
476            gacha_supply::new_supply(string::utf8(b"legendary_sidekick_from_game"), string::utf8(b"legendary"), 0, max_from_game, 0),
477        ]);
478    }
479
480    #[lint::allow_unsafe_randomness]
481    #[test(aptos = @0x1, my_poa = @0x456, user1 = @0x789, user2 = @0xabc, apto_orm = @apto_orm, creator = @package_creator)]
482    // #[expected_failure(abort_code = 196614, location = Self)] // EUNABLE_TO_MINT_TARGET_SIDEKICK_DUE_TO_LIMITED_SUPPLY
483    public entry fun test_sidekick_limited_supply(
484        aptos: &signer, apto_orm: &signer, creator: &signer, my_poa: &signer, user1: &signer, user2: &signer
485    ) {
486        use apto_orm::test_utilities;
487        use apto_orm::power_of_attorney;
488        // use aptos_std::debug;
489        test_utilities::init_network(aptos, 1234);
490        randomness::initialize_for_testing(aptos);
491
492        let program_address = signer::address_of(apto_orm);
493        let creator_address = signer::address_of(creator);
494        let my_poa_address = signer::address_of(my_poa);
495        let user1_address = signer::address_of(user1);
496        let user2_address = signer::address_of(user2);
497
498        test_utilities::create_and_fund_account(program_address, 100);
499        test_utilities::create_and_fund_account(creator_address, 10);
500        test_utilities::create_and_fund_account(my_poa_address, 100);
501        test_utilities::create_and_fund_account(user1_address, 100);
502        test_utilities::create_and_fund_account(user2_address, 10);
503        let package = orm_creator::create_creator(creator, string::utf8(b"supervlabs"));
504
505        let target_supply = 2;
506        update_limited_supply(&package, target_supply, target_supply);
507
508        gacha_item::test_init_module(creator, &package);
509        power_of_attorney::register_poa(creator, my_poa, 1400, 0);
510        let refs = draw_sidekicks_internal(my_poa, vector::empty(), 1, 5000, user1_address);
511        let len = vector::length(&refs);
512        let i = 0;
513        while (i < len) {
514            let ref = vector::borrow(&refs, i);
515            let obj = object::object_from_constructor_ref<Sidekick>(ref);
516            assert!(object::owner(obj) == user1_address, 1);
517            i = i + 1;
518        };
519        let (_, orm_class) = orm_module::get<Sidekick>(@supervlabs);
520        let class_address = object::object_address(&orm_class);
521        let supply = gacha_supply::get(class_address, SIDEKICK_DRAW_SUPPLY_INDEX);
522        // debug::print<gacha_supply::SupplyInfo>(&supply);
523        assert!(gacha_supply::check_run_out(&supply), 1);
524    }
525}

© 2025 Aptos Labs