From ca6fd4c7c61e957c9349e87e072db9765ab3fe58 Mon Sep 17 00:00:00 2001 From: Rob Konsdorf Date: Sun, 28 Jun 2026 20:15:04 -0400 Subject: [PATCH 01/15] feat(rentals): non-custodial rental flow over AtomicAssets lease primitives Rewrite the rental flow to be non-custodial: the asset is never escrowed. announcerent only records the listing; the asset stays with the lister until rented. rentasset drives the AtomicAssets leasestart (making the renter the real owner and locking the asset), leaseextend for same-renter extensions, and reclaim-then-leasestart for an expired-but-unreclaimed re-rental. endrent and cancelrent become thin reclaim + mirror-resync wrappers. - remove the "rental" custody intake from receive_asset_transfer and the now-unused logrentstart - rename rentals.asset_transferred to is_rented - refuse listing a rental-locked asset for sale/auction/buyoffer/re-rent via a single check in get_collection_and_check_assets (reads the AA leases table directly); AA's own guards remain the authoritative backstop - read the AA leases table as the source of truth for lock state, and pass the asset's current owner as the royalty payout scope - update the AA test fixture to the non-custodial build and rewrite the rental lifecycle suite + add lock-refusal and stray-memo tests Experimental branch for smart-contract review. Pairs with the atomicassets-contract experiment/noncustodial-rentals branch. --- include/atomicassets-interface.hpp | 27 +- include/atomicmarket.hpp | 33 +-- src/atomicmarket.cpp | 228 +++++++-------- tests/fixtures/atomicassets/atomicassets.abi | 276 +++++++++++++----- tests/fixtures/atomicassets/atomicassets.wasm | Bin 241841 -> 250697 bytes tests/market-smoke.test.js | 127 ++++++-- 6 files changed, 444 insertions(+), 247 deletions(-) diff --git a/include/atomicassets-interface.hpp b/include/atomicassets-interface.hpp index 097b554..90845ff 100644 --- a/include/atomicassets-interface.hpp +++ b/include/atomicassets-interface.hpp @@ -148,17 +148,24 @@ namespace atomicassets { typedef multi_index assets_t; - struct holders_s { + // Non-custodial rental lock/title record. A row's existence means the asset + // is leased and locked; renter == name("") && rental_end == 0 is a pretitle + // sentinel (locked, owner still the lister). + struct leases_s { uint64_t asset_id; - name holder; - name owner; - - uint64_t primary_key() const { return asset_id; }; - uint64_t by_holder() const { return holder.value; }; + name title_owner; + name renter; + uint32_t rental_end; + name market; + + uint64_t primary_key() const { return asset_id; }; + uint64_t by_title_owner() const { return title_owner.value; }; + uint64_t by_rental_end() const { return (uint64_t) rental_end; }; }; - typedef multi_index >> - holders_t; + typedef multi_index >, + indexed_by>> + leases_t; struct offers_s { @@ -228,6 +235,6 @@ namespace atomicassets { template_mutables_t get_template_mutables(name collection_name) {return template_mutables_t(ATOMICASSETS_ACCOUNT, collection_name.value);} assets_t get_assets(name owner) {return assets_t(ATOMICASSETS_ACCOUNT, owner.value);} - holders_t get_holders() {return holders_t(ATOMICASSETS_ACCOUNT, ATOMICASSETS_ACCOUNT.value);} + leases_t get_leases() {return leases_t(ATOMICASSETS_ACCOUNT, ATOMICASSETS_ACCOUNT.value);} }; \ No newline at end of file diff --git a/include/atomicmarket.hpp b/include/atomicmarket.hpp index cc4ae55..459da0a 100644 --- a/include/atomicmarket.hpp +++ b/include/atomicmarket.hpp @@ -317,19 +317,21 @@ CONTRACT atomicmarket : public contract { /* Rentals - Custodial rental flow: - 1. The owner announces a rental listing (announcerent), specifying the price per hour, - the settlement symbol, and the maximum duration a single rental can cover - 2. The owner transfers the asset to the atomicmarket contract with the memo "rental", - which activates the listing (the contract becomes the custodial owner) - 3. A renter pays for a number of hours from their deposited balance (rentasset). The + Non-custodial rental flow (renter-as-owner): + 1. The owner announces a rental listing (announcerent). The asset is NOT escrowed - it + stays fully owned and usable by the lister until it is actually rented. + 2. A renter pays for a number of hours from their deposited balance (rentasset). The payment is distributed like a sale payout (market fees, collection fee / royalty - splits, remainder to the listing owner) and the atomicassets HOLDERSHIP of the - asset is moved to the renter, while ownership stays with the contract - 4. After the rental period is over, anyone can reset the holdership back to the - contract (endrent), making the listing rentable again - 5. The owner can cancel the listing and reclaim the asset whenever no rental is - actively running (cancelrent) + splits, remainder to the listing owner) and atomicmarket drives AtomicAssets to make + the RENTER the real owner of the asset (leasestart), parking the lister's reclaim + right in the AtomicAssets leases table. The asset is locked while leased. + 3. An extension by the same renter only bumps the lease end (leaseextend); no second + ownership flip. + 4. After the rental period is over, the asset returns to the lister via the permissionless + AtomicAssets `reclaim` (a keeper, endrent, or cancelrent triggers it). atomicmarket's + endrent then resyncs its mirror row, making the listing rentable again. + 5. The owner can cancel the listing whenever no rental is actively running (cancelrent); + an expired-but-unreclaimed lease is reclaimed as part of the cancel. */ ACTION announcerent( @@ -468,11 +470,6 @@ CONTRACT atomicmarket : public contract { double collection_fee ); - ACTION logrentstart( - uint64_t asset_id, - name lister - ); - ACTION logrental( uint64_t rental_counter_id, uint64_t asset_id, @@ -687,7 +684,7 @@ CONTRACT atomicmarket : public contract { symbol settlement_symbol; // what the rental is actually paid in uint32_t maximum_rental_duration; // seconds; the longest period a rental can cover uint32_t rental_end; // seconds since epoch; 0 when not rented out - bool asset_transferred; // true once the asset is in contract custody + bool is_rented; // true while a renter holds an active lease name maker_marketplace; name collection_name; double collection_fee; diff --git a/src/atomicmarket.cpp b/src/atomicmarket.cpp index 2538c52..6d0a3be 100644 --- a/src/atomicmarket.cpp +++ b/src/atomicmarket.cpp @@ -1796,8 +1796,8 @@ ACTION atomicmarket::fulfilltbuyo( /** * Create a rental listing for a single asset -* For the listing to become active, the lister needs to use the atomicassets transfer action to -* transfer the asset to the atomicmarket contract with the memo "rental" +* Non-custodial: the asset is NOT transferred to the contract. It stays owned and usable by the +* lister until someone actually rents it (rentasset), at which point ownership moves to the renter. * * price_per_hour is denoted in the listing symbol; if it differs from the settlement symbol, a * delphi symbol pair has to be configured and the rental is paid in the settlement symbol at the @@ -1855,7 +1855,7 @@ ACTION atomicmarket::announcerent( _rental.settlement_symbol = settlement_symbol; _rental.maximum_rental_duration = maximum_rental_duration; _rental.rental_end = 0; - _rental.asset_transferred = false; + _rental.is_rented = false; _rental.maker_marketplace = maker_marketplace; _rental.collection_name = assets_collection_name; _rental.collection_fee = collection_fee; @@ -1882,11 +1882,11 @@ ACTION atomicmarket::announcerent( /** * Cancels a rental listing * -* If the listing is not active yet, it can be cancelled by the owner - or by anyone if the -* owner does not own the asset anymore (which makes the listing invalid) -* -* If the listing is active (the asset is in contract custody), it can only be cancelled by the -* owner and only while no rental is actively running. The asset is then transferred back +* Non-custodially the asset is never escrowed, so cancelling is just removing the listing row. +* If no rental is active, the owner can cancel - or anyone can, if the owner no longer owns the +* asset (which makes the listing invalid). An actively running rental cannot be cancelled; an +* expired-but-unreclaimed rental is reclaimed (returning the asset to the owner) as part of the +* cancel. * * @required_auth The listing's owner (see above for the invalid listing exception) */ @@ -1897,46 +1897,36 @@ ACTION atomicmarket::cancelrent( auto rental_itr = rentals.require_find(asset_id, "No rental listing with this asset_id exists"); - if (!rental_itr->asset_transferred) { - atomicassets::assets_t owner_assets = atomicassets::get_assets(rental_itr->owner); - bool is_rental_invalid = owner_assets.find(asset_id) == owner_assets.end(); - - check(is_rental_invalid || has_auth(rental_itr->owner), - "The rental listing is not invalid, therefore the authorization of the owner is needed to cancel it"); - - rentals.erase(rental_itr); - return; - } + uint32_t current_time = current_time_point().sec_since_epoch(); - require_auth(rental_itr->owner); + atomicassets::leases_t aa_leases = atomicassets::get_leases(); + auto lease_itr = aa_leases.find(asset_id); + bool lease_exists = lease_itr != aa_leases.end() && lease_itr->renter != name(""); + bool active_lease = lease_exists && lease_itr->rental_end > current_time; - check(rental_itr->holder == name("") || - rental_itr->rental_end <= current_time_point().sec_since_epoch(), + check(!active_lease, "The asset is currently rented out. The listing can only be cancelled after the rental period is over"); - if (rental_itr->holder != name("")) { - // The rental period is over but the holdership was never reset via endrent. - // It needs to be reclaimed BEFORE the asset is transferred back, otherwise the - // holders table row would survive the transfer + if (lease_exists) { + // Expired but never reclaimed: only the owner can cancel, and the asset returns to them + // via the permissionless reclaim. + require_auth(rental_itr->owner); + action( permission_level{get_self(), name("active")}, atomicassets::ATOMICASSETS_ACCOUNT, - name("move"), - make_tuple( - get_self(), - rental_itr->holder, - get_self(), - vector {asset_id}, - string("AtomicMarket Rental Ended") - ) + name("reclaim"), + make_tuple(asset_id) ).send(); - } + } else { + // Not rented: the asset is still with the lister, nothing to return. Anyone may cancel an + // invalid listing (the owner no longer owns the asset); otherwise the owner must authorize. + atomicassets::assets_t owner_assets = atomicassets::get_assets(rental_itr->owner); + bool is_rental_invalid = owner_assets.find(asset_id) == owner_assets.end(); - internal_transfer_assets( - rental_itr->owner, - vector {asset_id}, - "AtomicMarket Cancelled Rental Listing - Asset ID # " + to_string(asset_id) - ); + check(is_rental_invalid || has_auth(rental_itr->owner), + "The rental listing is not invalid, therefore the authorization of the owner is needed to cancel it"); + } rentals.erase(rental_itr); } @@ -1949,8 +1939,8 @@ ACTION atomicmarket::cancelrent( * uses a delphi pairing) is deducted from the renter's balance and paid out like a sale payout * (market fees, collection fee / royalty splits, remainder to the listing owner) * -* The atomicassets HOLDERSHIP of the asset is moved to the renter until the rental period is -* over, while the ownership stays with the atomicmarket contract +* atomicmarket drives AtomicAssets to make the renter the real OWNER of the asset until the +* rental period is over; the lister's reclaim right is parked in the AtomicAssets leases table * * If the renter already holds an active rental for this asset, the new hours extend the * current rental period instead (the combined remaining period must stay within the listing's @@ -1974,9 +1964,6 @@ ACTION atomicmarket::rentasset( auto rental_itr = rentals.require_find(asset_id, "No rental listing with this asset_id exists"); - check(rental_itr->asset_transferred, - "This rental listing is not active yet. The owner first has to transfer the asset to the atomicmarket account"); - check(renter != rental_itr->owner, "You can't rent your own asset"); check(rental_itr->price_per_hour == expected_price_per_hour, @@ -1986,14 +1973,20 @@ ACTION atomicmarket::rentasset( uint32_t current_time = current_time_point().sec_since_epoch(); - bool has_active_rental = rental_itr->holder != name("") && rental_itr->rental_end > current_time; - bool is_extension = has_active_rental && rental_itr->holder == renter; + // The AtomicAssets leases table is the source of truth for the on-chain lock state; the + // rentals mirror below can lag a keeper-driven reclaim. + atomicassets::leases_t aa_leases = atomicassets::get_leases(); + auto lease_itr = aa_leases.find(asset_id); + bool lease_exists = lease_itr != aa_leases.end() && lease_itr->renter != name(""); + bool active_lease = lease_exists && lease_itr->rental_end > current_time; + bool is_extension = active_lease && lease_itr->renter == renter; - check(!has_active_rental || is_extension, + check(!active_lease || is_extension, "This asset is currently rented out. It can be rented again once the current rental period is over"); uint64_t added_duration = (uint64_t) rental_hours * 3600; - uint64_t new_rental_end = (uint64_t)(is_extension ? rental_itr->rental_end : current_time) + added_duration; + uint64_t base_time = is_extension ? (uint64_t) lease_itr->rental_end : (uint64_t) current_time; + uint64_t new_rental_end = base_time + added_duration; check(new_rental_end - current_time <= rental_itr->maximum_rental_duration, "The rental period would exceed the maximum rental duration of this listing"); @@ -2016,6 +2009,12 @@ ACTION atomicmarket::rentasset( uint64_t rental_counter_id = consume_counter(name("rental")); + // The royalty engine reads the asset's data from its CURRENT owner scope. Non-custodially + // that is whoever owns the asset right now (the inline lease actions below run only after + // this action returns): the current renter for an extension or an expired-unreclaimed + // re-rental, otherwise the listing owner. + name asset_scope = lease_exists ? lease_itr->renter : rental_itr->owner; + internal_payout_sale( settlement_price, rental_itr->owner, @@ -2023,37 +2022,52 @@ ACTION atomicmarket::rentasset( taker_marketplace, rental_itr->collection_name, vector {asset_id}, - get_self(), // the asset is in contract custody + asset_scope, name("rental"), rental_counter_id, "AtomicMarket Rental Payout - ID #" + to_string(rental_counter_id) ); - if (!is_extension) { - // Move the holdership to the renter. If a previous rental expired without endrent - // being called, the holdership still sits with the previous renter and is moved - // directly from them; otherwise it is moved from the contract itself - name move_from = rental_itr->holder == name("") ? get_self() : rental_itr->holder; - - if (move_from != renter) { + if (is_extension) { + // Same renter, already the owner: only extend the lease end in AtomicAssets. + action( + permission_level{get_self(), name("active")}, + atomicassets::ATOMICASSETS_ACCOUNT, + name("leaseextend"), + make_tuple(get_self(), asset_id, (uint32_t) new_rental_end) + ).send(); + } else { + // A previous lease that expired but was never reclaimed still locks the asset. Clear it + // first (permissionless reclaim) so leasestart can re-lease from the listing owner. + if (lease_exists) { action( permission_level{get_self(), name("active")}, atomicassets::ATOMICASSETS_ACCOUNT, - name("move"), - make_tuple( - get_self(), - move_from, - renter, - vector {asset_id}, - string("AtomicMarket Rental - ID # ") + to_string(rental_counter_id) - ) + name("reclaim"), + make_tuple(asset_id) ).send(); } + // Make the renter the real AtomicAssets owner and lock the asset (the lister's reclaim + // right is parked in the AtomicAssets leases table). + action( + permission_level{get_self(), name("active")}, + atomicassets::ATOMICASSETS_ACCOUNT, + name("leasestart"), + make_tuple( + get_self(), + rental_itr->owner, + renter, + asset_id, + (uint32_t) new_rental_end, + string("AtomicMarket Rental - ID # ") + to_string(rental_counter_id) + ) + ).send(); } rentals.modify(rental_itr, same_payer, [&](auto &_rental) { _rental.holder = renter; _rental.rental_end = (uint32_t) new_rental_end; + _rental.is_rented = true; }); action( @@ -2075,10 +2089,11 @@ ACTION atomicmarket::rentasset( /** -* Wraps up an expired rental by moving the holdership of the asset back to the atomicmarket -* contract, making the listing rentable again +* Wraps up an expired rental, making the listing rentable again * -* This can be called by anyone - it only resets an expired rental back to its listed state +* The authoritative return of the asset to the lister is the permissionless AtomicAssets +* `reclaim`. endrent triggers that reclaim if it has not happened yet (e.g. no keeper ran) and +* then resyncs atomicmarket's mirror row. It can be called by anyone. * * @required_auth None */ @@ -2089,27 +2104,29 @@ ACTION atomicmarket::endrent( auto rental_itr = rentals.require_find(asset_id, "No rental listing with this asset_id exists"); - check(rental_itr->holder != name(""), "This asset is not currently rented out"); + check(rental_itr->is_rented, "This asset is not currently rented out"); - check(rental_itr->rental_end <= current_time_point().sec_since_epoch(), - "The rental period is not over yet"); + uint32_t current_time = current_time_point().sec_since_epoch(); - action( - permission_level{get_self(), name("active")}, - atomicassets::ATOMICASSETS_ACCOUNT, - name("move"), - make_tuple( - get_self(), - rental_itr->holder, - get_self(), - vector {asset_id}, - string("AtomicMarket Rental Ended") - ) - ).send(); + // If the lease still exists in AtomicAssets it must have expired; trigger the permissionless + // reclaim. If a keeper already reclaimed it, the row is gone and we just resync the mirror. + atomicassets::leases_t aa_leases = atomicassets::get_leases(); + auto lease_itr = aa_leases.find(asset_id); + if (lease_itr != aa_leases.end() && lease_itr->renter != name("")) { + check(lease_itr->rental_end <= current_time, "The rental period is not over yet"); + + action( + permission_level{get_self(), name("active")}, + atomicassets::ATOMICASSETS_ACCOUNT, + name("reclaim"), + make_tuple(asset_id) + ).send(); + } rentals.modify(rental_itr, same_payer, [&](auto &_rental) { _rental.holder = name(""); _rental.rental_end = 0; + _rental.is_rented = false; }); } @@ -2280,36 +2297,9 @@ void atomicmarket::receive_asset_transfer( ) ).send(); - } else if (memo == "rental") { - auto rentals = get_rentals(); - - for (uint64_t asset_id : asset_ids) { - auto rental_itr = rentals.require_find(asset_id, - ("No rental listing exists for one of the transferred assets - " + to_string(asset_id)).c_str()); - - check(rental_itr->owner == from, - ("A rental listing for this asset exists, but it belongs to another account - " - + to_string(asset_id)).c_str()); - - check(!rental_itr->asset_transferred, - ("The asset for this rental listing has already been transferred - " + to_string(asset_id)).c_str()); - - rentals.modify(rental_itr, same_payer, [&](auto &_rental) { - _rental.asset_transferred = true; - }); - - action( - permission_level{get_self(), name("active")}, - get_self(), - name("logrentstart"), - make_tuple( - asset_id, - from - ) - ).send(); - } - } else { + // Non-custodial rentals never transfer the asset to the contract, so a "rental" memo is + // no longer a valid intake and falls through to this rejection. check(false, "Invalid memo"); } } @@ -2465,15 +2455,6 @@ ACTION atomicmarket::lognewrent( require_recipient(lister); } -ACTION atomicmarket::logrentstart( - uint64_t asset_id, - name lister -) { - require_auth(get_self()); - - require_recipient(lister); -} - ACTION atomicmarket::logrental( uint64_t rental_counter_id, uint64_t asset_id, @@ -2542,6 +2523,7 @@ name atomicmarket::get_collection_and_check_assets( atomicassets::assets_t owner_assets = atomicassets::get_assets(owner); + atomicassets::leases_t aa_leases = atomicassets::get_leases(); name assets_collection_name = name(""); // All assets have to belong to the same collection, so the templates table only needs to @@ -2553,6 +2535,12 @@ name atomicmarket::get_collection_and_check_assets( ("The specified account does not own at least one of the assets - " + to_string(asset_id)).c_str()); + // A rental-locked (leased or pre-titled) asset cannot be listed for sale, auction or + // buyoffer. Refuse it early here rather than letting it fail at settlement time. The + // AtomicAssets transfer/offer guards remain the authoritative backstop. + check(aa_leases.find(asset_id) == aa_leases.end(), + ("One of the assets is currently rented out (locked) - " + to_string(asset_id)).c_str()); + if (assets_collection_name == name("")) { assets_collection_name = asset_itr->collection_name; } else { diff --git a/tests/fixtures/atomicassets/atomicassets.abi b/tests/fixtures/atomicassets/atomicassets.abi index 4d94992..30b03ea 100644 --- a/tests/fixtures/atomicassets/atomicassets.abi +++ b/tests/fixtures/atomicassets/atomicassets.abi @@ -351,6 +351,10 @@ { "name": "supported_tokens", "type": "extended_symbol[]" + }, + { + "name": "rental_market", + "type": "name" } ] }, @@ -536,6 +540,16 @@ } ] }, + { + "name": "delpretitle", + "base": "", + "fields": [ + { + "name": "asset_id", + "type": "uint64" + } + ] + }, { "name": "deltemplate", "base": "", @@ -601,27 +615,83 @@ ] }, { - "name": "holders_s", + "name": "init", + "base": "", + "fields": [] + }, + { + "name": "leaseextend", "base": "", "fields": [ + { + "name": "market", + "type": "name" + }, { "name": "asset_id", "type": "uint64" }, { - "name": "holder", + "name": "rental_end", + "type": "uint32" + } + ] + }, + { + "name": "leases_s", + "base": "", + "fields": [ + { + "name": "asset_id", + "type": "uint64" + }, + { + "name": "title_owner", "type": "name" }, { - "name": "owner", + "name": "renter", + "type": "name" + }, + { + "name": "rental_end", + "type": "uint32" + }, + { + "name": "market", "type": "name" } ] }, { - "name": "init", + "name": "leasestart", "base": "", - "fields": [] + "fields": [ + { + "name": "market", + "type": "name" + }, + { + "name": "title_owner", + "type": "name" + }, + { + "name": "renter", + "type": "name" + }, + { + "name": "asset_id", + "type": "uint64" + }, + { + "name": "rental_end", + "type": "uint32" + }, + { + "name": "memo", + "type": "string" + } + ] }, { "name": "locktemplate", @@ -701,6 +771,36 @@ } ] }, + { + "name": "loglock", + "base": "", + "fields": [ + { + "name": "collection_name", + "type": "name" + }, + { + "name": "asset_id", + "type": "uint64" + }, + { + "name": "title_owner", + "type": "name" + }, + { + "name": "renter", + "type": "name" + }, + { + "name": "rental_end", + "type": "uint32" + }, + { + "name": "market", + "type": "name" + } + ] + }, { "name": "logmint", "base": "", @@ -747,36 +847,6 @@ } ] }, - { - "name": "logmove", - "base": "", - "fields": [ - { - "name": "collection_name", - "type": "name" - }, - { - "name": "owner", - "type": "name" - }, - { - "name": "from", - "type": "name" - }, - { - "name": "to", - "type": "name" - }, - { - "name": "asset_ids", - "type": "uint64[]" - }, - { - "name": "memo", - "type": "string" - } - ] - }, { "name": "lognewoffer", "base": "", @@ -867,6 +937,28 @@ } ] }, + { + "name": "logreclaim", + "base": "", + "fields": [ + { + "name": "collection_name", + "type": "name" + }, + { + "name": "asset_id", + "type": "uint64" + }, + { + "name": "title_owner", + "type": "name" + }, + { + "name": "renter", + "type": "name" + } + ] + }, { "name": "logsetdata", "base": "", @@ -979,32 +1071,6 @@ } ] }, - { - "name": "move", - "base": "", - "fields": [ - { - "name": "owner", - "type": "name" - }, - { - "name": "from", - "type": "name" - }, - { - "name": "to", - "type": "name" - }, - { - "name": "asset_ids", - "type": "uint64[]" - }, - { - "name": "memo", - "type": "string" - } - ] - }, { "name": "offers_s", "base": "", @@ -1067,6 +1133,34 @@ } ] }, + { + "name": "pretitle", + "base": "", + "fields": [ + { + "name": "title_owner", + "type": "name" + }, + { + "name": "market", + "type": "name" + }, + { + "name": "asset_id", + "type": "uint64" + } + ] + }, + { + "name": "reclaim", + "base": "", + "fields": [ + { + "name": "asset_id", + "type": "uint64" + } + ] + }, { "name": "redtemplmax", "base": "", @@ -1233,6 +1327,16 @@ } ] }, + { + "name": "setrentmkt", + "base": "", + "fields": [ + { + "name": "rental_market", + "type": "name" + } + ] + }, { "name": "setschematyp", "base": "", @@ -1476,6 +1580,11 @@ "type": "declineoffer", "ricardian_contract": "" }, + { + "name": "delpretitle", + "type": "delpretitle", + "ricardian_contract": "" + }, { "name": "deltemplate", "type": "deltemplate", @@ -1496,6 +1605,16 @@ "type": "init", "ricardian_contract": "" }, + { + "name": "leaseextend", + "type": "leaseextend", + "ricardian_contract": "" + }, + { + "name": "leasestart", + "type": "leasestart", + "ricardian_contract": "" + }, { "name": "locktemplate", "type": "locktemplate", @@ -1512,13 +1631,13 @@ "ricardian_contract": "" }, { - "name": "logmint", - "type": "logmint", + "name": "loglock", + "type": "loglock", "ricardian_contract": "" }, { - "name": "logmove", - "type": "logmove", + "name": "logmint", + "type": "logmint", "ricardian_contract": "" }, { @@ -1536,6 +1655,11 @@ "type": "logrampayer", "ricardian_contract": "" }, + { + "name": "logreclaim", + "type": "logreclaim", + "ricardian_contract": "" + }, { "name": "logsetdata", "type": "logsetdata", @@ -1557,13 +1681,18 @@ "ricardian_contract": "" }, { - "name": "move", - "type": "move", + "name": "payofferram", + "type": "payofferram", "ricardian_contract": "" }, { - "name": "payofferram", - "type": "payofferram", + "name": "pretitle", + "type": "pretitle", + "ricardian_contract": "" + }, + { + "name": "reclaim", + "type": "reclaim", "ricardian_contract": "" }, { @@ -1611,6 +1740,11 @@ "type": "setrampayer", "ricardian_contract": "" }, + { + "name": "setrentmkt", + "type": "setrentmkt", + "ricardian_contract": "" + }, { "name": "setschematyp", "type": "setschematyp", @@ -1674,8 +1808,8 @@ "key_types": [] }, { - "name": "holders", - "type": "holders_s", + "name": "leases", + "type": "leases_s", "index_type": "i64", "key_names": [], "key_types": [] diff --git a/tests/fixtures/atomicassets/atomicassets.wasm b/tests/fixtures/atomicassets/atomicassets.wasm index f3579af7e03fefca6f9c74e77fc0889e2320d897..bb1135e8290634bb8bd514ce536c066ffff1f41e 100755 GIT binary patch delta 68735 zcmd443!GI`|37}#-uuku%uMZ6(=<)B&uP@8NfXnQggHH>+(XF2qoSw@UF2Gvi4elD z%Zew25Q@m7+y@~Pxg~@ULdgB{xcuMm&)WN(GgEndFTd~W-}9Kg_qwmo`g}g?b6e}& z`+ke(ZfjW>8GYI*DiVpP>+PDW3j2(RsPjfysgz2M$~h1JGJ#1Z0gouM&tvC#QZzN4RoN$6 zneJDqK^=oCs2r8bQYVEbtR*>J{fpI8{;}3Bt@7=$r=C$T=J=7Pjy-GY$niDR{)JYp z<(=lYvOPa$cT2@sp!l@0Q%6pkGI7$x)5ebB%b3$o7(3;(ND(s&PdI(bl(DBy9XWOU zDPtqjV4IUre)I`b$Dc7atF$!L8hwo)b5>3D$df0YId;nN6Hh;NOr!f|qX;PS@ zTKG>`i4iSXAcm@APaVTiMqDPeM8c#gW6u~r@$}OqReETf?%~AoC_QP)_)|ttIXlvd zHHy-CoNpxNFT@mgVvajy?C3Ev=830{95Z_A=!i7dYV5?*#!nnM`n1!=PMJFLgo$Iu zMp}mrojwWW#!AO+!r_>ynVd-5u;i4nr%VJWIPl`8C6VI~bDUJMy~+8&iL@&zj+7La zShi}fN>vBTDlXoM397`hVzIK~So>JVXeTG;SRJhPQKxn1;^LC1XXWGgU1i+ z9eT~XSQVrqNj`FSvT{xVrc628ibO|GnsoBnk#6}BbZ+w<&e?d$A++Vkz}?HlYH?FIHt_RaS5_6zoEdyV~~{gQuq zZu`^*`yKmT`#pP;{gM5#{fWKV{?z`={@mVTe_?-Ve`SAde`|kl|7ibg|J(k}{==T( zT;R-c<~r9n+L`BE>s;sj)0yvF@7&Zh7JmNf7=REE_;XLI$?L6x|=X~RQ>wM>Y@BHBW==|jT z?EK>V+xgY`&H3H=!>NzXh@Kz4AUZR8Vf3Qt#nIU1Yg&BZI1zW3f>i|(FXA5OcJ(5j zI@GBhSTFxJefeF)v+&Ji_db7OUUBbbRusAZ_gnA!;?04X%;iX4dS}C$H=eusj@>fJ z4_Z-wbWxksLsIM7Ip5y#PIj$FCAog>2U|Y+Y)L9p?J-IJ`Cs=fe*c+Ev(g`D`odSA zfB4p>>5WWZbng%MeZTJI=d*_VglWHi;=)f>u6ih|p(iC{!NRNN{`0e^vno6#>9;-b z%Bq{Q$N8)zFIl%{Rg>AR$|S#%-THHWGQYU}^US^JnfErVez8gJ3;rbJu14<0d+xsN zlTFv_cd|yX#=kMYUCE2cU3=a9b!#@Jd&gR*-e4n}ZoBlZJ3h)TwjRm#i{88h$xR^nCM$k) z+u~d2J~iv|EI_`+^4~o(8|9zN2Ep4*uV1ik>6T5e`dQ^SARXO4f5hC`|IF_9J(l0_ z{PH_L-H@HU5y@NceD{uJTUKT#zmMd`b$7n9_KsJ-tV})C_hX`*t*<84LJkKjis=Byc*wTM*r?`J*+v}0lv-ko0-BkR#+I`tW z?Z)TeQQqlgTlMyThai@5`)hZdilo84{;n$h1-%|o2QK?ZRcnQ$$-M_5>4Dz&<9)(z7vcSz-S$?s ze(&9%M78gBKUN*+A6or1a@+Ok*0M)D>e- zj`e@9P44uMcn$zokj$$~$SB-@_=hHURmb@C$u{bkWpk5fIH*v#?|DkK@h=;4EjrtI z=;dl3f7#G?yB>s*N0YtXVWnsz&vW)IirCzc@_I%h$(w$O)Kx|z?jip2{W`a-!~$X* z<(RZ*yCeLM_UoDnvfCUq8Sua8phPPv#4bD=ohWqj;6clgH~HXwQEtV-Nj1X% z<>1%w{?Z|*B7N^+8t?0dP0Fh*K!pe@9DL{xB_MaQb*RnaCSl zq`bW3Z_h-$Tz8<~>x}*U>(1!Z(oU|8)Ojk2d85~%{(Xm^ibmqYk5gm(Cq}gM^>DAv ziGT{Q((&vHtAu~b+>`v5k7jst+xZu&VK5+zcR|^Fx@F^bf;}b zxoPBoXOykN23u`+lt+*Li=%9UnQqXiuZ|xGNDex_zZ&AFj^ADIv)F&`_`=E|?vPS4 z(i*ED9(i?ME*=$iZZ>V@k+up!Tj~VTR{Il#w)&q?W>psWS03NipExSlpL#+y^6ogH z^KRL+(K4JCIaEl6pmd=^>`KY{XCrQ{-|C8ZF5bL6_i#Tkrg!0(!WPV25@!1KV{+8t z{>5Wz!mJHpRxaM8-q)GTyHufNlLRhrxS%yWs#DI&|gh0oe0R11Qj#J0`&z#!1Pj+(^Y_8Hh3AkKgRU{&Vi(JSy zks7NKTTo@8w6!7K7T%<-ffEnI(B@9uTMhTGyt%Fa>BR1sTe+Pmq@UDSAg^_iDo zl1H9J033Ht88_08&+3GxCr>Z+JD=SOOWXVGu6g4LYd5L z%uK!M=Va;?_>Jcfsd}GXR!|C&#s;sk-0}XG=L}GFVMqH-%c_vOJQx?M_)Dk7Ft&%M z4fJn1zXQiqIz1s{iu=b+&nXh3!la%JCeJfiM_7ANP$D!H2)R5CNb;YGYYQW-|cIjnd8u&VvaQ$V}Fvvd*fi?ZMF_10#LWYy?6@5{x*Kyiagou}_(Mwg2~?hLM#B z|D?`_v6zpn7k6LbdHY8EiE~T+Q|9bqR~EMM-=9ofo*4k@W%aW42>s?xKMhYUPge>#ytJU!+gO zz}@SM{m%1G$Y?jG&bv^O&(2Qj{Qs=B@2>3{*NxJ7+_ z2sL*L?3D_=?N$~nJMOd-n{O4hHyt}>2+HSrPO`Ah9TLw+4Fz_5TEg;jpmNF@czFZ! z++dr{*0O*^A}i)YYxK4&E20Ejn|k%l?UIBg6tsj`7;Mj~vC5D|s-{R-ExSJYtFQ*7 z6OAY!iB)c;KjP*Z*`WturB>gp)j&Pue02>T@1CzZ`=tvnRk!-LE~Hed_tvU&485T# z=#7=jzFOE)^&1k;0c4`VTru{lI(Iy}=PdixVFx`UQ2-K1I7N|UKetE1p>m(&4)JSl zJ5-(N&${h^-8Mk+Mv+zyI9D=NV^tMe0Ka=Q(bg+4(leUkS5DHJn!Nwte)RSP*>3mS z%b?(x$(Cd$>tSBU+`br-xbgyZ86F*Gs)Lt(u;>d5)VubsZuy5}Fp#d?+N&2=f<5$H zd;l`eUR=FXQgmiCn3w$aWurAezW5}`?A7l3h*ubB)l%ZebcvDv>Lqz7_4<z54zXI${%-c4X9K3y%+D{uf3O( zIPhP++kTLlDmoy0q5)yXvN2I3HL}d7P0ElVnld&14NG>5E|&pp+ZNJ;a^r%&K5o(u zMCo*2M(EwPFQjMnwQGJ(JXlVQy(TkZ%Fp4jZxH#JO>Jt@9nz&t@_{}8#+eWF0~suR zU;>6#w)~(>Q=tjPz^ua{HSty;nN(^lLKqgdj>u1MMM zL8i=puzd%pgUVACRvYkz8mmnq>}!hTV7X`ejSr4QDes}q_*?tXIi)?~Bm!8g3Qw+! z!-NEK!W-yri2vF{6AOhy#cgZ_o}(vU3|rSHH@N<354TQ%IHSU?qT6t*g1hr5G_F8O(V4sH7p}$%bi8fW*d@Dnb+1x*F?#mDR?vK_8x7V~xs6AC}cLv@O5YqttKE z_8!}|JuKby@T?YQsYFhCbQRWY<@=BBmO2+~4)#|tRW2V#HY*reP7TUPeN1LO7K=N7 z2G&?8z&gE7m@(Ihu--seyXJ+kZi6hq+BFMDxGDqadR14J)hd=c`BS7W9);-t>8s=? z$35105Us;1`73GxBIsZ621p#0FK}=*rN%l-a2q(QruL7V1uTZy9Y$peIQaO~f8w#W zN0o3@!PmK{7=(KsSE*;j3;4Lgiegs?7PDvMtaLhtnaO~<+iWFzK3+>ZNtD|ZcGeJ# z%@+Z^u2sgq8-*z3$DY6%{OVUfakKUPum1WcF6ZNENoUhoCq?(Q}R>B?==Xy9aI{^`( z5?tUXpXyWy7Q`L6JW}weqbF&rtzZAt2(MEjnoR94^Jx4%Xd#-M;p`{t%uz(UUsNVz{5{;Gw6Iq#5%wPxl@kjM>SI+3}oU%oJVFAw~>I zk0TaQ#ddEap8zx~JjbxGkC*sQgTc8>hGgZJJ+mjgP7TkO?F)HdDv98SX#ip8eaM%5HJu6 z44{-wzvLBvAPGwrAxk)5O^R!rm$k<0*W3`}79UNPoD3UO{fk{#f(U?~i3Q8al_;YZng9RORK}~R% zJaQbqm)9loNu7KL62<&guXT>QOKgj)4Cd%23IydNUV#hpGu*MjzwEWnI~SnAJkvuP z_8|RvLCZm-Y_ChlyD?tSI_HjN5@h>`Ba$>N$!hd6Z&vrY))DCZ!tNr}iHxE@6dR|)#{PN8b>yF>_8*QyP)P{R)h^OE# zoFfzgVnt4XYTUTmzBTwi^2y~p=98ED$G<&vcSu$?HZU?tG!JsAPS88wk8CnQYl1Md zD1hTxhsoA=mH+PB#i@SrC>3T%;BYZ?=G9B#X+&fDsewhJ_%fR$*CoA=VlT=TjOydUOxj7IE zp+pJKMI#kv)F6D&Hh8Y}4teeY?@%k6@Xjo;1{LcTzo^sTpf^77hy*2k=Ffh27_#4a z_t4Z3ZbM1sUM&#o89{&^{L>g(=ovFyL3Uq&fC4df2JitdkO#R7g8jZ9w+d6%c@Q-k ztqF--$Qrp`w6DFw!o(~$CeU8=y3e_8d!18P{=gbQ z?>sM;EN&@GMO2s;SxAGe$o!9^HI*kMVjO)eX`i0ROETuiI;2QpC^2lfU{Z@{YQl4| zJh6?~Q^D4ja**jZ60r%Ws}4pxsr+~zmoy*#iaa-3%E}(1DZup>L+->oh6PdL7{M2K z6Bk*De0miZTZsZ%uL_c9)x~45QOCUeeTytFA83QQ)WC zeIuVJy`JeXl&;lx+eV;34{_YI1NskGl5%Gx+tlO!^cZi6dFw& zK4j+OZ6~Z!jH?F-HNFq)qrmD z68RMSP>1-{v~EEPsf(adqjIBikHis*+Vt;;j3z$02 z%O_kT9%04WFMJ(w4{ll|EL#$mHEkp#DX%43_2A|28KKRtgG(|EK8WZ*H5l;7Jdijp zpj{+RB8?(x5ojCqjM-6Zhe4e(1_lD)c=@ic2=MI#5=8Qd!u(BPLXtH_(LlEXk2$6> zV_d)bCM+_7JAQq3z`e3!p`oIu^^;rX8|M~hXQX$a8#Z8c?+0F}J zr_xIm3syH=s`Rn~y|LU$tUFfJbSu06gV5D=vzrw0Y z6k@4E4pR*VQwVzSz&Eg(Ae{V=aL8C>H0T83OsK*^l^K?p2Zrs%h?z0s27-^kYlO1U zIXWTF5%Q@9Vi0+_tSJ~{(zJ?{)hdxkpc@3h;&}z=v_M9}{gKOF5C@gU^D!_GE;${< z(&3514oJqxVUw2KI3K(YZEs&Rq^^*KomCQ8Ekrp&f^LXOT=odd9nc#oZo?<`20yx#suC_tAdtANfi7O4jwdu zKFp4ihxThwhhX4-D_fHRE7+ZTEujM$L;+ca`7Yva0lJ9rQDN26VH3ext%OhtG)0G? zbdy&d=Jm_YN=dznFt0KzPq;)<#beUvBd{iU;i3?#|2vD4+U~0Sw-)8PKV1}1;Pw_J zWmW~b*{kw@wnN!p8VdP*4$t zxgt;G+!V-Zo`oF-&-qrkwIml=CfEw8t4P4Ihvg<`s=7FU1Dz3u%Usx9;w=R}6m>)% z`tfz}m=Var3$4J%k_&aWM{sAzrI8GOp-sW6Dpgv0;D3vL6uyLFqvD83+HVe3D zxE#iFR#wLJbb8OofQ;lkC|$TJkzbl_#z;j%1(-IZ4SnT1)K7pcBG`bv=7|ars4A4p zS{Q5^TcaMgKJ{ z9-B!-q`i>E2YFy7&9W+tjF_JUhfik-d;sJ;3|yG}R%|SIp}6v$foh7ph9uHt-19Ba z#Gj7)e{0xB{H1Y&g2Qpohdh^FU@GhB1%^ey$i2x%|I@F}_=|2HeP|C=>QI32pZw+y z^`L+9w`a7#J`A{|eKK3Wr5CI7ccu8-|GUziAzX{`FXN6=>G zORrfe)fj<2!F>9u-=Bs>b@XIa>>u;TRO>3ofBBCZ)qQTB z!g(G2WpDMO8lfjEbxbyrNgNgs9oE)dLQhseU=P4|wA3`UT3>6aF4m=ve#}zkd~UMT z+vw@*VylCG+g20Nl|Il>=u2PVz+JF55FMJhD1Muv^ae*=Tzb#>6f9`s9p;v!+bFbB z_a&#a;1WG-H|uDA=5r6+HWdxL%`S9N3@2N$Z~F+)h=@!V<%Nw!kNY*=Oi#+esx zp(kUMFqDrIb1#yK=q}d_M{$9rDll3;{+ehJ7WZdk+OfEMjqohm(T1Usi2iJ>+9@T3 zf|U~&9rxRQXcXV3Y3}H5=!~mmWH{*yUQ}R3M6j7e0&`B5GY;+=Og8~P64uaj;t`O$ zO`Z^eKdxumRvSr9N4de`HHZN03_?JWaZQR$VpGJ3XqqyjG(cC|G?bp zZR3VkSP&<<5%x@(y+eSmhhu;cT}uZ6p+zI+Dt_SEh-Y$^Rp+G~*p~&wj#r9m>_F~1(yJg&)_RL=;SaSfq=Q$EV4t<6g9Gj1%s&! zYcu_w3L!fi#qo)%Vl;Ia7=d0WKibJFA=ll+%^WV3*ATKoqZr7Vf)P5Sb%cXz+QCrM zKI(9y@T8l`=fSPVhByhZ4{iZ8jU5czSgzQ{q;EGmE&CADEk{qwpMNZ)Y~Pvs8|NNM=iwHYeD}r2Xu+h^K%Dixc8DpxABHCdzQjIzq`zL?EF&IHZNb#iO01GQktqOysG6z(N zp};%^1t?%mL}oyV$V`?2v>bsF=zkbi8598fi{ll@asW88qJ%OPbjH5PD&uVyVk+HK z5AXvlIf(;g3UDSDg0RsT^*JJCB3GcCoCCO;SHhirpf3vwn3J538kO#?c!jF0I7K8b zkm$33JWxYu15Y6gSs2iSJL)#Ztq5j`3MVZ{ydTF|V=Vw?gWnjeXaL{i-YGeAvU3`h zXaia3ljFs>jm602;)CIY2GfdV7+NESNsb1@TAAGt)G%W-Gd)5LgC@y_^5Z$W$HCUl zsam6V*NBrVm=kc~N4UR$cdjrdH0~t1(1^i}qe2SE+DcOgk%1Zm zzy{N2TAXCnMYed*5Hn#SATC-*i40JdJQD>0C;vi8c1Ol3JHnvCISp)_xmW7;14O=g zQSzbC)36WXH@sQqxd?;gJQr}VGEcx}9zXDGKwtxx6s=`#Il@G`L7vS5lWE z)kBX2f{4AVNKgF24w`vw#4E+#M@&+qjqDYCXV4HGE%HiN7WQu#nN6@FqzRUw*b8A; zNE?!HZgLi7j^qkk#9kT3JS!WU17@5j<4Bf=VlTeCgD5EW=0fbvqu85g#NGu6-673F zc0-wYDJ$NB{fUr-E~8$HrtPz7jKYY$SrvnNBK8LLvcz5+{2*PAmUBSFK!JM)AW5tP z4pgQRSmIem?8T~Ln}=Z=aRG>20t#^bT|7{M?O`uLBfuJB1x&Hx5nOCh?1gVYI2u5O zcOgZNnqn`STfvhOVf`pWLJ%{CYF2HEgqnv7n}pR|{D$a^0XtmF6$sejYg`ooY%NFy z-b~X2l_Rk?7)QV1gjp1B5Rny=ONj^l*d|T3UARz^Gv$f!p9t zbZZzX$^%1LG>=<&jn&8z5oHp_gU$&8GXwtv(jeBc5w_VVXtWf6ai9(mCaZ+G4RLR9 zau#NcDWJZ8hE602DI{!Z`C{ zEKsbLq6vy?%hM&8n?MvV3n13A!MI28gXuNaVYcyy(shx=gFmHZu zix3XMPB093F*2~(ghmCPNotY8F*2Eu>4Oop28fdzjKUBO!oP8Y=nEV~yzrWfEm0Q= z0m7opT_SZ!AKTIQ>Td?IpT_~cu5XIhz-6-7bQVn2W0VwBy z=W{R2g=9!W<0jjpkr9$&BaFTdM(LDuc_GEZMPY=P9fU0lvx9UmOq|;s0vSLSH~~ia zodh3zq#)G_S|j+HSq#|ZZpdRaVo(@Xm#`tDGT@jNA?~Mgo<$x==Z;v8L_9q9BYR;# zdE;>o@xTmK`z80 zDkHeTQX!fcHzOXDqCA1iR#62KB`g}l-R$6s-iAb4G}F$F$r;>3#+IcE5xq+ydq zD`N9zj1snKIRXgD46 zkTS>4n8toEd<4%Jk;9A;QK{}-5|~T8Ngu-)upfLfjNL>)?4;eyXtAx)=npg84MsVH<3Yz`pb&R;wsBhk?ThQf=rZ`@ytMg5`M}q4938k z7o0bdo9;l4U;S5osp9_D(`u|6)hoI~U+df!5U??n&=TTr{kB9u*4M)6G5vC13vO_| z1&`2a2UrLnz+uJJmhFHOi2Kf!6W3-&FhvNwC3q>c9w3V@;e?$`ao;2w3X9{?a>24O zN|Ar@P>frK&>WNlcY)P)7~F>B7kP%x!F{nR9fNa#03dd4df0`x2FxJr2FMB54s2a= z1}|ZUBw>>zFs?{~8-yVV3w=Uj!zcs3z|!7mgmDZd>Z*{o*;S6%)x`#%J%NBE!YWxL z0c_=wuRNg)umkV`ZX(!;BD!5aE3sp@?VzuV8ME-C{6pgdzRj=Qt& zyoWUa4x+3i2D8wP=)UUkgp(YQeXt-(dyPs(@n4ps4{#Ru$A919M}MEO(1b1!#?q?3TDpV5azCaLqJ}6l!uKoD`1`En%}|!^5tBEp`QXMJm&Ed>M~L$j;yYM{{m2bRNYy}iG6n>vl8lN=`he(O z9CwBIjKqluT1}N0GtisNXC`|C69JaPrpiVr@+6@!00L&ql-aC6VeXkrIS7n@5ucnD z%td&mL%~$TGG2@zmYa}B>pj~A<2L=E3Y@N6Xe;nAG+QX~b~&zy%HsF**cc=e^uyL+ zr3!o~qs+^nIYU@JW~u?GL}1Lz7DAAmC0Y?o`DE5G_8U(KTpr0378Ohd z906L9?7R+x{mdqe>c&BbF^C&4IU7|%SIBI|v0}7P?f@b%hQJ$wr+7o~6mN*2?pAn1 zj57qqO*|WhIr|`G;svCu1^_v2nXk{znVFq~_|J4K8QS3e4?#?pUOoao-&*5Lh=|&g z?MncMs}A#^_B6|?2(zH}q_fQC0zpJJ7xcCQv*w-#75tIxLBX@aT>-S23%E15B(nyJh(T6aST}O?L|&R zQdA}+asq5aUV26%!R{YPrXwe8EG68>;UpJWW|9~tyd{}oLbJ+-JiH|#K3o^MxSq!_GWH;~=(S?kjWdCP932l|6wx<-KJt##@+6#1|`MI*6!Xab!(*~TWL{uR7 z^yVxkZwcE2TPBnLNnu>FWx?39l^=L+#xuDQtSTinAsPf*7DSXyh#y3*Y+2aZ?>s?flX78gs4)MGHRjVh5VVz@*dCbx-g2f0+D4cOrj z+-^JpI7xFmj0QI|y4`@naqk0jAv$8n#-caMyqN7uU@(d}i}KiZDCqzMTrF%XlklMC z$1vt}BX6w-H%CNR5-(=+l132PoX6W6xOrI=F}Z&QfcMM`OP8ZbOwQxJV-bB3jV9dz zNVUkrav-A213^`#J0FUYm?r*;@lb|>kE%L%8FmJ7qw>LK{V=`U=eWT<(1>^Mq;jX%P|VnO;+iuJ*?s$Sw1$dGnouDjwZ5m zP@$R?I`j;FX5&xFqIv8QT)|xbn4i0qV4UQiN;ErWa#`W2_pkf8T;=+Y|C~e^sQRUS z>NBOHl*FM&MX3x>I)PGZ{0KlWVi~VNfdvFe%XbFfLmfmRpaRu>Cg(uwhtMM`H|)%U z@zB_1#P}0@N5I2_X4p@emp6fNVr;bx^v|#m&HY81ZfJCd_VMTTB$KN{o*FIGu_d(hrrM59b)I{4j-xF?34n1>_-R| zK$*!Osq)l4`2l;i5uFGiEl9K1d7*PvcvS-^lDrB&&kS#^=Lcw#3^%z|h!09c6W01& z7Hbt29Ox@sPz5xe0$(<~wNa{X%Ui``n*2z3E8N01Km(*kBnns?>_x^me2|FZ*S<-m zKyt;9K+cN|mP!E`Fqpqt8JQ6I{J<1uw3Z{mB*=}0YZ!){Tp2&@bS!PtaNVzwMTU_(h1R0aJj*p#@R1aWA?F(|+t`-zK0vz??jC5Ts} zIA|fsahXPBQ$qLQrZmX2(0_NkmTHbgn-%_sn-WnXYm)(%R-jGncOD9HivdF6FZ3gH zgAXWu%{H39vd`?&WDf!?)~ihL5|48%=}A<+6Tr(i_bJZZrt25yau$vGsa+CpBIz*YVCOo+uHU&l4nEU>a}$ zD8>L1<(iy%Z|O0eRaa}%TY7e9HHL9e-*i^J%T|E<4}z(up6BNxq1lK5C-=Oq_wf*u z^VZvXq^G*%09SDiOJ9LY;>ze#;uQ!c_5w=%xx6_8B1K-()BjGWL-&bTwHQRq5J@Z` z5^M)NWW?w=?D`%q*anGU>nCHEOEHio1QOAgb-^#ZdHS|4YPVeI6B~G<>;4V;)P(By|MQ_9P>!K~xzrvKk`4`OFgMmU#PQ*I!KzTXzK6Qo{>)_^a&6yqOn8>UESmNcX4Q}rRh;!Ws zb!ul-h0qw&70(pFpxCJ5cyhBW!2>LV$5O%fa7atK<-umYL5Ai{EqqT#W0sm^HeurPuwn|Z0ewNkYnAmakvn{y%9l>y&u2_9K(j0Ddcy)s!~CYAcT zO0^pfBH=1Z^fsiYI>bILZtM7Bt2L&*q^BC(;k}=Y`X-VTITpFqcR9GrN9nJIS_$22 zS5>PT^`u=@0uO&zb(s20f4nQsbxpzQq4$2`tH8hpAbNp|hcH_`rI#9^KG84qQZM63 z(e+j8T=j(R&|CGxVWQ!^o1I)5A~Q9Z^xvE*x9e>VIu#TwjAjnjEx;YFbkN!i;3gT1 z*lSaTZom=_96~t{FS4~I5NJ}*4?NF86>fb}5%X*W%|D8~3TtYh@B(TNB7yju^!)m_ z%TgdK{IY~GHPR_(GD*d(2K5i3nKkhA3=s6l(9WH=n;NXP5tVShT!P*Iv)}Us= z!TRMIwabn!EYmynRR;r?NB8{?xco(5^|v;ka0$eW71%xQs#khYKeZX?-E$9hLB}Vs z+HmL$LmkyIgFR(}J!m$(9&6J(x~{+a8%Xu;{;J;k z;~ibKr`j7-H*rsF?Tg>lSK`?!jUdHW7pAWPyxV63Wi+cWEs%nRDI~%{@vr{9`N7O-M1tK)Oz11c_ zqtihusn0u9QF~^=1t>3Yu>!c@GC%{D7xz&IMg=aqbFIS7WO`Dqnt@Y+r|++h zXp^2VEFu9tJf+{=U#9%S{_5!M&-Stdww*0R+pUaIo{Zfau&sl$V*Ju*m@me#*V zWepyip}_H`OTTrH8iJ|!8;qrY@IC#|;LOs$|K9eO{^_ixZw7|~v+4|(iRwuMRPpwf zUO#xip8(@m{6O3G)_IQu!*$Na`oFlkJ@lIW)Xrv!J8E}-NVl^#>NECN!?(B4&;Ab< z`v3jhrUxs#&%vs+_$ZK*Iap1yN{fa#SUoF!?S6R38B7j`oORJ5sw^{W1Z)z$VGDRp z?{$bedW0}fm`yyh!P3T(-e8G4$Z8X2VdoF`UZHj}R1-RdzJ^l2fojS)VRC{m57{ww zFYhx9M+UOUc%T13!EYU^CIxUf~2yE>}co_>dYYl zhlZtP5Un0jv!~I{<|OC552*nzMZVBx%j0a&xT{Ryw8M=V?n&szZhM_`q#7}B4K{@` zl+SCfv0aA-=wi?GVvaQC8HO61Xv76mdcl#{6GrMyN8;4>i+a>i>LK-#?l1!9rYm*7 z5y-35(?^I9Ja0sE;DyY;=h0GNzoXST{{yFe*j#H(rkAUJ0OAk=W0wFF$$;0$p$En?;x8wC-zIN2T zMyh?<8TkYl8Yp5ARrre?A|ZDSg%?~RisWSd_n^Osam0W4f&LeMf*=*_O~ye0{T=XrMv0E|$0!j9N=Ih|g8rkSgjDJgqt&?Jq*X$18m;23)`&2L zi=0CvgZKw^_v2O99TM4z$19se_R5kF8TH%`o(Ii7=yym zdG5m3ouo?l3YmAC(ANPD9ZJ4w4xKhZH9JDDzsB#Rs&%^eI5ni?U!tL57NQ&C)H*eL zSp0RvIF+>S`B1+)PId3{2KWrFWd_=Zi$?Z>d4BYv;WO@8y7PF|0hjE;cjtyoMh_jY zx`G5Jj#m|Gy`DE-O;vB|qB>}AI4_n`aXb#4pgMD=a2e?(br8&&K7NGVm1DM_$f3MfG%- zT0CLe2rW)d?k?5KPf=$KqG0gN&0+`ERtuk*qc1OqzBgvXqYRjZz!^g_2`LU zsGQH)6Co|#uWy{F&TR3fATuKa!6mxu@<|Fm1*p%NB&#%kl368U#uJmUes}58$=K(g zhVKCLp~wMj=Fb#>cVI}kP-HLi;02Ra7j=(bIaze`-zF;_4FpGSukV?n{)NMXV^34( zqZ?kHjz?^&>debU#2^Btl1ok!6Du~gxJE&Vu)MW(#80!yj7Fqz@ zzS5hfs@mv+O%eBP-TibZF+KFjrz^S8UoLykm8;zKU8k$6zP#t2-T_>74{S4TC3A8p zf|hf$1&b&cGGt&{);H*#&Hz~%?ByNy^l0GVbBMM0B-VnyaQ+4}nR7;STo0+>A7>in zBFdmUK?fPl2t8`FI!gD7TH(>vzqNWpcqx8GBqU4n(tbN)qq^ZN$lQ0)jX+mhMz>mX zmg=pZ)_QlkUyMKukhOvy7YSpbv^Vw^=fw^{!_lkd|uN9GeG^-Kd_?56^`0?ODCYg=ninkG#;3{S6nYu4XzyR5x^`NXL1<8eh!5d)@w6M16kA>N&`VWu;YZ5D z{tRZ~euQ8$jc6yV8eMAz@99e}#Si1~P#mK9lB;ZJW$rq~n=odQ^@*=9`0q1n}V@zMgg$0f?FtzN{%>jA>{;hiORce>cTO{xypD~7L4n!0*nxA|P1ofr9 z;wo|YWf2hiG6Ym>UPu9hIP&f$z5vuCizo2~0rh~N8zRb2e*HO6vkF!5V$3O0Rm>^* zG^XTBten6BJ9YxIt~2O~L#a*3D2rswdKgL)>H;uk6DYu2k`Mt{F3^P&$bnNEt3o#< zV*TMHb#U$#vKq!TPv3pDDmIifLU;64es@5a!!Sn{4MmMYOCTBGX~3jMkeu`(KCY$P zgt?1ZP8I|ulUoEDSXK8cf)4GaG5O>Gk;T`9Aw$YALJ^SFxMt7=(#II^V|)gY=M$09 z7NX9H*A2m^SLb!WXSrlnSep@20b&qc0Lfc)&Kxxe&P94+pw|SK;vi>pK#2ers5&zQ zL_=gZfMAH+IjTzI5L4@;w$lv4B5;+Az#L%_nPyf8m)KzL*cY2kvk@1|qz9Q?slB-h zS7+)&=BjuX_5oH7bGC?D7-?hB1t7dqy3qbywLiSO@1ne_yyhBpK~Ncw+pftdu`gUB zhL2CLQC$lFxA&A;nxU}(*1U3k&|)=MZwOKur1AZos_TEPaYW*>@e??_>~=>mflc1ESnUZU`DC#{ zlBy-DtzoV>J!t6S`;YYzOH}^>**H^3r?N2@Uh@g$b>dMW_!==pP${tg%I_HFxIgF@ zmSk3Im%C*F_q|)~x+A?vUv@Ww!YcIlcdJ<)-xmjRY@I$LEXAVYlf6Fq+^ZjhS2SwFf|mG^miGbaMow~f(M<=z8?BA~x! zh7)MW;I=fg{G^NTRY%j($Ez_QbeJ2%&`4FW7iFn_C5af%0n0`h8NbOw86v-U1C(cpj#P15;p5P@ixUj%}r4?yO;j0h%+P2KScRs`)@bn;M6htURm|W=f>_HBJQv|<;K=o3Y>=!jqNC+to%6B#frw_3%PjK1lqLizQdF{sML_ zLl5*;A{FIw1XN-RI&rz$D}{N{Rc$0($NiO6K-G;x-IzV$Bf2oyvHYIOBWZ;Ok;A|W zDu28@2?2Wyk%T0dHX@)AV3z2zUV(JIAy9hO6Qg-GP3NHJ{Ny9hdibL%Xt2@7Q5>-& zG-~Md2@~2xxk9e`0^wl~2fm_Hu))*{m_mSb#xX)|2Xior(yZW(>W*k7Sdm#6pNj$w zG6dQRSj9HR24cw!>Pb*>_`-@}ypj8cKp)9^sU|8G#|SpSWjTfqtR((||KTsz4}Ys^Bvc=up30M94$wHNeK0cy3tKn@JBsy}^A!1Tcvf5M|-y z{W28TN`B`TG?cV@bH7nQkV=b)tN@{7XmwGT$&AGyDN<~xEp3hP*eqz=lpQ5{2y#bK zy-GKPgx5s5S|1iiicvmShbd?PdLE*g(VijpO8`$p-V%hKKDUV#-=HTvglN}FeeXl! zI$eziH60mSpqXKgk9?L(_Tf=eJgJ?Kz@R*qh zr8JAWR+m1yV^gvI`=cTxG(M^>%aRWe-6T2*nDP}bU>$GO_r0L1 z3ueGi8)a<0d%gbp1=Vg3;})v0#u>d+4vL*jQIkF#o*y(f8&Lv?Vl%`axijDWL?>6P zzTE-=9gaFM9k?Hm&&JP(=Tg`vMA(@ynCKft9i? z0CP*uX5-ZGUNTLl+# zx7XD!g@FKUF!3L+tK-{3^9B^l2%juWeC>7BujT1TXoYT20fN*Wu(KPLQ^I12*#>dXG0$ zzZfiYQS*C3dfFSRERBL^nJaf-B;*Dxnvh%!7d7V+3<_qWq+a`m8rV_-E*6CH(C<3F zUUfrP`>j{qtqUzZb-g-AeW|}%ud1l5@_5Y7Z>s8jF9M?mi^XX(FlRsu-Vf~TSwGe# zhU`3Jj}SnDDqxU6v26I=_@+80X!n}81d}_xl}7ch>`;#N87DY9q(Ud52;u~n?M{Dc zYUaM3ZcC4QTN=CQZFL4P@VB=Gwec9G|Ghz7grxr!%h3Tl)SRz-PlC6>y+`Vy8wK@` z+bG-{#;7)W_C~d98voO~J)@Lg+z4Bh;Q#23F9_-*2y45jKkt22k^0)f2?EPpNz@pg zAfPnVI4IKrniY&8GZ^Kp@u2RxXDasBo9IITUvmv_jU0Y?w9*<8v_#USfan za^u0TlkhSkT52Nb2p6;;W_5+k^48&U)6^-4S*0~Vb8u*GhDJ1zRv9)3m$b;6l510; z!XblSY&(bdF6iFJtG2rAWtG1}Ed20u^*N9UL2s95@zEW`yLC^!V~(T9{Z{`GnV`pQ zgG4-*(f|M%OcPBPC}S>);_n#`3Vt@Lq{D%A(5lP&UQu6NI>2^I$X;v=k4IfRfM#PDy8BG&tIhFgm!VfU*O1 z8Tvzro5biK3#RUL7J3*)Fkl3RAlc~-P`WurUzWjWl94PQFdsTLswVajxUJ@0c>{l7#(>QQ~m7wU*I zdO8itUZ_GaP6c1!sVUv&OSSJ#pBrq1wL^hv()q04-$%s@z#72-$ssHByf0NnuL|g9 z?&Y|~ouaXw73D-)VLc>*UVvOAx)S}>m#T9fZdfG7x%!2#)wI;lzh&quEQeaWPXX`- zA`70&Vx8pq;V!)x&h|n(x!_HJp2*4$d6QX#O>7&0^U0T3_Bfn1q z=%HPG>DQ`f8wg4xi0@po?W`3`B5DvlqEETm)1$dau2RD>db|-DvPS7EF3_b}Br{JK zS(xuO@FZQcRYQ-wz6Gc2O_{6k#L!5AJIe)DkO+tnBtr}Ud9|5)R`4}QfA)>~8^I+i zGgeC!YL=r@6&H)s$xcw3+Q5AJ@IWL_lH6#wv=o z$C92@S2EYV{uS?xm0#>D)-8Tfr{TyoRdT4~kF>FNsBB3L+iKW)y{0j2$+p&!80oyWW~6Vl zwYqe=ni?Ed9Alu3IDrX=!E21&L-1Nft1U5HK|;aOGFOl+)t!D-?PFJc8Ss(@o%~gm zrOc5c?2cStaIFRsD4r#rk(7YxKGAGecyEc|d_9uHKS46)&c|yExAFte&3KZh;4h4~ zuseqw^fiiBc5=Pk-<`m1t+-lc!>?*UD!A@EkTj|NfOLcF&g~@4k0L6HnE(!%oZnQ3R1YXBg8G$s zMC3Mt3cLYdtAr%sE>lzcN07$0Z~+3Wn{ru4E?a_P#mhR7hEDoN08L6fV6tI&l-xE9 zs-_=9gb)^yLCMd!#_=Oj_(ewH!y;FhSBm(x7zIMA`Wbd30zaTUi_pd)5OAQO!mbhM zWG04m<`9MJj_MBX(zx`b$PeIrfXdU8!@T7(K4JtAeGzo8n02ASh@?2m6 zYe7R3GQR->%#_MjP}yQ-szA7>S#D+_kIci>K@tRsWPQ^gs%^~t<_mgVs2l&l?bGam zmJ=N6h2?n;EIwu{muuizOc_O6rnpFnrj4;6XI^#=EL_yVnS;3o2Y5v0gW!?Qs0cIQ zt0Y&jD|+$vOyk2%bItYXNEh_73Q-_w@hrJ|?o5k8DF5zm#rH%5xxASraDYnTcwPX( z)tLdwEmhev9!(6&+N}?4vsAdlsB<(>ZCU9%mt*Aph7wjz40QL^XDI97=<7d5 z-2bnI7PB0goRZT zg`Y=SK$tj2L_Uv~DVF!Og;i7zuQsH{3bNn+lc7q_7IU)*YA&`KH$+e;d9I0|9A^i= z96{sVGe?;`ERs^=fxrMUD%i=_;x&=f_`KNh-AI@fgC$o~arvT@CEpZfS@N!^5SA=c z0~lXea%9LkWT?5)I4PPgY}63yE;q@ZNlM}~(R8I~SX{e<%lMWU>L!~%dPKs-;8ck$eMbU6RmVuKZ@HwUa2n5#Y>;o^s-k@j|1UTO>le#=)OHsK8ZuU{muIm62%$4aX z99|8Dfq#`5m(PT9AHIFSLH!B_+>8MAPT*o3*%wZe5<<3`wE&=FyVa}`me0*yU|^Dv z5#odVE4v;CxeMbi)>1n}zEXu(u$&LkldDh7>O=mU1J@oCW=bG)LD#beXgX4>SkKp~VsL zdH<4~%Z^_mvm|dj;d&%{?n|mF4Gt9$Z2SDpgMc8+73KNG^0~YymU8RK6=TsBwaOO} zAazB+b3VTrOPMtxfdjb8y8vft5XZ++UFR5Q$5G1V#{$dpT#TGAQrjj@8Pgmi6&DjE z47+u0tS70^c3jTFzmKB^GZ)Eu6XW;hiDn8nQs9seVu+4FaWw!h3H8`8u>i*}bW{xbQ!;|X=4F~#>MnQ90LZJ( z0f1H)>yQjUv3kRbWCp(H${s@6=m9J0B>7Kx1SB2QMD5WfkqAn!+R8xgP;+zxiHBEI zgOhRjDAElYhhcn3W(#VvU2BByldWW_ypWHTZ?Npiu#1u(-3Rw3A{pqmgIJ>@dJJ;I zcq0q7QMbJ*#kAIzVLw6|qSNr0lPoWLu*?DWN^X!wMXMp{xqo#^#g>ACd|PBClR^y` zE_y!5S!6q{(Oz(D7qG}g_ynz#iG5O9i)LWSvy4Ng_Ay5Y8I^FScKP@gLXm+7U;$!i2wd|}fvJw(a2RJYl6 zom%d0v!mIxc*@&%{4a}}cU+7}&A^onlV}z;Z#E^7>|oGrt8V0P;94hlB@ zygn5Qw(Q@4TD8Nap|E7Yebr*_Q1Pk-C056K9llBqvOp@}^5oAw4VM9n_r700^)Nua zh**GcSQZfnE2EA6BI5Mr?ddqJyRy1dh3lqLEiR08l9QC6qMCUQJFf5nWP41E$%my< zo$5KFRjeX_&;iEdmKl_S3y@VIhvOQ;qJ6+Vg17Kc%u)lIMi8)6z*oaZf>+$D31i%= z38B11T6_;>%18nqb6(7+DB1Bxane#jl-g1Rfqx(Ix0FB0ptuCv586hF7;Gb?qQz@I zti$C#ifo__OM%TfDs`S_!sm&6^LxA${o!iq0;ifYP|&h$lMkz=7P4@Kla(v5B1+EC z2*5zTm~@`>s`7|Pj}t6oJ3TPUtI{C&V3ecM!$uh_{^CdZJAd1M`%wmS%Fdv)|Gx+J z$4u%v6BK2sP7ECNc9Yk!#HhDf$Zcxi2q>Zy8UQRh833?!Hf)q+e(b(lUXU8ACzL0v z4R8b)uvjQUTyRzEqFO~kV&irh4VGCHm9MsoaGQXiiV>j*mu#M}28+mQX<&n1F{VU( zLefK@Dp?7xEYqKYn@g;*V*N(czM+2VfjBTzgZRD*Cs9N{3)5sgU4&V}GaFIuDlmZq z^T3%sUeSn}rtVbq9i{6e{)h(n z6ddiTX<_{=s^3{|*HG_^+L;nn&o@ai-iOh}(oxp55dAsdu?nLo+0InQ*rG?_lsZn` zF@9d0=V!$Ni2@2DB*vXyq6Uf*jTFWWT0Xn#<5QOZ6IT5SpSHu!)UP8#%$?jn5r<}) zHPK6s@|x(@^AJ45U{FT~91q9_JWgc*sElylQV)v;iv#{~t8yx&8Nv*a=wt|1ipu|x zUuqK2O3BeE(MtKR5ZBnERKnrF%ixM%JELP$lA}@gG=t*6B$%!usDKlAt5R3H9tZI!;orkQq~bk13;yRu*AnvgH^p~u5$^vr!Z%M1xz+>YF>C|$X+y7zY4O?$$WiGGiSZ0fL_=6dQJHy1W zOd|JfP2#CyVQWf;rwZf_5UhJ<9@G{V_s|xCW<^Z`RVV+YHKoX+O8NVF>w28Z3%w7d znkae=9l2tD1ajOVsEeb0?}VIunwE6RwNx7IyNItPO*B(IwO;e0st$*#G|Y zL=zi!p@I=EVa#zX%b#?Aay`4?Gs4LBIR&&*El#6<`=|;&o+vR66uF(s+UX>N9dy@SW-Z9>Y z-hTn2TUeqar5oBhQh*BJP8g);xS5Ez;NEjRB?{k&(vJVYv{y!^HQ7Mr{U zy>Y8JeuBAQgZ_#HT;WwWJBe)+0V9988y1f&eDaB+4S3MW--5JD#HJbb!fi`ZFG!3S zQIrS!<35y?3=0r!V5%7fV#op_`b<0$xL+U2sGi`9*1OGM9}d$eF7HE~3(g{2D5oV8 zY+Av_8&j;(qk^b}dsJ`>^rxDG#jC0~Mb^UKkxiZ$PJ=nxI9=G+mpTu|Cs%(`yZu#M$DRApC9-W7ofhSTN=QK*(yrE>Q?24UTw0>-6D03? zgHl$sDw59Y5{G}|%`05drx!8qa2z!+Y%w7{8pkD75Wp6#7vRv|^duLQySI^GT;%Rm zFN6T}UbJXJW&c*C0AB=n2lzV_6|i^^deiUd0Z<%Pp%WNIF-z8aVPQ=Jx9K9K$E0~= zT8j9AV_sZ%4JFJ(pNmKa5Qw}m?_gpXQp?EjT1NV7sXs*K070tDI$KNYxT%s_s%~Cp z1zI8VCQ!>-LU_PV^1v-Rs!QOPu0oa7YDVXP9-jrp<+tF+uBL)`sEyFJ-n#1o$^!)P zU4vEb1b4WG^1#|=UPFWN2(dAh)lhiff~!f6ZEw1oM*sR;S`MH~=(Qht$|y?a@&Pc3 z*}*3UP#^OV44E5UOCNzL{y5i^H}YGscw)UCPwZVT4ft?pSQ<9>Gd)7i-c)u8x<3S`o_M@+_?BlrWDb;!;hV_Wu>J|iknA;5iO7J(i zWRA&a-(XbiPrUyws>^)`Q$u^O0%~BAYspbUyE&8_w+k|+2e1J3+Qr8Q(-9|o_o^Y} zI8a@#8A^GQ$`R$U;Zswcy2*GoX%3KxUgsy=L!AJ^%OBUj0j+w1yW9Xklg(>xpmG$F zJ`4*(4&O11n#Ch6f%yvILVAG5eBM8d8l1lmcX8L@)GN0;c0}%xX%o@R<8hH8mQa}& z2!s(so*M{UAn14R@WAK~<~{`wb{^q^0_vippIrn8ST7Wy11b*dscB?g2R46%tQ)bh zh1Acvm!Bx4jB{e&@Fb=3&O=m7b3Ohfw3#OlQ6m5NBt`MwC&{bf3r0`}T=?+P2uy-q z{I3zT06la2PU^;cM^f#m?&;*c&+)_3&qq>Xp1)0gkdHPA$d`s@QGps*Ksx}tlTp^Q zqHY(sHV?`?)9y0aE+G^+4nK+PzD#jE+A-RfphBmP7#U@w$_T-9BusiXo@q zPu)XUXK2L1gFlV zr0|+z@*Z7Hx!N#i4dwZNB=Dd$7{F@fR3{kpz$}XAf?Fw)C$FIzfpDOlmOHTcPg+A& z;!U8bUK9>a{SX&q!oli`Uz!D@%dGpUnp46r4y5`` zfl2^9u{{U%l7dkAhb<%Fr?OouiG^S`T7t3S?K#vhdr7O+9Dms_8>&7>AaZV^5 z#pA|d=d|KX+&_+5R#hU9*6-w#4`O1aPQXYE8;{vle>|mVfo;dr75d|*@pNTJaaF3_ z4g{(FUM__ITHrsJrMNP+v*-zg%l2A2!A2`Efb2}5E1iSfX96w8Jb7;d-G)cuL2OQJ z8B109)`>I@TRX#Fq2b(h5@j~pCD}vTL|$=$t+}L-8Cp+fDBnAY5+S-H?k6JiXVJt! zOQ_M2NIy0I2CH@)e7@27(Uoe3E=RL%O zi>aP?^_h+*IQWucx>`H?U&Yix!kd;*YYiwYu>osJupqw9hf1hv^&15Tzz7;8iHo}* zOK1e`zl%pqAvoUQJyWR5Iivo&Qfd~dqt1O7!$|qgQfiFxc(jz}*nS_x3#L-7dV9kA z9X+L+wU{P2_O@rL(q8z29pLO8*14R$|1|1>3uKP2t%Ul{6Z_d;6#L*WihYQSVaRk& zj|6J+&PI1OqTXF1vtgxin1@cUBt;n8hL4}RGb)KB*+y9&`@l^A2kU0ItJUrq(0t$n ze=vit!p`SK6mjfgBE8Cw-BQUCdQq(Sz^&8{VCt&*)PU>V2Jt(ae<-(zO28DzRS{L2 z+aZ$wjmO`v>c5;jR9pMf?TVy63k&RfhaW*Hn*DA_ZW)_V`uMU|_ zEmNQ*2xS9r;Lwx_#r1xCaZ`$Z+NcFSFWwSN;>+W~2Rx=ZzH$#ZnT1<@vk zB3c*RLQTDk@0dgH0^E+63x&Q5Zlx}QYLQWudnj#yRHAZl8yovWlo10&z4mfD4m5^I z2^T{U{0IOFd_V-4E8{{se zB$sB=#^8PHLR5FgLKhp~w~#t(m0nxuuD#dZOU)C+1PY}>mqP&v$N17o<1V{bSJsd2 zMVSZL8B4j`ZV{EB-j6P#hw$imAIS-CycKZwKTwly+LZmaDF-WS3dj=W)w32dvY&>x zq4q7f%<`^3RMN^?i`{|fx|l2ioV(a<%fZFksa5Z%g{aQZhpCXizn^03&Jy!Cl%NiC z6p6`O7pyl8mcW)HtRF9ohApTHLs!lz&VS(7=khg=;$N7Hy9n^74^VP79CQl8Z}{zV zmx94dp}+nQ?^;St!`lgxPJEDB>HuH)Aaw~cpM(6~QE;T~4^kuj`u7K^o&HE#<_clM zmqBNbrF|JSNL zsdraUvGWf1Tj?qhR;{GA+NQ~0fvp{ngheIwOAX&2p6>r0FE%AM0XSKU3a0}LnY?^ z4)LEJsn~P_8Ii9lb;adI>&ddY;p<%rTD%@8?+=`^L2*LI4bZv=1dMqvy8_1Q4V0~& z{_+M&*B}4hKrQq~qm3@H_1g&4dN+I?!d3P8+ZWay&QiW^QziIh`z9AHezeJ@+h&_7 z590jf%@Bj0=K1p2#hW*)z|dg}-2xhba9^Ka+X9#-vf4`aZN+kx!y?<`(Pt|qj`WN8 z1wpmN*no&%5+Xp9>C$OgYbCORQ2Vifkl^zR391FSY8%vP7nH%AwcKl)_R$5zIcQFP zJ`$}bF4=e?atiD2*bQQ{soI3QaFs|U$_K1NRsl0?3_wh?G1Oeg7;mlF+2P9Q`mMzd zYNp=`b^y@t;!QiCbn4Dc{|NfotbGq)AsyJv6W-f=+aD`g8s7M$Td?YBi;vP9^Hw~#4Q0ksk1FJI;gFJECg+p62TCEeJ)pu@jd71G!)XVOzGc=CFe z-K;(0vYQh}sB>*CFiQ&bc^{R+(1n>?^~p-a2xto}=Q^LF5aAYAB|b$R+CA-G6QTZ* zv4A2;R2cXVQP&&z3c9u<<^Ku`iCui(Q`8e=2?a)d{`M)=#Rjz)rB72U5bGmPW5eCA zRjgcRQRpYs%Y$qxFTkdHgs>@Wga^V0g@g~)a8!CxfJudEGI-#gN~D>sd#JHa=@WZg zcvklr_s3BGk5$jO>xXwQu;kHaC`wb{&E8>iI6Tuk2+hh za6Wka{k}@L!@&J6CYrV1T@POH|A;@}h73C3VjcE}ymLUYPR_HS)q{NPv#7z_JndP9 z!pon<((x~T?;xpeBZe0}hy7DGKv7PUgE+XWYN(kud=~0o^k?eaSD{R>{=(#ByGFI~ zsvuSMlMrwa1mOiU7xCIz>=Q&nJo#0s+fws{xr#)w?y;>IJzlrE_$YY7PHcZT?@%Q} zSh;T8fG~ISwnTgFNH7%XL>Cr)`)6ef|n# z235cYCrboRJPi71^9nSN`e`P*{oPlnGniYL$QGm)?z@%h$lY&Y3R%2Wm<;YNTi`&c z;45DJDk{0{RVbmKdX>aocH!StIr=LfU;T}`A+sw^P-+A&V7Zm^U&p>l`3Y!CplI!w zZIZd9i5Vd;S+7y4JQw~QHQDtVgde=rd!44aG0Ww7AluyJM!G|udvnZmdCq@>X2>)8 zO^Dq&y!K5TF2(D|Z_+Bf9)F+m<@LUIpcVS|Et*m>?9I2SG(2og_-k`s_b%ZctMIJu zIZ4whhD~{oZmbw~{rhxN#js8v&4^@ z07}*}W-D$#yTkcim_U;P7Gk6Lf6;A38R6SX&v#d@<;t0bmhTn*70#)>70EdvM2#7_xI6iWfg+s#&U%y72x zfT&*8}FFoBt?P>pCG>dlq*#&iZbo01~L}r)P}GAis~xwfq}xpwE0 zZy+MVOs<#N0Sn;xv&mfWA83nk1zO%ciq9VUB0* z|5E$3w_X833$fvcn3Ti`!NFWt%-BKJ-dmPbG4&$Gc}}M8Fsz?MJYeb8a=wUwrv%b;L?UPQjEU zI|;C(j(wD@2;}u&P5%pP>SQnJW!gLcTr$__Wrs`ln}+GG;uypUA{NV=@cMaT-{Jf^ z;JUs@li~E_u8z41N($Tun8o`X9E(P;{oTQ*9sFO%w1pYm6=a(IKg~gMIU~Zv|0*)M z)#G&1$71b4L&mR(IM zC(=AD$led<>c5~X9PiJv@+!|c4)&dKE`U=DO|s00yndzu`}$#lfb1_ej#weUQ6}UE z+2F!ai1NY!6I+AbpL>no$(&Ntq;r>eTq=Y1;H42Sj5m$s zWmmk(jej;|)Hj_F=xtz@Q}<-v=QEeY;c8exUk=tE z!?&lIwvkw+Z+G~B&$L8sOVUhljPlWR(*bYQGfYqWmcz3%Oicu_4chB7t$(Y$ezvgp zGSIkePR~RYyYqldlOfPzC13R&`6>XgO#Vxz%c=Y*tZF1?dhA>5t!kRa1Q5f4MxbAC zk}&$M&d)crYu0-WO+Oh75OYHwh`92iX~0VSFG9j!G%{JWZd8Y%>Cv!RmE{pCo}S#K zvFR#;LrG(pp6!{%D;k@Au=4o4vFVlQ#&-=t(uE5FICST}Sw^p7x--j&Maa8Z#xKGA z_Oea}pJ-+p)#xeyJW*>XO~?x(>3^w7k!oi(H=QC*nb@_ws5O}VoaX44yCQf^b0zAx zo14)RYi0{@i_y8JiKt+!E1wtIBJWqcD%Z58Ie`@MEzCqWMXy>FBA+p_kJ)0rZh>Ov zYNA%0dzn()fR<*Qq}eZx?+;tDO*!oe^B6ttkLf9N*To#%x~I)?5j8`e|EpH69(>84*=Q z>fo|=%7aAgK)l!6376!v?aZ}c1y{FMRxrE0X(y!dYt8SR+1)!HIj16*FV}80f+#uiu0$4*qc=`oWSWc?zfl#M+lJ~igrM;fKarGZ) zxTLJOjCYbi2vix|^ehNr71Rq7500<^Y>feSp~^8R<%n(HMOh;Yq#-?bpG9sKUKrn7`)UT3bQ&t~&2 z*O?L;>*4-`O#LeErSSNiI|v*c&%p!jbB+7_#C=}idxsE#HY8bk3$Nvoq7ZPrSlVG? zFOt9fAz)p_fG2t3FmTwHhr&=1H9-ga(a`1CYsI(1bAq3-NlI#-o1`WNE4;w{7s&tF z%JV;X{`_%2bfr}~eBLD4+`j-TiEUblw2ndgvB0#I4`I|&gG&l6f&^7Gs*w;Jd1xZH z@U}wJOwuR`&v1!b6bUBA`yBTlVg6XTmf|oU{}5)U6-zr0ZNAK7Mwx~Z8| z6v;1-GHDX&^eA(mJ>mu>R%_S0s%$wxA*RP}d1aB&1CVmJ+}MA zMvfm1W{3PRSygFL`BawHrp9>>TBo5(T;8y^1YpP8#@PBpZK4z&*2n%ovr*6_XW34`xj5GJ33w-0XC)(m6 zJ>mAzVD8z8YPQquIA|s^ZMUlHC4aZd7PlTM)RQ@Ss4(zJ&zhPEc8b8AU}K&*!6Zdn z=aLTJmuzbB$_XYOHg(%4*lzz~g6SyTpF7bsaklXF6V0RHYmH#ihb~M=_EwJYF5SV6 zC!206dmSh-YDBz!vav?Qmrt<)jf+jD^LOrDY1b}tT1HN9XP6kmo#1uHZ@wRhePeXYZ5%MkMs32UO$Wu_SSoEAwv0k6kmr09 zUiRa+n8vu+C#*i+TT%2U+~ijC3`&ucR>7kErDz}g^Fn=qyD-kv2g_zc(0IDSX#6k} z75Oy0B7@3Nk7of(wb5N{3%mo9O-_13L+%CtSy!dDkaYYWd z?KFJ)EBJ?4d$(DJ7#;30k3{2>kKh)p?`jx&nt3Kej(0h*fo&m@}e+61m>e2F7p3MZBp6^@$Wo7kkNa4HHaF7=g^ z7B$lnMtKTHmW{iy$TP8c%*5i66SUSvrL5JJ8lm{Y$)kLs)frhhS*kyxNLw(qa8i-K ztwo4AY1&l%HcqNq%e1a|T zjhI%Z{pm(=`#rQS6APssWn&9%okmV8l}J8BaodaZzKO+?#~>E^c&gUCP|FXNXgg{m z`cbRi);A3`FD)85v2ffZU*Tvep=f%^xY8NYmu`44^?B=S^BGk%5izuvJ-xM0#uld9 zLDx(%Y%?(8TC~4VzHgbSy}REsGtP;@^%-w@VrR$j=MR}gKKhWE!0ncszTA6-sls0_ zN4)LJO&x=H&wl8My)%Xntun`UuUu^|m9XbO!=cX@9=Oi<_}^>I_T5|7nVk+Ve$eFZ zE?94_k^GOG@x;!F;m0?aA-rs~r4{tT=cVk37vi7%ipU$1^@SIP#<9eO;~b<( zL8K;;8cw2YXq>|TSv<*1Cwbw5>&|GOlVm$6!xt%)Cn?F9Qh|*nFPte&T$uC+tN!&O zEQkt`B>#oRiGsd3Wxl<_LKSl57-=%x|tDR=5*#B4`mdtl1 zo_hy_cojm2tGwdjs%3^H9?-p)6(q)P}ed72D z(&&`a$4(eOV|+;3t21%R87EH}JN}F_CQhF*_Jk=DCWh*;crh9{b1KSBlt$~O(xy*5 zZ3;%k>DKdqa~da0n8QJwJmH+qoyOYyP`&D@wxv*g=jHI`aHv7UdZC8(8fr(Csz$0z z>w5KECa8v5=jS)BmtUISB+@jTAJ%2MG!m}UtX{o_k#e0=P%n}n@w`Y`C=#~+$|A}9 z{D=iw`!e_l={(#7mj%H zUp!JLT#+AEbx^fV8-=$v(P*3ee2t3Ha9flvLqjTDfvj*vxOt>qI1+9YZj+3b=0}^C zhofG8)N6>pEMLEVKDzW!v3+>Iu<8)56U`6Tse{bsk>-{8;o{=_aDH(Z#|3TI4^WRVbkfm#Q{x+jZ#J{(b#{{!o9UKh~e96%J{f+*I{#Jjdzt=x#|MJ}Z+CJ#4cGfr#IS)IJIBT6poyVNVopsI=&Xdkl z&eP6%=Nac&XM^*c^Stwd|3z-2-fuhaI`26jI3GEmIG;M7J6}3qJ5}LX;ful-hi8W` z311q%EIcQCdH9O(mEpPJdEu+VSBK|^7lf|~`{8TD3;jNMjgR z!}o>n55FCLC;V=BNBF()`{575ABH~)e;ocKyfgf1__Ofm;V;5phQA7b9o`lGCj5_Z z!Hz3yZ4QS+ZtKET1tBlw9_u#ux8yZZxB6e@HAvp3BYmr?{O@<&_2nCVJv}_!#|wGN zU54b9cg}zDwdZcRWB*L@ol?4L&8wHZ^zmVt32)|(%Tj+yy3I; zp-lQclK%GxU*6#VoL^kJN`9``_Tq+Wo!#%37c?w=fSETx^X~SIo3cmppno_rS0i)j z9d~Wp@%e`A(Xa6@C}@~`2$`F&U$Sjpjou#)(qG(u=bWs$J(5bEd&f%;XH8-)(syjQ z^Qo0Jnt3!x-*x|;551N(xyRVf&u=WKeD%`zvIq4z(yN|+b?bdK+IbS`OPTJ!mtQZr zqnIg+lz%RJBF{3t zYU#F>UwrtApH+SX($Ven3+LYOMo|_Hyx_lA*r0SHa(CVN&K;}1cshGfFZ#8M8YDMK zlT|xjyy1a2PROk9OX;50ZRnOs-keU}_}W34M< z(zhq8E#Ow9@4EBzTVDNi{f2Bfy~^?%K7RDo2e!O(N7lTz*>;}1{o9Y{X2b7wrsLn% z7oV?|{)R38=CZqA4APU9Sl(m?S1_Gn)87gj`p2iwRcmOwqyznzzgI2&ZKTitc+d2A zSbqEStMB}Dd$scKRD^u5}Th4xt;y1o40G#BdQ8@ zAGb?fhhm`t#UZCpA{5mcu#4* zp?KF=s5n$u=yvr~Z47@et!!#hrMmk=TXxUsREGKWSao&F{j^G~daU&uc=(Ikc=+*9 zo2HJ6`>(ZsM8#M6`wdg-5dXstGmtc@aJ71wn55eGT z7rLQPVJ*zXQ=ay;+si%Nce>72z5O{|E79IVT?b<{k#4yhO>y_8s*nFo_eOsI?gygj z^SYm<`rh<|usZoC2zKOb{qd5S3VIpsGOh)03_9yn;ufa%3iiV29h4RJ$x+7QJ(f6z{>U)P= zs8j?0rh(U??{0_AQ$77>4}GWAAWS5Z=;$6+h9>j8@WEVIq~)~_g%Xec7@FKJ6mp07 zFAOTLU4iw)uv!2Hp5u=2e;U+L9pkqhJQ1^AFt{O_y<_k(NKr#hLgOb7nTGebhH$E_ z4;x*A+By;H9;%IDh4ET@vOCUSc-S?_jvqb^%W?hTu{u(SU3d;UQRw8w!&f8klA#Bq z+{U2^HNvkoY%AVB74I5JH9HkGKO#MMpCG{3F|ty$!Sl)nQ<>e)QdnV-Ny1o5jN1dq#_|F`5I;tEv;xB5v|K7NU{#_%yk`n*}U~1TN z;<}W7nz$2uZ*)VndhhY&diRfh>Cqh;5G3tG!xC!RfZ|48Zd~WN6-ABwj-wj*^+%MW zQUBzKM|bmq7T(rCGV*~rpAG14zab|CoEBW)No z>US8`2hB|#Wt(g0e=+LMjB(WH>SJt>8e@a3iD#fW=-5BgXiaF=Pc>RUtI-TJ?;Z1J z8U-|~O}r#E@$tuT;u$!$`pch$W3?5pml|U|jPalvkj(S_v45t~n$WD9YP9ZNR=mrf zTkUG2tdnZ6&R$1(+VTI%DBV<}E*h=pw>hCbra$t8?yA4uxcl(!- zlXli!@yMI(<>C>a>}Jzf9_g#doj?|Zw>(lQIDnVeSr>VrD8Ky#p}4~*Gy(aYGGRFW zK02Yi+S1kz2ITLWP*P9#ZVKjjJzqw7iO ztZSc3jd1=+%rcp*_35myCF_Ti+PZe|x(_DsgJgG{)VgpO7+PWCH~C#>K{OX{GO#Nr zwJSQ_&i0Y#Q{5aikKKGesckCj$8^@+k~Qe$bk?dDSZmhFoX#C5w{>k}()-tv1<2;& zO?oSuJQV9cX7Wg^%mb5W;eFn;M*gr<`1_7ix`6$@cM2QIKb7T1oq80~?>==t-g}=$ z-hc0DO;83cUb7%4n9{b5?7z+eR*RqB#_1o`{?uDa{F6^V0M-6_`tgItTNVvWiR)qf zb5t6IhKYs(6bg0LM?oxD?iUw6?RwhsX_u&Q>5D1gJbn#6KneldPuaK^M`MNu&Y?YhupcSPv_8eT`c5?sXv>VdL6uRNJ6HY zgVaX;<_l_JZr&x)Yc4EMC;EjK&aDZDc+$2Rt@Bz|3p=t}$n$SZ5<+ODuq11|TatS~ z==-WvW1nR;_HA}!x%efpV{>)0n!9mUPjzw%T0hN7HS}9nLziy03(a}u`BN?mMpf6% zG-H<_*B?B)y}#|ET1^2ytjWFrsxD3~%KXjg6|GyVW^;LHt_haY_5XfxZNJV1jibAv zvBE&969)bk_f5`>6%Q+hzqpX3zF0>2&Bbj6t6Bl<{b%=Wdg?zN$gQ(e^S#*)MAq@n z?D^*6R{*%SXmiLVW7TOXa6NxXeDA`jY;M#aaOon;^wLs@sVy%HQUmeBw8)%P$&}n- z>PJB;Ib_+~*?#(lG7kbea`0Oxt*6s+^TAWpnR9JqQ!D^Tk}+Fcg|rRvdP< z=TDs1I!LoZ;;^gx`ajM)0BKe_9C_6io1Yd3hh2RUQY_0q?CK$Y|M}P35_@Ow-4;9x z>{r(;v4wnaV&N7&7hL8K^;@gc{j>dM>M#Bxzblw+GpwbEp=u1brXVZ`1RmT&fw9UR?w@*Xk46v(p}wIm8cIHtk_5y=C~=>x>bh&Gx9_-ibbHGWON9;7=tC3s zN7jqm59uNN_3M~2f7Ze_PDN3Pe-qv}$a`IRuf2%ru9P`?Q5$fsS&I%vN|B^|C@HlK zQ}U$zbi*=#m6R}3u35|mv=l#NahqC{XOs}M#oUR0axq1&y6)VJ88rWfOC+g*-|YGm zvXjgG>ldH+zf$^dH=d7nqc=San``?`eKCXbrKPH3RoA5_)f6a4-7+Q#fetAUP83ac z`$zMk`YDKnsWI*4zy=i%dwG5H+&~Ox8)BzZql7A%4>Qu+t15{SY;9UvqZIdp<7sM? zTGRn^%RF7TEj2|NMx8`85LKclvTdT(7bLL~TIi{_c7`aw_12!4*^XNcEdFzDTZF&0 z|8|F(??3c6>ZoScL#n(0CL{Y$a2z;)udTZFkLEyHU?)K)i@{e;c27hfoF)WGj(bL| zkZzT5aVXK%jmN^Y{&U>^{-E23s~P^1+Xt6s7#mticGi_e8V0p{wExHLrF!#jztOT7 zrDv~YP?1gLG*yB@PFc1b!)tQKm8$ouWq0&&oL;e9|A~i6{GXTC$%i;jyd*kS$%=NM z{KSgEAisGlI@PsSGKQCz_;!o6lV{yM7F#5t#E)CZ=;y8I(D3_^R}|ROQW800QO~;j zBsBBs-TT{S=pV{U{F=_J@Atjua1>Z@&%qgM{Gyj_-OBqq`lT!TA$!cqftl>t>FhTC zJ1hGjyZqk%ne5A|W%rC+@iNEw&b@szMUW(etGq9P6?|b$OW)jAM^_a2O&{zo^RldD z+=Ctcs{13EdPolHHTGBDe`v%iizrtA)6DcSKcr`sYwTA%xSzk{?r>za3}ugIk(^ch zFieO=Iy{ioGZkw(C@Sx>JzH&f&z_Nmw3zaqRNfb_6U_sjC+1;BIqq;Swm<&i693ILWMGkp zsQdPRh^*kUhwcV0c7C`OQjU9=DN7%2)EKs*@>E=xfG|4ik|KE96meg>XZc$n9@~y= z8nmOm&>?UbvZNd3lZ0Gi^?C&s&>~d$nl|g6QYHQwj~ojo^wJ}xutax0G6k$?f*rbnNXl${G#8w%Io69 zkyNE7<1aeIbXnOw2jIkvZ&NXcgOw6U_Z$c-IN*d!A%C%og^{oOuy|sLqW%+eyf9{p z?L`MLb^K=@Ed#{gdo(&+Ac0&93AVpjMIC}P+*vPCS?vx9x@CVbuhRYX$?7hy`}?aO zt*~u(*|Y6F>8ATcR_=K~zr;>9T9$5g9Flf~CPyRC z7`MLyR$@*Ys>+je2&=@0s3zdIm;p?UNJa2uHNCSQCkPCtu4Z%(je}oRp&bva2fU83 zHB2N@hIh|FpWQ=TRJ6g>mPN;?-1E7N?L(!3Kq7i$K|2QEZyy?$&cMKtv(I%oV_lz4 zToDw&a;L#sb4k$qN@bn85o$B5{R^Hvpb&tB6i$Q^cl_c%@?7yA1>wI49yxTl1=pNh6-$3k_)MWNo(D5MsUtg~)n#V|x?U;Bw$^n#!LRZq<0qwkZ=OE>)#asfI^k6w5-)(*c4pWzVwrcdWeqIDS*4U!6Y6MVp=kP`cFp?pD3N9bRu^!!WKcQpO$sCvG7=D>WPk{^}hf1#sfxCk<`$_+>5|EFz*;QCt8rQ$QdfO-U|bUe*Fuy>w#%O`S-h^i+6!;+vbZ1XmaRa*0=s+I+JDVgIrvioZ=? zrcU4aWtT~ZymA%(ZhK`?t%J+Z1FATzaky1Hl;$DB-)!hVyfxNxW()|Il_6sl zoCc8-`+;_s`0CY${VoMe>~>S(!;Z>L(Fph{=mcQL;-uU8m@@>OfoixemTIp3%|G_l zBa@5hrfGqJ#Q|LhoR&P(fDR%=c^40`aL_njT+lf<8JDn>4^)f62OXF9(#f%WE)ZlC zi7n#gl8FImkOA&uN5n6k zTp)!efS|uop}O3E@pW#Xef0X#Bx9m_;*G`@!Pg0b|J|ST#sTE&e|@7%&m{3Wi>qUe z!KD+}E>0menVCk*lP&Z#T?XWnez za9`L|@Ky_Tu;1aW;n--O^;RFf{%2F(SVy5XFL9tqV2hAM3_Y03mJ?tBnSOZtfPcZO zZrL}ly2c;=_P|B}qC@ouTndc?mwQ6^P>T5XzFn4_y;bIvBRCur9hYVLP&5OfC=6ix zc5(77mZIX*qB{%)(?TcSWgjwiHNlf^>ty>=Hz})}McUsVQ!x>2X)rH879wug|~^w%sHS$tM0z z|BM~Okp1+I;mQ7#8}8KtT<&~xLl+Z}jRFo{C|hJB;3aAR#YqJ6Lbk)nJLo_Ev4t*z zEdhDHOP>4rPNWsXP7m_Ud@VKmRv=7}D>~o>tH@{76*k$pDGf(Pe0hkxF}MK8xm500Lc{Ief>niZxzWHEQfqw#H68S!ze{ ze4Xj=*lpzRoJ4X1Du$=W3OefzP$Dq+H%ej!^AQ1q zCYR~SDS;QzBP7OJJ6_~<)@ziPUrdXPpdi3107I*_ExBuPXP^f5KB+9B4uM#`E@~x~ z!AL}{b;>;wzc?KiL@VjQlV#t9+@WSzU)?l03C=U|ZxRA5_w2TiI~c8FMG>cf#)+GG z6&mu|n`{dwROE;>jzPIk3OUun@KZn4C}LogB4Rx0gNE_(vpZTI=x zULNG-EqC+W1&S+(O2CiowL%p{yttfOVsSMX1LA<0O$Lac3j>Z8#$A@F6yPj~30lq! zHL{?M`Bj7VBy)8y-C!JW(84Ya-Ms>GFK}!hTAT&kr77T6#tN}EDPrzqn)TEDjs=s- zC8q+tA3_@pi%?0IO^#Pc&Wg=-!NqloF<1f0qs%T9ld|8b*xu1U!2YJmQcM^$44Mnj znsizLsui@}b=Th(e2KDx*9Z{{C?5S@BMYCdO}P$RGhlycV5jy#WqII>SQ>z^3HkxE zDg*{gsG0QYerI=1P?`FO95*mrpcwZq(y=# z;8tcdPh=j30V}p3n(yUC^S}r4!8`LRAXD{Gv1DOmq~{^tO3$EX8OsBdIbsj7W0*K6 z;U0xIRCffxNXtc9EHWCYxop%{Q$-Ngz}V5nVQ`x8WSbLaECk}Y@}jvC7glYE7!0@% zdaKqXo683W!C;H1=CVl?V3Xka5G=9$0Z2fLzzrnj+onkYwrVI5hi#irUf_B8In&=-a0qTE|N@8j3;Mb|i?jnwyT~~;3t8nIIareFW4Mv`AQgJ^@<_Un15~ zKsV5W4pZX-cD0X6`-scK@$3W8LlFlUj&`I;N;WJ{o@+QDfEsL%l0(FP610LJ2*99q zv{>XO7k7y!D~#)L=;kQLBp7?!MW2SR|@?R)Bb zp}t9~d(k9lBIN`{kAXI`S9(vTq#h&-NlEBErS9y}4EX@&vB!MCu+sAZEhjmlOmi|P zww>=@4>BpDdROFG6*#Wf>8POb(>f(S5}UORD*|n9fYh^MDkm`;o@#=p=j3P+9+l9X z1qRfqi2sQluYqVsdO1nH(VsqsoTkDVN#hdPpu8L!L2#){9k@vnAAN^916gWZw66Wl znqVkQ8)$jKW~rGcHFIdIf}4>+bWN*yEKIv0j(WLCD&MYE@ACCadqMYyjyIcu2!pH^_X)CswqMiLIk)Hfs2#etCN=tn(qX-!uuzF z&k(j=t3Yw&k>{Wg)XhmOTj31D-cZ72e_mb7KQUx16`34M<^eh!3u`lNCUi2X}it`=?7+W;J6~J;8R&7+M9| z$4j2r0$x5a8bTQM5|orc^2#s31N3A|L4U-a^RPmcunBAgq3XaPn^g$uEpcOs4b-41 z?cKiybAm#dO>WRDp+lgWmv5%^*3p=7Y-(0W6O`QrZjToVj_(8n=COf#KT~^<#vX$? zXpk=JEnWo-54wtF|KQ_RF98SyH~?{Os=>uNt@kd0_TyHVwukC5L0f|6J7ClB zCz&mW!(V>fcO^|yTY#NpOAZc82swC>S*pVK{wQNSy_g5-{RaQiAEy61BXJOp;E(?C ze096O?Z>lfy}p-$%-CJ8-X8rFzF=-XUrqTpx@uA@*iV7$mDFXfd0zE)m%)d`a0fbE z;g)S;8daz%*rKTcj*@|?d2+h~+bXX(jsr)GY^}Jb#B>X=bPuX&+70+cATO%9jIaA6 zWUgwfs*Zc%4^gq`({sy(YLwLkgS`ylar~24&CCJK$hM8^p&UdHIC9vHVz5@scsNkd zB&N(a-BjyRs{P{30x#g;WOZ@W#8h@yEtE1Nx~s%U;<5FK(bb8Pa4~|9U@Ks~*4lcS zxO0^46;DrbAAU^E%=CzgtLesOp5 z!ZEtM_DzZ)6|ZNa)NN+!bW-u-Jj{jeFmaA0^4#9QTEf0M{M6ndDDLzef)7hh$GMhf zTnOg~ccRR_6P@~I#mUYR5bny8oN+2{>P&LFVVgg>xUqlF4K}i=M$pqIKOfWtatGde zFH+%@Qer7-4}U1K{KPK}SfHIP5GVUi7ocSF&-kTPvtPN9&{;xCiZ;qe;TW8hE@AKPr9At@l*1S{O$yFoF?@!}o(5~PAHTkBT zHVfLSQqx(hi48KiKg=zc1(-D1ec0Tq)k<}%`M9g@M9=qU z!*vt&qh% zY_h1r`#fEobm%%=nRcBzT^u+N6(ccRMu??cr}w2?r%p{@D%{-Gm3r*XR2ew`an2fj zLlR)E=Dr#zijj<;H-aLGS^7{Zuv!YxCukKBx-Q9(=4hnlk&?}>?z*CtXcpqj1YOP* zZ6l>y%%`8%_G=O-Tb}l}0^LShW)RnN0)Ok=z~71k4^-diZ*|1qntWe|f?@2a=_TDw z9Rz+=dP}J^?c|e+R}3)QfgcH&D?#^4J?XM>w@PRro$>+&fnI11;1($ zOtd6+DY<3^GMAKN)lEBA*+t-3MIzp)3F3`@RlLCy&)2$EvlSO>0a=c?Ug%NSQ_&71 z-*bjT8-We)C9OUHtphFsDb9h=9tAx1ri63i&=rHY8gZ#Q*-AL7K_3Bh#C1qILtgLhhhDO-^4L;67>I;Nr!_y_(jhO-MgmGn~`wU8*h9fing z-CCV>vI&3PiYP;@Zo^t})iu4dxT1xGjTf>h{^!fq|5M(eRVao2)%dG)d}7aM4H z5N>VjO~kP5kfm2K@o5M)p_6Z_m0wA06w-kJI(l7IbMis9U<#BxXrqRcZ>u=y>Ey#? zWi~;_OEFwDg7$eAlFkD9^6Tx*mk);fuYCEL5yF)(V6E05 zWp9=zUm8q%@{|8%S3W^QXr(>*%b>hvd-9iIEszp8&zD0vNW1b^Dsknn#5x78d
z*Od>^0eMe#1;@~X!K6L;dk!Wkeu~s+_Bt3W&|Y2nY5zTa7lI@qqL?`H3+Tj0&<*5Q zLE4YDTLEE!TED8aA8k%D<49XZIuInuNzBbS(hv$#!;wZR6Gz%O`$BKn1fn)PWR_^) z2SS$hhEbg()h0fn?`;@MO^TM--)=APpQCJ5c%CRi7}AYhm^hbC_|6{TY8+GRu=De5_NZeykwNp;oXrhZ>B_z@gSz&jB4%iK!x! zgvtVj$hpF^cuwF@D86{j6)DB8y|RJzpI05NB_5}z7a!YyPCV5(`K+T3iP+I-3*k(5OcRaJ>R413#}xP9FlpPK1BJ zUqE8bcBZubZ+KJ2d4RxYoQhOgD$Wg}x05dX0roNsKeyhwIk0EA1xeioP(| z+lShjsl#j7wYJff$wmqWtL;Ov3p9FdD;p5R#YkiezP#f&7*nOvg+RwGk^iV=## zz`p~bbmHPNr&KufoJ86<=OBbO2;4HI4OLTOO0RRAW@bb~)r>#RL{wqXRh&bU$?nxo z;UZHnp-xtRHy0#SG*(Aq*1yKKMX+6ieyE}<*XBI)x>=V{t@Jf-n4JkV0bzsY@V=^J zliPuU{UH^qsQ8DVHCp2cFK@qLZs?1y=e%hi>8qL-A!H7QZZXQ{O*Vh@SM8hRdBt+{ z0`^-$0&c%u=kbUeUFV6oY2Qx`S9vDcPc?|gAz7el%1sBJi)QG5ll^s9WD-OX4-@2> zNBgNVfaCRkYX1WC7fL+xCar=x{ne4U?g21{#g-RXkC4+Yte^Axs{#7`H_a3MRoDNw zj&b3^7^8X1Y(H3aY~M8ftvmN{ z>vOn&V6y{{vZ1z1_zF$;p#X|KmjkSrH&hLEa9GHElhpN4^4p4^s0-8z zGyX``6`s`Vk5qf}TmbXc=#g5Ap%oy zMgU!78w40uYoscxzzF52FdVC?4yshThuM@?q=CeIGkl~vfnuZfY}MSnF;YeA+>Sme zJ<_MBOyMZiYhR(|X6z_+IFM2Op~jlx(W-6+BfF1Q{YthG)S`FD z=N+pqZn^?18rK8+hQL#DCH_1&h(FK6eqrHM;Ag)v>X?Wij=5=!YM`&(ZdQ*`{Xjs! zj8Rqkp6%v>K0xK{#PgiBeo2Z@z%#vGyLrc$4`2rcQs$CjjkSw4QPUpS=W^NYD-^1kI6V;L3GOVbHup&$NO(d2UO3KQ&MOjC* z2M7mwsT~|(U$2wYApqeeCjt9DecR+tN@Jf|VBR`TmG6Ok_y05G`xAitmw0EUp8QV$ zdFbh?ZQVVQ%j`N?eVBz*ZO!s2s_(y#R5*%2e43@!nBFI-5qra=2mjYG>HiV{GXsK} zJ5`l6I102PA^Sozz#P&1{PEw{H(REvGpvg1WK>*md#bpjr>Q0-X_|usBF!Ptca6DX zni^%9Jk)Q`hWhbTVmd(3{4vd{6y#pz(^X?oVvp(jW-hBQnyzrEV}^T7{ugV!yGTdP zz!_?2FwZ43RLg_5ze9q6+zEpTf*?XEO1b9U8X4JASA&m$_8&HX%&5sRVkrg-mx?f0 z$}}=qoJFD}g-@LbU;o)lpX3z^*(_e4CJO@H+jOlo;I$u3%R-CIIR*#uU=PBNMamjhet1!=_J$)wsc0yb6+a5*G|2?BGQtk9pznaR6 zRLPJh$g^-ruQH}2ZqsU$dxSZXNHHRh+e|o_$yhvfJ9M$N4BO;Xy^#B)nSYV$ht2^T ztCC>|!_v4e7rmKX7m1}Z1V?sZ<&eT~y@ZXAVyJ@+X`v$s4OW=TE~fQj{&umN6ew{0 z>Z}#rtx1;dcEW7cZJ+eBYPNDnKZh5=jrQ>+>ZlYEkpiJ7^E99n5D}xdz!oY@)upPm zbYEc^X62=-syGve;ko6e*JbK34D{4Zs)f1dGSRaBahWXoW7 z&SoMk$W3t?v+zpQ3go!wqxDk60Bw4OgsU_G;DMy;<{=H$s^3m%C^tJYSvFkVB`u6q_zggHDVwvtYhz$O|{abgPT1E(3(} zxpqE$F=a6v$+H7VU#DP?z=EsixS);&szo!oPXWsX!!n+-^u*xv5*}P1^B1TCWPHyo zfOh>i^Th&nb}j593YOby~b*>dtf6%pRv1Q1O%&*s~2jEh^ zZ=t#fL0EDI-W;eBPX8K;?1skWq5yf*je;l>_-^w^5Z{cqajm?BZGi;8zoDaw5D9zhT1? zVA<+J1bh{{42WrSUoc}AgOJkr`ov=K@4UCTCK9L6`_AiBW5#zz(l(lOI>M~DSRG?J zT(5Ww2QA;28FM`>-)Dq-mSl0ytFBjF_r*QU+t)*>UWARY>K2D1+&j2IH7~#3p1>~MJI^g-Xkyfamw-Ojo8C+I&O#8;j-|VCiAqFf zf4~*^aEUs)X5|#vgtFJKP

cYa9Gy6{F3Pv6|AO5%b7_67l#M4guecodOM5Sow+xbgF;B z4W^c8-1%YNy#;Dp-28e=K=(UKbW63))T*z~uC*mY_d(2a3C^2Qf-CoB^Yg86#$_&G zD>sAwrWT|wJ2R91u5MEInY`QKzguN4y&WKGX_nuvdJ$hfzg@Lb_nW%QgwzKwQvs=G z$7i-TJMK_T^vlXrx(@EUFb(fiLr;+Pa=>^pk%LZRDtAf92b{zhyV=2)>=F}FC2$)P z@l!wt*@Pf2glCh*;^Z=0pziqGovLD=#B$*Qx=8o7gCDtDm09qRY9RNPU|5Bpnr+Kf zB93hkl*mtPaIpK}u2X)K9(P?S+XT?uV6gEH8!)ES!6BRh1soW*#5jcf#AZw-pNju^Xi8!p z3b_4`bNNWV@mpv}Ygij^RFT#Owb!6_DUz}0g1@l%&%?o`{KWCg6@u9bH_ijAE^COg zcFnM(4sw%a!U)!3@^-!3haPvjhn7m)?L&{@9Xt@D1TJDz>w6UIYK$gL{*jNcxf_DlqBv(QiaO~PHrGzr%Cw_%|zyx_hlA%mo zF^oI$1tw)*xgu|V)spa8HoAyx^F?TkfpZK6H-w;=(g}~#8zO1LGZ}g34tX? zlBBsWY!+%WegWBtUl89l_Lowz077+%-KmKSsqW$oL8<~cESivq-B}lGwlN;09-+~I z7D<03zah-1Mt|`X{k>j&!;J;GiO1Q?3@rp?@_=gs@qngsl`3lq^5XuUg>#N8Bnoi7 z&K)zMGZML(vP#v@$5AE$?rt-0m1g_$h(rhna~9?ZWF&DA3)^|kEf{h81{e>}0E-&}4DxWo zoH63;WU2Y)e$~Jd$@nB#9)}7EJ0EjnAA7t*Y0&6*2g)tpcihuVvM~XQ(ox<4sVeP#T1w$ zMXOx6=s>4F`!RK(uBtM3KL#)1jMaIME8KFja+WDuht4LN#p`Tn$(nVlC$+|14OBC; z8GnKwt<>Y@*eBF%yK9%8jB6duDp8W;aoT)LFg{yVk zJoc2DYK_fo4Rmz%xty_Hbu0qAhyF-Fxo?`4>)~{{)ofg^BG;c?xo9Nj_jZ(0XAqSxb}c5a4G!bLsRsu8ku}A@PXBI)uKF9 z3lDAp>l=dDqpOa^pisR>XPvy~xbKVpPH%AF_!IH8=lN!8Dm9x^b$DWXfo%=0zZ~}i z^WC#*6b%p_?FWuo5~Hhvqyc{*9Th-+Gi8GsMcC08*|b3%Q=e_H-n~sn>IP==bE;V# zTmyrC<5@=PbIBY)%Ld=y$)@~8+ZsbJy1uAd{pr@)!HJ00h6$6&YH}vu+1gi|R1cs| zo0rrft<&0j26bQoTh`*F96f*g$Si(Ib?=#tNkxQfsi4v`A3-xGb`@cTh&PfN?9~J} zTz_P0ZO$y$X`5xaF5j&7+ouV%X)^-5TAHp~)SRX-%C2|2zHn>8Os1bTFKe^n?Ja6F z=JVF~>JYu<2Q%wsRUb^jysWy^$&6UWbos~T-IrClUi0A|`=t=qs}(vA50VyLVt$ z{h(J>-Q@fdC07)$!(sJ&M39JfyO;_IWaUymAy4P?i8JYQ_=M1z4G1ATgvtYG2C96P z?S`8Q8Jj1jTLGn8NsR6c52+5g0@PNX>MZhOrLq;UF3=saU6Fkoj>~TTaRy%{hS+ zTJ>3UinvuGJ=Vqv7J#EP-67Q|YsUZ*;lwL2KZW^W+PLz88%?Y;A4ku~1;0f$2E)Ac z@vD+p!GI)L9q`*LfF}{jtcBvRZzQMwyfT|Ynaat!gr-b>6X;4XnB2EyZSJBdT zDi{X1MaYgL9B47|SP>3b1fbRJ%-YvfKiY4FTu2~>q40GFEel{H zXS4>!x%CsI&n2~&gGo|SToBeg<=U8B(aV79d(6f;Q+#2``6Yxv^!a}Dl z9UOPEF1ExL}eH5-{~XghM#4WZmdDUA>< zdOF2AS|=&?5>cd}GZ=wku>>twct}Xz4gx0^ioj_?X{0mvZdU`cFhYchV1&$?@+5^3 zK^8C~on_mMgV&0qSFS`8iQppVsM_OgYj+4(V4yjHkzA!^D$ zQS(uAwS( z7Jg=JtQS60&GjFS+4Y$^E%FmE{A+Xk=Ww~NH*-E$hk9w{D6=cQByfl!_z@B3-+ZoS z*T?=CPVR9(X>sOUfw|%f)uGXJ%dNOQ6F7wA1)m@|Y`1E3V0DfbNi4E%AbGKFAi3WH zZXmc$#0?aI$H@B<8yUNlIp|BEcif!%r5aI>hGayjg1h59H1Vd{`X$!*Rn%cs|yg^(~M%jqJC7tNPpUU--lzKdgSr@ZUeaRWAeouhZBH{qj3Cy&3QR zfPjZBgcBvztf73jRj>nkp(62;OthY6nId>=L-B(B|F z!$L`IRj+6PGT_|g?P2l~kbpYRoU~iDgfGopwHr?OxLLDXbuEDRFJf=dGC%KDP2gSy zTf%s7T^4l>xnkYkmW6WAJ0iH|ERC(%*lULVr25vDaFC^G&#_thlWL8wHvFV2^!Lhq z`;$6Py=_kaSyj?>h9PKf`&o55_y;j|^X%nBfR=}TurHB5H7~G3adDE^AG!3BpjbBi zn*E}VO|_f+MTp?9zoe<86??>1KQ@>}{6V9_Pe>aZh%IOi;bbQLDm5?tHPhH9HYxI( zIt#cq^Ebg_JjR*xepicrB(fT#97c6SDU@o6K)V~h_OGewAev+*ZiS3tME0tgkCCIi zHbFYn2HZos3?eWf0qn6^cFr&`Ap!G9o^I4k*m}#qG5yU)N94gOA`A zE74~hfC1a_RMdg59T7RnTLG82K2=5`G5P=>mWb718=+e$Gr?012B6pK$294w<3}9q zNk3G8W^U^s5jjv<-Q5s2(zsP=*vQ*BX$j(ZdQKJX@KTJyioHp1z+GQ(<#&M7Q22WquvepM$^FRiCUtQq51!ahyy|mh#1uC$$%*H2%><-13I=aE3Hn> z037gqujDLIl$MrKl5^z0LC&}*Bt_5vo8@ zi5ueuk)wczqKLo38-x)ZsM6&I%onaX6s$~z+D)*m2L(5i_89j%d4dn{BO0#RRa@838v^A5!yRUdU0u<12$4X( z{(`^Y{P-)vb1l}T${gwH{c+BPdtWF89f{ki7@7qJcf@XXby*pr34!xreg%YD1iYC9 z7#=OmtFCTdtA&T%d;XL*Oimr$z6AS>@(ooc6YJ`J{NSOu5U~;|DjmozL)^9}R}rU`3m`VF1pyo+RIrhbE~jSp(aC;gADzK* zISfG#o0rz8Ml^&G2nUB?`jF#P+X+BAgKe3U>uK($-(OGDkF%|w=FyY?L73!oHcYZg zQ=&IhFUyo6%O``+#2#9*z6e%%~cOR&m>)##ob_3l7N&~#Danq}z z-pfApA?E4QbZgvL4)|D8>)z}26Vb4B(fIeD2tQ0o~bfSL_*+D|a z^L0EW{>e$aUX62Zgo^=Lo9!1mdHbPfj%lKMCPlHBimbpu#`i8s4e(5Z8wEy4%A?H~ z*blz|ryOTr==hmn%a4;;&=AIcZdNcIc^qb$(#kM?FYzVd&y#%K`WG&Lx z6)hag=+MwoMF$312zrJOER%z{uT&1^8k_->wV~_8R%svdQW;?WRseOtwco*pf0~_c z&St0U5EYcx&=OA@++Dycy*qk3+5>DfH%g9gukFT|)+ydDBp`e`Ga?DWBD+p8dw7KA zd3yf?;dqBX1k%>JN6^5opaFfX7l^LWFA}h|9Pc;yX_t9{C0zPrB3`64g9) zaYa-o^&Q`v!kC_@zxv)xkLeD{v?U3#1r(6)v0cW!SAq{03;)X#ZqON|QuQNlzcbE- zBY`NJBkUZ*&dV}&i>!<@UUPf1C4)gOXs+d0hFH#7N9WC5&GmjX0H@7$S{vO$H|CG> z7P_5UVurNPlhda)-fW?xb)b&JODkSF zwbISYQ;eI`g4hu|mN|C4~28t33j?K!woGrhGQOdnYg6(c9&>YATg z>w~#}EwS{tnxEpH4hXD#lfo_ab*{ZHdq*G~te!`51e4he{abuQzznh_%YLZ%Aog5% zJvOJM)3Z#r##u$*Wq5f3wfGRIGp*=JkICa=M zH?>{j6?Hx3XLsn=Vb&;Q?O2>(d;r5|ABJFPblG2W?(16LNmv-jV3N5?+ zjOxL9ru7!t?H46TcKZQNgldB&5k+@_XI2Bw988FV7qOWd_HMFf41bqG&nhtPv%Ul+ zSEGY+@%13K2=xrtq!LuNhpgGiWaxM1EHei~Wgu95`Sg?h5yq#Vf{7CDu+nH25keHR z!a_4knuhqKKzavorvfev1uTj1Ubpc6>$h|x05ohU5a1!9{xq==CKUK5us&6QfVIog=QY!2ne+-#!%CdV zM*Aw^5gSy(;St00+vzeWePXn6a#%)OO(!E+CL<#0p&t%jLVaixZPzKVx5Elz`jz&> znaJW13KY*nmsoE!5!{(iXz~n2J8S2}DUO$k>9#?Y@yB+^X6Wv04ZdQSds5%**It*? znhQ3Vk-HbVtu%MG*7eBh{x$IiF-&rnUf2&NU7dz^V5+2H>aCoMKrltifE%z`=vYXi zYUQnW&g;w8T+?-l#7KC_!NMH!;`H_rm<5Q zZE}W90E>cp=CjgVeER8G!BmD9eG9N0!; zi69C%-c>+N>e1P{n*@76;Ghi3Lv7BW3iLOd%N*!$V)r@{z9DJ?6*q)$z==Q;f$nOX zVp&S?5fR=8#rm0sc^h<0%i;a7h?T74;^VD+0i%V&jag-wwC3r3`RJ0Bj$wSD2t|Yl zf`cLW86d}6hS}R;>`>MG@gJJSmHJfZhhxta!?|@wU5=e#>+XP)K4sJIkRT9olYf{F z0>QH47Rq!KJbp4JM3hS}*PL0QquneCW!dd>B$7gOb#R6ZaMxr-W}u<9W>!Z%V4~>3 zn0@e>F$rP;Tfib)6`2yqZkc^!-QMtsFE*J|dg}4iqBr%_ z`}YLRA%chBB!HZ%{!Id;Re`tIPqnfIN?SRn7)q@9n)hz-ORb&3Z__vD%MCb?vuF+F z|H;MqrMx)5@&2VSztVT+^9M~XDD z2mm@v!Js@mg#Fis=M^+5U;nG^Wow{+`ctapQg;4;gq=T<;J!|OIiRAgg^Wy^h5pH; zH=C~x(oLGll+v@_>jc}7L*Q!rtjRCKME!2+0%!ckZ*eXXs0Jj=Y; zTgMCS_pK29=Z;v^N0(Mdj23-hECMmE>{AUf@X5J9j~KuF|A=7;roOrWrA0z9w5`T(6FjjCTHQ!~>=19P zhpQo24B7l9ZvOYZffdwoOU9$p4 zWX?%ta^-M}HkH#pG%zJ4c-WL$VX8{c*Z}bf=7`c*qrKD@2C3QC$ijCzvu_z+%Ui}1 zSk}i6(Y(&}O+1bPKb9qgd6{CmI9-^!Onhx-d_20Gn$~VWqefpi0+0^j$Vq%*2IEVC z=z|_b9(1Hf5%K$}E)WwWB7>EJz%YxjR$v;#y%&gA>_DoG5rR^gZC3Jk$`hO zhG21+VD7=@%)$C;IODnx(U+-5%#%YjZm(Va%MidF3Qmm=kqt0yhC&^yFzYHLxb`hP z=vjlX3{Fwb8>+{m-j_r5OgttJ(?c5IOPF+?@&YH>APm8gj*Eac4%44x>2LCdu(c-l z2t6Y5BFJc=8GnQ>-|JmdL!jezt$EedA}u$6!5Ry>Mw|UE$DN%o9-(=6P4SVste*f1 zyWgdD-;QyXboGS-(}2<*#<;9HcF^P8H^fyvi%j};Xnp`!p_D0-Udem zJGEQ{RUfyh{*zeHP~q49oFAroW$^CRy!OaCIpM#FxGnv}dfs!A{-&du9LLQhkD7uC zbwRBK;_H=7$6)KO)U2q$<*(INW6y6mnxopP*uKyG@dJigmxm33)3k9r)v37JHwn#{ zcB*HrS`vs6ykyd3_pyx5z16fnN{_V5w|e$bx{jiLV@BxxyU#~F9@lL#4Sowfh!naz z2w5;dd_YT+n^5hOBtY1zy&I3wT_U3Anche1A&6%O%InN71D0zfmk&vO0=|T#fuPj; zkH!TK7{T$!=*sj6a3R&7AA!}c{__!BFj9{P9lSeI4{gW^LYB+?c3`5R#Am?sp$4rt zHOG(A(NgznTD6!*YM+Fdh^d*xNV%)}-cg8$#G&d@`Y<$Ff3zN2!bbPr;=<9onRc%> zyOO#@XWDpgw5~tQf{+)O$DF-e>S;rri-ys{QZKNFcb(gDgfE~r>&^kW9ek`l)?S|} z)~&?Fh;^F^eld?8tA$m-L^f|9tD9mTzZ|Q_{Od|Bu(aZuzq(ld@ z$$Q6WUbt3poGu$Jv%@$_h0nu%!b5Z<^?ag9&9-)A+6r6&nDH(|%*jRIV3oqOXjOt~ z)jmQW7OMYdff5CHR%EZKEOqAlzRi51ZWiw++qK6uW1PC z&4=r#`d*VPbFjlouDWonZiy?U(BjBcoQv5oR>y&j-;C8muxE78IDIZI<}Gf;=7_BGBw@caz89lI?L`~!9U$TR2dL7aY^(H(__3YOCwFG-S(0d7S9@Z zwV5_vADzBf@IUGs`c`^_W2%j(gs7OoahdYtabIQJ+;Tj=ry4i06ZBAz0*>JceRx^= z-bCGk6ZG^}KVg>xl8?d~nmAy=`J}}$jd-Bi!KPvYuJ{Q)(m7&?c1_2L2+4TFTs4t` z**rT@-&wdbSBLHH)KW9^MBR7*xT^Im%C~Ug6^zhEfbJ*mW@zj3JO}>N4dAsAQ2~P( zEjVINg~A<4>@shksE5}59XyG9Bf3xGZ{Kj&KgaCq>6DZ-2JL~ALWv~PAvK2;hfH^) znsgT{1ez#s1QLgU9D$C2*I~jd(8i?@q#Pc+-*kNDv2nSC&a6F2#}R7;P1Wvm&_51H zBSemmBJ=x6h&3w0XSEr!Rm3`Zro$xN@3`O5EM+Ekd$50t0|G#LP&nGn6`y0U_bP)x zzI!8l@>1Pe268Y3?UEm$jjlu^uwGlHr+FD&fQzKOhE^A3Ic~8>7;Mm!bs@a~k5}bN zDO(kgq)SDbpPF*&q2f4EM{Ng*56zU5MdQ5jWF76yZWtU9#2c}&?FCxvg!BmEH?k4L zwx4Uw_b2NExP6_AFe2#w!WdC^esGKhLxe}y&W}c#lt|{Dc)|Xhbyr;{T_+Ymd+I$o_fe zB!r~AA%rC2O{4@RqS|U*nnw2>fjzq2iMzf=yIs;m;!C91ojRhMlk zdW%+V5le51MJe5OwYIH!@@vi(qX5(_$fikehrnzVixdKkew zr=U+d4rI6$$ySa^utxIjIV!exgeGnF#7@!?Bf-b<;2czl+W`hQ0-r+eJ63fGPr*6QG^|0P`hG5|kM)lE-!)cAfR;mJ-7^=? zMRC+qt$<@s9^J~z#;GVhi95!rw%+qqH&U!6gzUu0x05QHu)`AQG;t9w2p_Urh3Tr3 z#;b1DEG`+Z8hC#J^B8G z^Amu&(KiV4n=ny54#3TusL}#bVw7W!X2~$1p`WTSccgN+&5Y(|c^TeG98GO!Y8z~5 z4&=fja?QAgLL#4hk!q>+NhjzY6|TROcmU)5TZw93ma1|yKc#iAPmX6wb25kWW606FtL8=|E93Z_C`Q^s?EicA{osVA$2iMpk^UpW$ny`4}-; zGVNx%ZvNP5(_tNQzBkW(9#$m2pg;M%O3-i)pP^FpL;4JLw|;nO1{|CU`1A}=hXSrL zQ$%|b9NhkWi0_-DS_-imI|sZ|A^$W-)stlSTnI;-U_>GOA-*M`H-8+j@uayb zQldgF!!Nq_=3LcN0|Oex_LARyo@%8t?wL(iujWSP>X1g_2JD6Rn11PdVx$5!ct)h zx|+jMFwr=DI?ET{hgGV>YkIFwi%cxzb#JRq)&@QRvpny<-EieiH&c1@%~bvizXG{LJQv#a8>Y|v_mim4LG&uW36{!mzSgKO3t$gni-$K`c(1s=IHeLDH z5*UFBd91Qj^#E6W15F`t*RS>`@NGea7Vxr4dKq}?HS6eme7=g;t)}OzMAt(oPuK;* zdf{bNiN`;y96EpF3T$8qWxYa0ap7`^QgIwzVAuxX7_crh+t9B7^zj{@S0Eh1!sTy^ zjriCc2$&wMaZ<SsfBR1F~ z4lY6yMZ$x;jBeiY8jKKb-=x}@E{iWevYt=XoyLwXoqk8`QP5BKPAUmuARwNaH)5lX z)MF;)v8|TAR>63%359X@&8m*>)-#)xAsQPt+xuL_Vv*UG@S^pB6looWf zCfj5uPNkUk=2<-WuNBaxoqx57@B35X*pod?1?mRJOtU9>%{29pb)292D=aO1VSYn( zzv3CiH&nNho-~9VC|2ROz|TbjhC@gcO5JOVO|mC#QEApmF5RM-KtW` z*dma~Zvo1=TU7{uv(+B#FI!bxJU1A!i_QLu|M@oG-UJ zJeKq=Z-Z)iq1SbGatLkSOhYH@hnwJ80hj6SjcD z{jx+=*59hTSGCX&UH7Vn`r+7lRVNT$=t&Y{=_dKJPS*20ZeIoR@u7XnaIeApZK36% z{VK7YTgn?{Bs7SEhRo}`w%u~rsH ze2AWoI|kg-Q2j%D8ZLgQlC2kb$cK8yuUBaC4HQyK6z4P8i{F5MzN=s**v8>#81}e-v zjX24!7NFm&xPg388r11(#w7}R4_ysT6tFnJs8pz{F>kf5Mr(57^sy$qTNpPkMEN5~ zhEd*3G|C(Kw$lt3x)=n`w4yNxS)eW*48Sk?5|u>5aYjA~{}fye1={eC_Eq5olvCm0 z8gf*#B(^X#Jka;sF5D9(2obbBk+RZ-J2T20=-d6oeMfAjwxAW;*1okA*n*=J3-?u+ z-XY=xjP1n$IYzrr_>1d=b|iElaw_@@2RnVAtSCmxtf|-8HpQVhkMKp;AY2FayVOTz z+7&UR;H{rkz|lcd*;Fs$NnT1yq>_U)izS1>r5oA#*hasGTC=U240ksZ1YMihkmE15 z=(5SjTnV%RLV`Z2SLqwaD?oyVuE+fZ@ zZe>{~58936W_NxckNn46oRrqDXkR>U9aE*!3Pi=UQq@R<SWmX#|R^Re~j}^JSDm6ZT`*j5q zSh40kj8~mh)%7$%{eGWSl=qDuZliD1T*H9&>?B}yK@;5wZBV6;(Z1ynyGnHHigt&#% z50r4Z7!B-{d)k6tQHTOmG96|n5nP@F4Kz}kGk{P0UDXbT|5%xrHsQSe?M>(H*1&ou zl|%mC-`d>n^WUnrW!%6vj-COE)a(KTwI9B<1-0rIY~26D1$#9tLJlOcBNsGit&936 z?xL-Qc;KSi0EGVeqDE+FxwV`Y%qPB6%>Z^}Oa#Q|TvEwh1>zMVgCYCnxk*3>o1s*J zGF-L{AA~nrHdv(FXEnl1a<|LiaQa<_f^X1eZMoz4UTd*$@~7Z?l?uXo>U&jR!Yhp9 zp+7==kN+1KHE6_N?M&n3w{aI5CcksLQaBfPp#hSpcRvlc3x)HB`{_wZ)b2_n?UGaE zch|qwDEXcJ0F9R4zy}HUlwZLiW@GO8ljfpPF;w&uXY|-i)hi zm~URze^icd-b25rT%Wu}chhj6EOuHn!Z$Ba(MaFCi;DXC=ItckyuYAGSOCdktJgqQ z2Vmg4uW)o04jJ*Ab~^xYef-(DGSYhh3D+hjH_W~XR58`O(CtQg+vJz_%@!pSQh-)%tkqpG5TK+__e@0_@+ z2WKz$7*nWPHt1ojHP&9RgEeL{9&#@vqtM1&;%>L z%N+QM zM#EafjcZUZ!`sxLB>nJC4f;10ytf*odnom-mMRfTp)a>YU!KZuhEl6)Q2a^~Vz}0! zUk#mQlDyf1O3$wHpaUy$jv>A zabD1RWd(>-<}hJdbdP>p*9hTBI3x1VG&zVlm`%pBR!jc|{GW+5g`~op`g8AF{%C@_!R(d+XKgq7e1V zQopn!cd8*f22)s-GGhafk68XsBfA7>NaN^G$371#Qpd58_d{7 zB&(>r5lI6c+?d26F`x;J3M#XlIKe|YQWK9poNJ0c9B*Rx!D>oI%1CKS-SsHC#ZvnZ!$Wj!7qK(l6e}ArV27?jHr-%|?HZ`9I;D0D*b%H)@U$@!Nz)A)qd$-< z;~@+qh^9*%R^suG-6rTo%TfrJ3Dc~rSrjhluA@PY&n6uW5{W2Ij8#E3)(QYo@HV)N zPB=Kyb(f!+bH7 zBCD2-gLlPKQyhn$i>IxIz={=v(AEIZ*3T6W?MN`L?r4bUkd_p~OA>H{6FP=OL#~!e z)%aop{1TB9)RN*uHh3U}!-14;((@CySsVGKmNZbRck zkMXmzEy!pNpKMD5kh;Ge9l}pcd+LD9jP{i8pAsH4mNzA1vf3w674)W45~bq&a&8jc zfz<9K>WY+=NbQm8(E%H?PTj_blc@%>&L&f5*-mfzBfr`T3f!53(Zun{6iR?LV@-;H zg+EB4CVE9{suR`GgX&PAV0Y>g*N_{`=BH zXYgYtvIUe13RwKagJ4b2|M3q3M@o6;g8<%l#7(+mvyEeCFC&*M?M7W{V%Do^`Yp#v z%JB~hTEhuF0AELUtm{keN>6sDO!*`#Mf3OFsj=vIJ!_ue<~`8NWnH5s=k?H-wWbFR za#y>fXZdPha8^%L``)g0sHd)Wx~E6A;SZIs_LU4OlJ`G^PX5c?Nsp05^O|1N*mrm* z-NXA&FVy_OUDIoLU-mBF;3*kiB)9$ps{MGKF*bO(eBCpq?x=@R7mi|>hL&8>K>PP8 zU-zu3yQL56Ub#-YD;_Cd_q?h5<0Gj1({cIo31xmIXfUorCaRX?R z*|kEsb3b&+Y+ZP>DZ}+9oX0;2O6d8f7Ox*f`QF>z|E!_k+qr9qK=@ojrs1a{?r0*1 z-&^_K=~Q1Ro=#L7Y!VQHTqU&V6IeggVMhhi1OOD#WA}~(ecV2n(&aNA`*bWX$s&F0 zBE+7@*Zo>^8?N1)aG%~^+jQ;1xX@e=tirzam`w8p+89ws z%uSOi)JBp(i94!L7j0^SB<4OUOz44dyrE@t4P74dktx(n>S$5D`BXRl8dL=ju#v}2 zrPs{5EvEPH%xTjo+;|tmMb3na0&{trr#Gz7!@Ox` zAzW+S8tp_qEg%T0X|0WCDimzun&}4HOQsvF_E?K%bwz`lbjcVPoa+&TGvdY`#RpQT zet9)gnLNa9AyL#w_2fLu#X~vt`IRdbiUX@9GsxJj?3zI{0F16PUBG}8WxEEGw`;Hs zx<~nk$5ais=UJ?ABledg5h&!e8!=a(WuPtai=Sl{7y>KaTiFENw76gl)#UxND8$;y zrLzox!)MdIGI43MvB@z%JDb*{V-@TtiBk+P%K@>vHxTprp}CZ*nbmW)QFQ5CGKc(g z7E=4_KmMSd_Cx$3A0p5hZ(2ZMuqGWfk2>jEF57CFZ`X3!R*#vFeQ`IxJfH6O`h2qm z9-o)Z@s$NAxJL@s=Ti&F*xtH2^YB8S(jP8F>38|kLh7Ol6&EZv%wgXbC{?W~ONSId z9e4Nzs#9ezwr)y-$4TB|3gZy*h}h@vuBXclTSSf2exJ;b7EvARJwCOFTrNPi3&qGh zss+2x*?DxI9+-BXxROVWJyivXJdsiC11Z*sw=q;6VxM-LslWQrr=?MgsSyO%<%_AF zz(aTG)5U20BR;2M1eJkI(2HC+I1KFqLLDri0EJr8T zc};ISb;zfGe<}dX{{G*CnV!MVe7d7&yUt3i*TX)|&shnW9r4Nh=Sm7yd#19rigx^# zsXFR2Rl3=SSG$o+J8)IyW2^D`Qh$DM1k9R))&P$hvp8{KWA49(_Sn+{J*lTV&+Ej| zOQ{xrw3e2l#M7_RO7LEeb<_b7BAgb&GY0rQIV^jyGrfQRY`>&85Bdd74IG}8>l~1sk>kwD&UI#t8kCdE z_wJ{N2D)HFXKuFh$w3*J19F`G)3c<=A2Xb}12ddC>4P(P?0#zQKRA0t25;I=O&iLS z`=v8ICnqD9^K7pdntZmR_173K6_+V#xQBXX@6o+ zj$J*jbILu~xO1JE8R&g`rV`^BKF5$TzO2Bhbr=hB}>(%*XqW@iq_7}m(yKRYWY zcUbx$jLm!@y?^eY5gC4&*-uFeeh&}K$a3a#-ZrXM{J=IS^ZC+SpgNzvMbB7%xHX)* z1^OjzFNM!rVDX&oAZ?txlS26DF4}7rp7jfQ zF0i<854~;`KfjlHS^V`rO5;cO6QaL3(W9&!p8qaXCWrP@PgMBevR}}&K#qHlVv7gA thv4OW`2$2!&27gcKBO;D;NZ`GK_@B~_xXrkQN{irQ { const aaTables = { assets: (scope) => atomicassets.tables.assets(nameToBigInt(Name.from(scope))).getTableRows(), - holders: () => atomicassets.tables.holders(nameToBigInt(atomicassets.name)).getTableRows(), + leases: () => atomicassets.tables.leases(nameToBigInt(atomicassets.name)).getTableRows(), offers: () => atomicassets.tables.offers(nameToBigInt(atomicassets.name)).getTableRows(), }; const marketTables = { @@ -373,9 +373,6 @@ describe('atomicmarket end to end', () => { await atomicmarket.actions.announcerent([ 'seller', ASSET1, '0.50 USD', '8,WAX', 86400, '', ]).send('seller@active'); - await atomicassets.actions.transfer([ - 'seller', MARKET, [ASSET1], 'rental', - ]).send('seller@active'); await deposit('renter', 20); @@ -389,6 +386,8 @@ describe('atomicmarket end to end', () => { const sellerTokens = token.tables.accounts(nameToBigInt(Name.from('seller'))).getTableRows(); expect(sellerTokens).toEqual([{ balance: WAX(17.6) }]); // 88% of 20 WAX expect(marketTables.rentals()[0].holder).toBe('renter'); + // non-custodial: the renter is the real owner now + expect(aaTables.assets('renter').map((a) => a.asset_id)).toContain(ASSET1); }); test('lowering the collection fee after listing discounts the executed sale', async () => { @@ -688,26 +687,26 @@ describe('atomicmarket end to end', () => { /* 3. Rentals */ /* ------------------------------------------------------------------ */ + // Non-custodial: announcing a rental is all it takes - the asset stays with the lister. const listAndActivateRental = async () => { await atomicmarket.actions.announcerent([ 'seller', ASSET1, WAX(0.5), '8,WAX', 86400, '', ]).send('seller@active'); - await atomicassets.actions.transfer([ - 'seller', MARKET, [ASSET1], 'rental', - ]).send('seller@active'); }; - test('full rental lifecycle: announce, custody, rent, extend, expire, endrent, cancel', async () => { + test('full rental lifecycle: announce, rent, extend, expire, endrent, cancel', async () => { await listAndActivateRental(); let rentals = marketTables.rentals(); expect(rentals.length).toBe(1); - expect(rentals[0].asset_transferred).toBe(true); + expect(rentals[0].is_rented).toBe(false); expect(rentals[0].owner).toBe('seller'); expect(rentals[0].holder).toBe(''); - // the market contract is now the custodial owner - expect(aaTables.assets(MARKET).length).toBe(1); + // non-custodial: the asset is still the seller's until it's rented + expect(aaTables.assets('seller').map((a) => a.asset_id)).toContain(ASSET1); + expect(aaTables.assets(MARKET).length).toBe(0); + expect(aaTables.leases()).toEqual([]); // rent for 2 hours = 1.0 WAX await deposit('renter', 2); @@ -717,11 +716,19 @@ describe('atomicmarket end to end', () => { rentals = marketTables.rentals(); expect(rentals[0].holder).toBe('renter'); - - // holdership moved to the renter inside atomicassets - expect(aaTables.holders()).toEqual([ - { asset_id: ASSET1, holder: 'renter', owner: MARKET }, - ]); + expect(rentals[0].is_rented).toBe(true); + + // the renter is now the REAL atomicassets owner, the asset is locked, and the lister's + // reclaim right is parked in the leases table + expect(aaTables.assets('renter').map((a) => a.asset_id)).toContain(ASSET1); + expect(aaTables.assets('seller').map((a) => a.asset_id)).not.toContain(ASSET1); + expect(aaTables.leases()).toEqual([{ + asset_id: ASSET1, + title_owner: 'seller', + renter: 'renter', + rental_end: Number(rentals[0].rental_end), + market: MARKET, + }]); // payout: 1.0 WAX -> 2% fees.atomic, 10% author, 88% straight to the seller expect(balanceOf('author')).toEqual([WAX(0.1)]); @@ -747,29 +754,33 @@ describe('atomicmarket end to end', () => { atomicmarket.actions.endrent([ASSET1]).send('renter2@active') ).rejects.toThrow(/not over yet/); - // the same renter can extend (1 more hour = 0.5 WAX) + // the same renter can extend (1 more hour = 0.5 WAX); no second ownership flip const endBefore = Number(marketTables.rentals()[0].rental_end); await atomicmarket.actions.rentasset([ 'renter', ASSET1, 1, WAX(0.5), 0, '', ]).send('renter@active'); expect(Number(marketTables.rentals()[0].rental_end)).toBe(endBefore + 3600); + expect(aaTables.assets('renter').map((a) => a.asset_id)).toContain(ASSET1); - // jump past the rental end; anyone can wrap it up + // jump past the rental end; anyone can wrap it up (endrent triggers the reclaim) blockchain.addTime(TimePoint.fromMilliseconds(4 * 3600 * 1000)); await atomicmarket.actions.endrent([ASSET1]).send('renter2@active'); - expect(aaTables.holders()).toEqual([]); + // asset returned to the lister; lock cleared; mirror reset + expect(aaTables.leases()).toEqual([]); + expect(aaTables.assets('seller').map((a) => a.asset_id)).toContain(ASSET1); rentals = marketTables.rentals(); expect(rentals[0].holder).toBe(''); + expect(rentals[0].is_rented).toBe(false); expect(Number(rentals[0].rental_end)).toBe(0); - // owner cancels; the asset comes back + // owner cancels the (now idle) listing await atomicmarket.actions.cancelrent([ASSET1]).send('seller@active'); expect(marketTables.rentals()).toEqual([]); expect(aaTables.assets('seller').length).toBe(2); // ASSET1 + ASSET2 }); - test('renting after expiry without endrent moves holdership from the old renter', async () => { + test('renting after expiry without endrent re-leases from the old renter to the new one', async () => { await listAndActivateRental(); await deposit('renter', 1); @@ -779,19 +790,26 @@ describe('atomicmarket end to end', () => { blockchain.addTime(TimePoint.fromMilliseconds(2 * 3600 * 1000)); - // no endrent in between - renter2 rents directly + // no endrent in between - renter2 rents directly. rentasset reclaims the expired lease + // (asset back to the lister) and then re-leases to renter2 in the same transaction. await deposit('renter2', 1); await atomicmarket.actions.rentasset([ 'renter2', ASSET1, 1, WAX(0.5), 0, '', ]).send('renter2@active'); - expect(aaTables.holders()).toEqual([ - { asset_id: ASSET1, holder: 'renter2', owner: MARKET }, - ]); + expect(aaTables.assets('renter2').map((a) => a.asset_id)).toContain(ASSET1); + expect(aaTables.assets('renter').map((a) => a.asset_id)).not.toContain(ASSET1); + expect(aaTables.leases()).toEqual([{ + asset_id: ASSET1, + title_owner: 'seller', + renter: 'renter2', + rental_end: Number(marketTables.rentals()[0].rental_end), + market: MARKET, + }]); expect(marketTables.rentals()[0].holder).toBe('renter2'); }); - test('cancelrent on an expired-but-not-ended rental reclaims holdership and the asset', async () => { + test('cancelrent on an expired-but-not-ended rental reclaims the asset to the lister', async () => { await listAndActivateRental(); await deposit('renter', 1); @@ -803,7 +821,7 @@ describe('atomicmarket end to end', () => { await atomicmarket.actions.cancelrent([ASSET1]).send('seller@active'); - expect(aaTables.holders()).toEqual([]); + expect(aaTables.leases()).toEqual([]); expect(marketTables.rentals()).toEqual([]); expect(aaTables.assets('seller').length).toBe(2); }); @@ -828,7 +846,7 @@ describe('atomicmarket end to end', () => { test('rental payouts use the discounted fee at execution time', async () => { await listAndActivateRental(); - // discount applied after the listing was created and the asset custodied + // discount applied after the listing was created await atomicassets.actions.setmarketfee([COL, 0.05]).send('author@active'); await deposit('renter', 1); @@ -897,6 +915,59 @@ describe('atomicmarket end to end', () => { ).rejects.toThrow(/differs from the expected price/); }); + test('a rented (locked) asset cannot be listed for sale, auction, buyoffer or re-rent', async () => { + await listAndActivateRental(); + await deposit('renter', 2); + await atomicmarket.actions.rentasset([ + 'renter', ASSET1, 2, WAX(0.5), 0, '', + ]).send('renter@active'); + + // the renter is the current owner, but the asset is rental-locked + await expect( + atomicmarket.actions.announcesale([ + 'renter', [ASSET1], WAX(1), '8,WAX', '', + ]).send('renter@active') + ).rejects.toThrow(/rented out|locked/); + + await expect( + atomicmarket.actions.announceauct([ + 'renter', [ASSET1], WAX(1), 600, '', + ]).send('renter@active') + ).rejects.toThrow(/rented out|locked/); + + await deposit('buyer', 1); + await expect( + atomicmarket.actions.createbuyo([ + 'buyer', 'renter', WAX(1), [ASSET1], '', '', + ]).send('buyer@active') + ).rejects.toThrow(/rented out|locked/); + + await expect( + atomicmarket.actions.announcerent([ + 'renter', ASSET1, WAX(0.5), '8,WAX', 86400, '', + ]).send('renter@active') + ).rejects.toThrow(/rented out|locked/); + + // after expiry the permissionless reclaim lifts the lock and the lister can list it again + blockchain.addTime(TimePoint.fromMilliseconds(3 * 3600 * 1000)); + await atomicmarket.actions.endrent([ASSET1]).send('renter@active'); + await expect( + atomicmarket.actions.announcesale([ + 'seller', [ASSET1], WAX(1), '8,WAX', '', + ]).send('seller@active') + ).resolves.not.toThrow(); + }); + + test('a stray "rental" memo transfer to the market is now rejected', async () => { + await listAndActivateRental(); + // non-custodial rentals never escrow the asset, so the old "rental" intake is gone + await expect( + atomicassets.actions.transfer([ + 'seller', MARKET, [ASSET1], 'rental', + ]).send('seller@active') + ).rejects.toThrow(/Invalid memo/); + }); + /* ------------------------------------------------------------------ */ /* 4. Default marketplace creator + balance migration (XPR) */ /* ------------------------------------------------------------------ */ From e05dba8fca81224c296bb7a582ec322ed3e97328 Mon Sep 17 00:00:00 2001 From: Rob Konsdorf Date: Sun, 28 Jun 2026 21:43:00 -0400 Subject: [PATCH 02/15] refactor(rentals): address review (simplify lease-state checks, refresh fixture) - A leases row always implies an active lease now, so simplify the lease_exists checks (drop the redundant renter!="" clause) in rentasset/endrent/cancelrent. - Refresh the bundled AtomicAssets test fixture to the updated non-custodial build (pretitle/delpretitle removed, rentalcfg singleton). Sale/rental cross-listing is intentionally left to coexist: the losing listing reverts safely and is swept by atomicmarket-cronjobs (a cancel-invalid-rentals follow-on, mirroring cancel-invalid-sales). --- src/atomicmarket.cpp | 6 +- tests/fixtures/atomicassets/atomicassets.abi | 59 +++++------------- tests/fixtures/atomicassets/atomicassets.wasm | Bin 250697 -> 244895 bytes 3 files changed, 20 insertions(+), 45 deletions(-) diff --git a/src/atomicmarket.cpp b/src/atomicmarket.cpp index 6d0a3be..f05e624 100644 --- a/src/atomicmarket.cpp +++ b/src/atomicmarket.cpp @@ -1901,7 +1901,7 @@ ACTION atomicmarket::cancelrent( atomicassets::leases_t aa_leases = atomicassets::get_leases(); auto lease_itr = aa_leases.find(asset_id); - bool lease_exists = lease_itr != aa_leases.end() && lease_itr->renter != name(""); + bool lease_exists = lease_itr != aa_leases.end(); bool active_lease = lease_exists && lease_itr->rental_end > current_time; check(!active_lease, @@ -1977,7 +1977,7 @@ ACTION atomicmarket::rentasset( // rentals mirror below can lag a keeper-driven reclaim. atomicassets::leases_t aa_leases = atomicassets::get_leases(); auto lease_itr = aa_leases.find(asset_id); - bool lease_exists = lease_itr != aa_leases.end() && lease_itr->renter != name(""); + bool lease_exists = lease_itr != aa_leases.end(); bool active_lease = lease_exists && lease_itr->rental_end > current_time; bool is_extension = active_lease && lease_itr->renter == renter; @@ -2112,7 +2112,7 @@ ACTION atomicmarket::endrent( // reclaim. If a keeper already reclaimed it, the row is gone and we just resync the mirror. atomicassets::leases_t aa_leases = atomicassets::get_leases(); auto lease_itr = aa_leases.find(asset_id); - if (lease_itr != aa_leases.end() && lease_itr->renter != name("")) { + if (lease_itr != aa_leases.end()) { check(lease_itr->rental_end <= current_time, "The rental period is not over yet"); action( diff --git a/tests/fixtures/atomicassets/atomicassets.abi b/tests/fixtures/atomicassets/atomicassets.abi index 30b03ea..cf3d9c9 100644 --- a/tests/fixtures/atomicassets/atomicassets.abi +++ b/tests/fixtures/atomicassets/atomicassets.abi @@ -351,10 +351,6 @@ { "name": "supported_tokens", "type": "extended_symbol[]" - }, - { - "name": "rental_market", - "type": "name" } ] }, @@ -540,16 +536,6 @@ } ] }, - { - "name": "delpretitle", - "base": "", - "fields": [ - { - "name": "asset_id", - "type": "uint64" - } - ] - }, { "name": "deltemplate", "base": "", @@ -1133,24 +1119,6 @@ } ] }, - { - "name": "pretitle", - "base": "", - "fields": [ - { - "name": "title_owner", - "type": "name" - }, - { - "name": "market", - "type": "name" - }, - { - "name": "asset_id", - "type": "uint64" - } - ] - }, { "name": "reclaim", "base": "", @@ -1221,6 +1189,16 @@ } ] }, + { + "name": "rentalcfg_s", + "base": "", + "fields": [ + { + "name": "rental_market", + "type": "name" + } + ] + }, { "name": "schema_types_s", "base": "", @@ -1580,11 +1558,6 @@ "type": "declineoffer", "ricardian_contract": "" }, - { - "name": "delpretitle", - "type": "delpretitle", - "ricardian_contract": "" - }, { "name": "deltemplate", "type": "deltemplate", @@ -1685,11 +1658,6 @@ "type": "payofferram", "ricardian_contract": "" }, - { - "name": "pretitle", - "type": "pretitle", - "ricardian_contract": "" - }, { "name": "reclaim", "type": "reclaim", @@ -1821,6 +1789,13 @@ "key_names": [], "key_types": [] }, + { + "name": "rentalcfg", + "type": "rentalcfg_s", + "index_type": "i64", + "key_names": [], + "key_types": [] + }, { "name": "schemas", "type": "schemas_s", diff --git a/tests/fixtures/atomicassets/atomicassets.wasm b/tests/fixtures/atomicassets/atomicassets.wasm index bb1135e8290634bb8bd514ce536c066ffff1f41e..16d8a6f840e1fe90dd78624b95995d1f598a03c7 100755 GIT binary patch delta 54686 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 z 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 ViOZd6wGHA!M`^fMP7{me{{TbkB_aR- delta 59948 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

q*=KXO`}~G!uz?{&(H~WCbdIi$;hC$zhhrS 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 S&pvgGWA>{F{;QJx{QehaKvQi1 From 3e032f862c184d417f2a87e2f8c59b8e834b323d Mon Sep 17 00:00:00 2001 From: Rob Konsdorf Date: Sun, 28 Jun 2026 22:02:17 -0400 Subject: [PATCH 03/15] test(rentals): refresh AA fixture (leaseextend expiry guard) Pick up the updated non-custodial atomicassets build so the integration tests run against the leaseextend-after-expiry guard added in atomicassets-contract. --- tests/fixtures/atomicassets/atomicassets.wasm | Bin 244895 -> 245055 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/tests/fixtures/atomicassets/atomicassets.wasm b/tests/fixtures/atomicassets/atomicassets.wasm index 16d8a6f840e1fe90dd78624b95995d1f598a03c7..a2a2a91e3e9f2b008212c12dcea125f457da72cc 100755 GIT binary patch delta 9649 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 r Date: Sun, 28 Jun 2026 22:04:41 -0400 Subject: [PATCH 04/15] docs(rentals): drop stale pretitle references, note cancelrent precondition (Copilot review) - leases_s interface comment and the lock-refusal comment no longer mention the removed "pretitle" sentinel; a leases row always means an active lease. - Document that cancelrent's non-rented branch assumes the non-custodial deploy has no legacy custodial (escrowed) rentals in flight, so "owner no longer owns it" always means the lister moved the asset, never contract-escrowed state. --- include/atomicassets-interface.hpp | 4 ++-- src/atomicmarket.cpp | 5 ++++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/include/atomicassets-interface.hpp b/include/atomicassets-interface.hpp index 90845ff..6059bf9 100644 --- a/include/atomicassets-interface.hpp +++ b/include/atomicassets-interface.hpp @@ -149,8 +149,8 @@ namespace atomicassets { // Non-custodial rental lock/title record. A row's existence means the asset - // is leased and locked; renter == name("") && rental_end == 0 is a pretitle - // sentinel (locked, owner still the lister). + // is actively leased and locked: the renter is the real owner and title_owner + // holds the reclaim right until rental_end. struct leases_s { uint64_t asset_id; name title_owner; diff --git a/src/atomicmarket.cpp b/src/atomicmarket.cpp index f05e624..ee74fc1 100644 --- a/src/atomicmarket.cpp +++ b/src/atomicmarket.cpp @@ -1921,6 +1921,9 @@ ACTION atomicmarket::cancelrent( } else { // Not rented: the asset is still with the lister, nothing to return. Anyone may cancel an // invalid listing (the owner no longer owns the asset); otherwise the owner must authorize. + // Precondition: this non-custodial flow deploys with no legacy custodial rentals in flight + // (the asset is never escrowed in the contract), so "owner no longer owns it" here always + // means the lister moved/sold it, never that the contract holds an escrowed asset. atomicassets::assets_t owner_assets = atomicassets::get_assets(rental_itr->owner); bool is_rental_invalid = owner_assets.find(asset_id) == owner_assets.end(); @@ -2535,7 +2538,7 @@ name atomicmarket::get_collection_and_check_assets( ("The specified account does not own at least one of the assets - " + to_string(asset_id)).c_str()); - // A rental-locked (leased or pre-titled) asset cannot be listed for sale, auction or + // A rental-locked (leased) asset cannot be listed for sale, auction or // buyoffer. Refuse it early here rather than letting it fail at settlement time. The // AtomicAssets transfer/offer guards remain the authoritative backstop. check(aa_leases.find(asset_id) == aa_leases.end(), From abe2b3482c24b6db3863731738e88c11ebcf617f Mon Sep 17 00:00:00 2001 From: Rob Konsdorf Date: Sun, 28 Jun 2026 22:23:51 -0400 Subject: [PATCH 05/15] fix(rentals): cap extensions by total duration from lease start, not a rolling window The maximum_rental_duration check measured the remaining period from "now", so a renter who kept paying could perpetually extend within a rolling window and hold the asset indefinitely, leaving the title owner no guaranteed return. Cap the total from the lease's rental_start (read from the AtomicAssets leases table) instead. Refresh the AA fixture; add a regression test that a from-now-valid but from-start-excessive extension is rejected. --- include/atomicassets-interface.hpp | 1 + src/atomicmarket.cpp | 8 ++++- tests/fixtures/atomicassets/atomicassets.abi | 4 +++ tests/fixtures/atomicassets/atomicassets.wasm | Bin 245055 -> 245234 bytes tests/market-smoke.test.js | 30 ++++++++++++++++++ 5 files changed, 42 insertions(+), 1 deletion(-) diff --git a/include/atomicassets-interface.hpp b/include/atomicassets-interface.hpp index 6059bf9..006a0b6 100644 --- a/include/atomicassets-interface.hpp +++ b/include/atomicassets-interface.hpp @@ -155,6 +155,7 @@ namespace atomicassets { uint64_t asset_id; name title_owner; name renter; + uint32_t rental_start; uint32_t rental_end; name market; diff --git a/src/atomicmarket.cpp b/src/atomicmarket.cpp index ee74fc1..22f5237 100644 --- a/src/atomicmarket.cpp +++ b/src/atomicmarket.cpp @@ -1991,7 +1991,13 @@ ACTION atomicmarket::rentasset( uint64_t base_time = is_extension ? (uint64_t) lease_itr->rental_end : (uint64_t) current_time; uint64_t new_rental_end = base_time + added_duration; - check(new_rental_end - current_time <= rental_itr->maximum_rental_duration, + // Cap the TOTAL rental period from when the lease was first opened, not from + // "now". Measuring from now would let a renter perpetually extend within a + // rolling window and hold the asset indefinitely, so the title owner could + // never get it back. For a fresh rental the start is now (added_duration); + // for an extension it is the lease's original rental_start. + uint64_t lease_start = is_extension ? (uint64_t) lease_itr->rental_start : (uint64_t) current_time; + check(new_rental_end - lease_start <= rental_itr->maximum_rental_duration, "The rental period would exceed the maximum rental duration of this listing"); check(is_valid_marketplace(taker_marketplace), "The taker marketplace is not a valid marketplace"); diff --git a/tests/fixtures/atomicassets/atomicassets.abi b/tests/fixtures/atomicassets/atomicassets.abi index cf3d9c9..aeca859 100644 --- a/tests/fixtures/atomicassets/atomicassets.abi +++ b/tests/fixtures/atomicassets/atomicassets.abi @@ -639,6 +639,10 @@ "name": "renter", "type": "name" }, + { + "name": "rental_start", + "type": "uint32" + }, { "name": "rental_end", "type": "uint32" diff --git a/tests/fixtures/atomicassets/atomicassets.wasm b/tests/fixtures/atomicassets/atomicassets.wasm index a2a2a91e3e9f2b008212c12dcea125f457da72cc..ff7b0e5a97b03af32c06d7b381813c1a1f7c91e5 100755 GIT binary patch delta 1613 zcmZuxO=ule6rOYMOWwRYZ|2>mX?>)V4(}zG%D;|gHDRnw-*{T`$VHFx5(5D8qQZTG(}O;7>e z{Ce%?Pp6E!s(t+tr7+5Xr^{zTukea@?|Yg820QF&ce4F6pO`xSOPj~pJp*Q9CRL_+ zBJ@l%QB4XYOp3*l@w_BFfe$Yi%r$g2Cd@>fM$k=In9@YJG?DHECgfMsC_q(S5(R0R zxtKwO&j&QsQ&qq@m?}eEX&^>&5kvU7XhTR2R~gZjK18i6JwR|&MONnGve@~`#mBqq zD%4^$7avqds{((nu&zKb7fViyq4mx=wGH(Rr)g+so#sP(g|%EP0&UAH@m>+7{*1t* z-a$$C77>RX>7|IzQPcYoBvOR$j=W7TAUXAg^CnoJCA{8w@lY41f$6bP;(pqqq{Z$~ z@~&Kji`FnaBZGt<&&6ZT*Ra_U5f98R?6wv$fq~g<6-|{0N4ER4Yb6wRCPmp62Mw@I zX~>PRES}@4a!EA=I?o3jst=5iGsCqA?NmrJ$GMX3hC7U#L2>o#Trsq3;p;4g!KXY2 zs_@G+%*DFPBoM3XKPPg{Lp^w1kH}ub|gme`Uf4>cE35&J}>LCpJlf zwdBu9DT6 zD-}KaO+=G1W1$tO#60YDAR=?^r|jPmM%2ac*sWCTz!>Q~m+oFHIo8$rbs8GKP0s%C z{DnM!m@_X?7_y!5V=zudCg!X0Q^f|zMtKj!c>t@+*}>)9p4qIfvcc8No~tUIO%o2r z_B#DHx0`#zxl7J`Ptzz9@!D93{%3CtuZ?OqM7z7;?v4|*+BTf@U)FSy=#KquAN@t6 z_Qx?gSU+l{87bnmec#RY+xzKl66>=6;W#B^H(!zm?fF;bqcqHuxjD7J4+&@{#U zPbTQTtoWUcKKc?v6W4uv(Gh%AFs8zy)LoAX9@rAWy zAjqwdqfU#Fv_aPty^Z=_&hoBuxMEG(AZazjwn>|ArAOw`slq?U>nX^ns9 zfBN>#o~dX*QMvbc(}S;;-=M-M`?I1`qpBtS*vDIyiYQTbxlV_U)H_m_$s+f?E=;{l zc?C?NfWjSa#NTloTjN;4HNLAVH7fzMPh~1G&0dP>I9sk zs{>POVFph&J>dD_F5}4;bqWBk?+k#Ip^h{-s+ovzNN!gd(UCrmN=JG~imr-$&R%HH zOb_3k5$@etQDkC+Z3SqVc*$82tQVZ6VXZpLhxII5nOH%xp;zF!AqxEwp$ISzX)i-~ z*^*vT->Iv|(GwY{YvKte zkr~uDAS)l~KsHivJm^|6_ln7`F&)ri?lXm%IL9JaxnCqfCO&qP zWS)Foo#IrP_>hcegpmk22bI_Ivv`i^M*zqJ;2SaLAf!U1Or(hn((0OH;kB(f;wNqSJDhhq0(fkY* zPUtY#cmV;NyI|Kg)KQw)%gvAuPIGGLg$k@lCf;+3%f!3xv}%+N_22q+zL$zk+P7#| zdGh-=E#OVVN_nxuKJ9E8Pna7{MYr5Em0t#id#hIZ7IuA$|8k}5>Vc)ay~kG$4U4+F zp__DXMX#14MP?}DHo4S$1fxuoFNo&k@`I(i~|Y?dCNIlDSblQXVDl`j`#s)pP>vFNO* zNTj_mOGo9Lp0V$T!VK`i!$i>=-Ixa5SZr(og#`*@50A-tduM{~W9w^s { asset_id: ASSET1, title_owner: 'seller', renter: 'renter', + rental_start: Number(rentals[0].rental_end) - 2 * 3600, rental_end: Number(rentals[0].rental_end), market: MARKET, }]); @@ -803,6 +804,7 @@ describe('atomicmarket end to end', () => { asset_id: ASSET1, title_owner: 'seller', renter: 'renter2', + rental_start: Number(marketTables.rentals()[0].rental_end) - 1 * 3600, rental_end: Number(marketTables.rentals()[0].rental_end), market: MARKET, }]); @@ -826,6 +828,34 @@ describe('atomicmarket end to end', () => { expect(aaTables.assets('seller').length).toBe(2); }); + test('extensions are capped at the maximum duration measured from the lease start', async () => { + // listAndActivateRental lists with maximum_rental_duration = 86400s (24h) + await listAndActivateRental(); + await deposit('renter', 20); + + // rent 20h (start = T0, end = T0 + 20h) + await atomicmarket.actions.rentasset([ + 'renter', ASSET1, 20, WAX(0.5), 0, '', + ]).send('renter@active'); + const end = Number(marketTables.rentals()[0].rental_end); + + // advance 10h, still active. Extending by 5h would push the total to 25h + // from the lease start (> 24h) even though only 15h would remain from + // "now" - the from-start cap rejects it (a rolling-window cap would not). + blockchain.addTime(TimePoint.fromMilliseconds(10 * 3600 * 1000)); + await expect( + atomicmarket.actions.rentasset([ + 'renter', ASSET1, 5, WAX(0.5), 0, '', + ]).send('renter@active') + ).rejects.toThrow(/maximum rental duration/); + + // extending by 4h reaches exactly 24h from start, which is allowed + await atomicmarket.actions.rentasset([ + 'renter', ASSET1, 4, WAX(0.5), 0, '', + ]).send('renter@active'); + expect(Number(marketTables.rentals()[0].rental_end)).toBe(end + 4 * 3600); + }); + test('rental payouts respect royalty splits', async () => { await setupRoyaltySplits(); await listAndActivateRental(); From 78f42f17b91fa63560e45a1bf41be6f03bd475f6 Mon Sep 17 00:00:00 2001 From: Rob Konsdorf Date: Sun, 28 Jun 2026 22:44:58 -0400 Subject: [PATCH 06/15] refactor(rentals): collapse holder + is_rented into a single renter field The rentals row carried two redundant fields for one fact: holder (write-only, never read) and is_rented (read once in endrent), while the AtomicAssets leases table is the real source of truth. Collapse to a single `renter` name field (name("") when not rented, the renter otherwise) that doubles as the rented flag and carries the renter identity for indexers. Rename holder -> renter to match the AtomicAssets leases terminology. Refresh the AA fixture and tests. --- include/atomicmarket.hpp | 3 +-- src/atomicmarket.cpp | 11 ++++------- tests/fixtures/atomicassets/atomicassets.wasm | Bin 245234 -> 245079 bytes tests/market-smoke.test.js | 17 +++++++---------- 4 files changed, 12 insertions(+), 19 deletions(-) diff --git a/include/atomicmarket.hpp b/include/atomicmarket.hpp index 459da0a..d6467ff 100644 --- a/include/atomicmarket.hpp +++ b/include/atomicmarket.hpp @@ -679,12 +679,11 @@ CONTRACT atomicmarket : public contract { TABLE rentals_s { uint64_t asset_id; name owner; // the listing creator; receives the rental payouts - name holder; // the current renter; name("") when not rented out + name renter; // the current renter; name("") when not rented out asset price_per_hour; // denoted in the listing symbol symbol settlement_symbol; // what the rental is actually paid in uint32_t maximum_rental_duration; // seconds; the longest period a rental can cover uint32_t rental_end; // seconds since epoch; 0 when not rented out - bool is_rented; // true while a renter holds an active lease name maker_marketplace; name collection_name; double collection_fee; diff --git a/src/atomicmarket.cpp b/src/atomicmarket.cpp index 22f5237..ea25a1e 100644 --- a/src/atomicmarket.cpp +++ b/src/atomicmarket.cpp @@ -1850,12 +1850,11 @@ ACTION atomicmarket::announcerent( rentals.emplace(lister, [&](auto &_rental) { _rental.asset_id = asset_id; _rental.owner = lister; - _rental.holder = name(""); + _rental.renter = name(""); _rental.price_per_hour = price_per_hour; _rental.settlement_symbol = settlement_symbol; _rental.maximum_rental_duration = maximum_rental_duration; _rental.rental_end = 0; - _rental.is_rented = false; _rental.maker_marketplace = maker_marketplace; _rental.collection_name = assets_collection_name; _rental.collection_fee = collection_fee; @@ -2074,9 +2073,8 @@ ACTION atomicmarket::rentasset( } rentals.modify(rental_itr, same_payer, [&](auto &_rental) { - _rental.holder = renter; + _rental.renter = renter; _rental.rental_end = (uint32_t) new_rental_end; - _rental.is_rented = true; }); action( @@ -2113,7 +2111,7 @@ ACTION atomicmarket::endrent( auto rental_itr = rentals.require_find(asset_id, "No rental listing with this asset_id exists"); - check(rental_itr->is_rented, "This asset is not currently rented out"); + check(rental_itr->renter != name(""), "This asset is not currently rented out"); uint32_t current_time = current_time_point().sec_since_epoch(); @@ -2133,9 +2131,8 @@ ACTION atomicmarket::endrent( } rentals.modify(rental_itr, same_payer, [&](auto &_rental) { - _rental.holder = name(""); + _rental.renter = name(""); _rental.rental_end = 0; - _rental.is_rented = false; }); } diff --git a/tests/fixtures/atomicassets/atomicassets.wasm b/tests/fixtures/atomicassets/atomicassets.wasm index ff7b0e5a97b03af32c06d7b381813c1a1f7c91e5..92bdb3444a9841aedd965a02780b8d4970617d0f 100755 GIT binary patch delta 4860 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 GANVhzVEQBg delta 4787 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 custody transfer -> rent (holdership moves to the renter - * in the atomicassets holders table) -> extension -> expiry -> endrent -> cancelrent. + * 3. Non-custodial rentals: announce (no escrow) -> rent (renter becomes the real atomicassets + * owner via a leases lock) -> extension -> expiry -> reclaim/endrent -> cancelrent. */ const WAX = (amount) => `${amount.toFixed(8)} WAX`; @@ -385,7 +385,7 @@ describe('atomicmarket end to end', () => { expect(balanceOf('fees.atomic')).toEqual([WAX(0.4)]); // 2% of 20 WAX const sellerTokens = token.tables.accounts(nameToBigInt(Name.from('seller'))).getTableRows(); expect(sellerTokens).toEqual([{ balance: WAX(17.6) }]); // 88% of 20 WAX - expect(marketTables.rentals()[0].holder).toBe('renter'); + expect(marketTables.rentals()[0].renter).toBe('renter'); // non-custodial: the renter is the real owner now expect(aaTables.assets('renter').map((a) => a.asset_id)).toContain(ASSET1); }); @@ -699,9 +699,8 @@ describe('atomicmarket end to end', () => { let rentals = marketTables.rentals(); expect(rentals.length).toBe(1); - expect(rentals[0].is_rented).toBe(false); expect(rentals[0].owner).toBe('seller'); - expect(rentals[0].holder).toBe(''); + expect(rentals[0].renter).toBe(''); // non-custodial: the asset is still the seller's until it's rented expect(aaTables.assets('seller').map((a) => a.asset_id)).toContain(ASSET1); @@ -715,8 +714,7 @@ describe('atomicmarket end to end', () => { ]).send('renter@active'); rentals = marketTables.rentals(); - expect(rentals[0].holder).toBe('renter'); - expect(rentals[0].is_rented).toBe(true); + expect(rentals[0].renter).toBe('renter'); // the renter is now the REAL atomicassets owner, the asset is locked, and the lister's // reclaim right is parked in the leases table @@ -771,8 +769,7 @@ describe('atomicmarket end to end', () => { expect(aaTables.leases()).toEqual([]); expect(aaTables.assets('seller').map((a) => a.asset_id)).toContain(ASSET1); rentals = marketTables.rentals(); - expect(rentals[0].holder).toBe(''); - expect(rentals[0].is_rented).toBe(false); + expect(rentals[0].renter).toBe(''); expect(Number(rentals[0].rental_end)).toBe(0); // owner cancels the (now idle) listing @@ -808,7 +805,7 @@ describe('atomicmarket end to end', () => { rental_end: Number(marketTables.rentals()[0].rental_end), market: MARKET, }]); - expect(marketTables.rentals()[0].holder).toBe('renter2'); + expect(marketTables.rentals()[0].renter).toBe('renter2'); }); test('cancelrent on an expired-but-not-ended rental reclaims the asset to the lister', async () => { From 907454456e75e1c87ce455762c6994d831919094 Mon Sep 17 00:00:00 2001 From: Rob Konsdorf Date: Sun, 28 Jun 2026 23:04:43 -0400 Subject: [PATCH 07/15] refactor(rentals): drop leases.market mirror and the market arg on inline calls Match the AtomicAssets simplification: the leases interface has no market column and leasestart/leaseextend take no market argument. Update the inline rentasset calls, the fixture, and the test assertions. --- include/atomicassets-interface.hpp | 1 - src/atomicmarket.cpp | 3 +-- tests/fixtures/atomicassets/atomicassets.abi | 16 ---------------- tests/fixtures/atomicassets/atomicassets.wasm | Bin 245079 -> 244598 bytes tests/market-smoke.test.js | 2 -- 5 files changed, 1 insertion(+), 21 deletions(-) diff --git a/include/atomicassets-interface.hpp b/include/atomicassets-interface.hpp index 006a0b6..d4b8a9c 100644 --- a/include/atomicassets-interface.hpp +++ b/include/atomicassets-interface.hpp @@ -157,7 +157,6 @@ namespace atomicassets { name renter; uint32_t rental_start; uint32_t rental_end; - name market; uint64_t primary_key() const { return asset_id; }; uint64_t by_title_owner() const { return title_owner.value; }; diff --git a/src/atomicmarket.cpp b/src/atomicmarket.cpp index ea25a1e..6360ecd 100644 --- a/src/atomicmarket.cpp +++ b/src/atomicmarket.cpp @@ -2042,7 +2042,7 @@ ACTION atomicmarket::rentasset( permission_level{get_self(), name("active")}, atomicassets::ATOMICASSETS_ACCOUNT, name("leaseextend"), - make_tuple(get_self(), asset_id, (uint32_t) new_rental_end) + make_tuple(asset_id, (uint32_t) new_rental_end) ).send(); } else { // A previous lease that expired but was never reclaimed still locks the asset. Clear it @@ -2062,7 +2062,6 @@ ACTION atomicmarket::rentasset( atomicassets::ATOMICASSETS_ACCOUNT, name("leasestart"), make_tuple( - get_self(), rental_itr->owner, renter, asset_id, diff --git a/tests/fixtures/atomicassets/atomicassets.abi b/tests/fixtures/atomicassets/atomicassets.abi index aeca859..ac1334b 100644 --- a/tests/fixtures/atomicassets/atomicassets.abi +++ b/tests/fixtures/atomicassets/atomicassets.abi @@ -609,10 +609,6 @@ "name": "leaseextend", "base": "", "fields": [ - { - "name": "market", - "type": "name" - }, { "name": "asset_id", "type": "uint64" @@ -646,10 +642,6 @@ { "name": "rental_end", "type": "uint32" - }, - { - "name": "market", - "type": "name" } ] }, @@ -657,10 +649,6 @@ "name": "leasestart", "base": "", "fields": [ - { - "name": "market", - "type": "name" - }, { "name": "title_owner", "type": "name" @@ -784,10 +772,6 @@ { "name": "rental_end", "type": "uint32" - }, - { - "name": "market", - "type": "name" } ] }, diff --git a/tests/fixtures/atomicassets/atomicassets.wasm b/tests/fixtures/atomicassets/atomicassets.wasm index 92bdb3444a9841aedd965a02780b8d4970617d0f..d3943f61be14236b30ec2920431304dfbfac7c43 100755 GIT binary patch delta 16318 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 aD0~wQ>NceRAji;NJZF>KV2^#sX8RvU0Y+Z{ delta 16803 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 { renter: 'renter', rental_start: Number(rentals[0].rental_end) - 2 * 3600, rental_end: Number(rentals[0].rental_end), - market: MARKET, }]); // payout: 1.0 WAX -> 2% fees.atomic, 10% author, 88% straight to the seller @@ -803,7 +802,6 @@ describe('atomicmarket end to end', () => { renter: 'renter2', rental_start: Number(marketTables.rentals()[0].rental_end) - 1 * 3600, rental_end: Number(marketTables.rentals()[0].rental_end), - market: MARKET, }]); expect(marketTables.rentals()[0].renter).toBe('renter2'); }); From e8613b72b01be88a2c8fbcbf81fce459a98968da Mon Sep 17 00:00:00 2001 From: Rob Konsdorf Date: Sun, 28 Jun 2026 23:17:46 -0400 Subject: [PATCH 08/15] docs(rentals): note the lock-refusal helper also guards announcerent (Copilot review) --- src/atomicmarket.cpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/atomicmarket.cpp b/src/atomicmarket.cpp index 6360ecd..e10e9ef 100644 --- a/src/atomicmarket.cpp +++ b/src/atomicmarket.cpp @@ -2540,9 +2540,10 @@ name atomicmarket::get_collection_and_check_assets( ("The specified account does not own at least one of the assets - " + to_string(asset_id)).c_str()); - // A rental-locked (leased) asset cannot be listed for sale, auction or - // buyoffer. Refuse it early here rather than letting it fail at settlement time. The - // AtomicAssets transfer/offer guards remain the authoritative backstop. + // A rental-locked (leased) asset cannot be listed by any caller of this helper: + // sale, auction, buyoffer, or a new rental listing. Refuse it early here rather than + // letting it fail at settlement time. The AtomicAssets transfer/offer guards remain + // the authoritative backstop. check(aa_leases.find(asset_id) == aa_leases.end(), ("One of the assets is currently rented out (locked) - " + to_string(asset_id)).c_str()); From a0990351e8345b0a180801edb71d5fadd0f1fdae Mon Sep 17 00:00:00 2001 From: Rob Konsdorf Date: Mon, 29 Jun 2026 16:34:51 -0400 Subject: [PATCH 09/15] test: refresh bundled AtomicAssets fixture to the reclaim-hardened build The integration suite runs against a snapshot of the non-custodial AA contract. Update it so the tests exercise the un-vetoable reclaim and the lease-duration backstop (atomicassets-contract#26). No interface change; AM 231 pass. --- tests/fixtures/atomicassets/atomicassets.wasm | Bin 244598 -> 244689 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/tests/fixtures/atomicassets/atomicassets.wasm b/tests/fixtures/atomicassets/atomicassets.wasm index d3943f61be14236b30ec2920431304dfbfac7c43..c1bd3edcd6f874e0ee2e3953eaca039096eeb5f1 100755 GIT binary patch delta 9860 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 DQ?V82 From 32bd125e950c0d336fe31bfaa0170ebf357b5d6c Mon Sep 17 00:00:00 2001 From: Rob Konsdorf Date: Tue, 30 Jun 2026 15:50:15 -0400 Subject: [PATCH 10/15] refactor(rentals): drop the lease-state mirror, AA leases is the source of truth Post-review cleanup of the non-custodial rental orchestration (pairs with atomicassets-contract#26): - The rentals table no longer mirrors lock state. renter/rental_end and the never-read rentalends index are removed; rentals_s is now immutable listing config and the AtomicAssets leases table is the single source of truth for who is renting and until when. endrent reads the lease, not a mirror row. - Pin the asset_scope royalty invariant with a comment and a regression test (an extension must pay template/attribute royalties from the renter scope). - Extract send_aa_reclaim to drop the triplicated reclaim emit. - Add collection_name to the leases interface struct (matches AA); enable leasing in the test setup and refresh the bundled AA fixture. --- include/atomicassets-interface.hpp | 1 + include/atomicmarket.hpp | 12 +-- src/atomicmarket.cpp | 82 ++++++------------ tests/fixtures/atomicassets/atomicassets.abi | 4 + tests/fixtures/atomicassets/atomicassets.wasm | Bin 244689 -> 243991 bytes tests/market-smoke.test.js | 74 +++++++++++----- 6 files changed, 89 insertions(+), 84 deletions(-) diff --git a/include/atomicassets-interface.hpp b/include/atomicassets-interface.hpp index d4b8a9c..3f3d11b 100644 --- a/include/atomicassets-interface.hpp +++ b/include/atomicassets-interface.hpp @@ -155,6 +155,7 @@ namespace atomicassets { uint64_t asset_id; name title_owner; name renter; + name collection_name; uint32_t rental_start; uint32_t rental_end; diff --git a/include/atomicmarket.hpp b/include/atomicmarket.hpp index d6467ff..989cd46 100644 --- a/include/atomicmarket.hpp +++ b/include/atomicmarket.hpp @@ -676,25 +676,22 @@ CONTRACT atomicmarket : public contract { typedef multi_index template_buyoffers_t; + // Immutable rental listing config. Lock state (renter, start/end, whether it's currently rented) + // is NOT mirrored here - the AtomicAssets leases table is the single source of truth for that. TABLE rentals_s { uint64_t asset_id; name owner; // the listing creator; receives the rental payouts - name renter; // the current renter; name("") when not rented out asset price_per_hour; // denoted in the listing symbol symbol settlement_symbol; // what the rental is actually paid in uint32_t maximum_rental_duration; // seconds; the longest period a rental can cover - uint32_t rental_end; // seconds since epoch; 0 when not rented out name maker_marketplace; name collection_name; double collection_fee; uint64_t primary_key() const { return asset_id; }; - uint64_t by_rental_end() const { return (uint64_t) rental_end; }; }; - typedef multi_index >> - rentals_t; + typedef multi_index rentals_t; TABLE marketplaces_s { name marketplace_name; @@ -861,6 +858,9 @@ CONTRACT atomicmarket : public contract { void internal_transfer_assets(name to, const vector &asset_ids, const string &memo); + // Triggers the AtomicAssets permissionless reclaim (shared by rentasset, endrent and cancelrent). + void send_aa_reclaim(uint64_t asset_id); + }; diff --git a/src/atomicmarket.cpp b/src/atomicmarket.cpp index e10e9ef..74559c3 100644 --- a/src/atomicmarket.cpp +++ b/src/atomicmarket.cpp @@ -1850,11 +1850,9 @@ ACTION atomicmarket::announcerent( rentals.emplace(lister, [&](auto &_rental) { _rental.asset_id = asset_id; _rental.owner = lister; - _rental.renter = name(""); _rental.price_per_hour = price_per_hour; _rental.settlement_symbol = settlement_symbol; _rental.maximum_rental_duration = maximum_rental_duration; - _rental.rental_end = 0; _rental.maker_marketplace = maker_marketplace; _rental.collection_name = assets_collection_name; _rental.collection_fee = collection_fee; @@ -1910,13 +1908,7 @@ ACTION atomicmarket::cancelrent( // Expired but never reclaimed: only the owner can cancel, and the asset returns to them // via the permissionless reclaim. require_auth(rental_itr->owner); - - action( - permission_level{get_self(), name("active")}, - atomicassets::ATOMICASSETS_ACCOUNT, - name("reclaim"), - make_tuple(asset_id) - ).send(); + send_aa_reclaim(asset_id); } else { // Not rented: the asset is still with the lister, nothing to return. Anyone may cancel an // invalid listing (the owner no longer owns the asset); otherwise the owner must authorize. @@ -1990,11 +1982,8 @@ ACTION atomicmarket::rentasset( uint64_t base_time = is_extension ? (uint64_t) lease_itr->rental_end : (uint64_t) current_time; uint64_t new_rental_end = base_time + added_duration; - // Cap the TOTAL rental period from when the lease was first opened, not from - // "now". Measuring from now would let a renter perpetually extend within a - // rolling window and hold the asset indefinitely, so the title owner could - // never get it back. For a fresh rental the start is now (added_duration); - // for an extension it is the lease's original rental_start. + // Cap the total period from the lease's original open (rental_start), not from "now" - measuring + // from now would let a renter roll the window forward forever and never return the asset. uint64_t lease_start = is_extension ? (uint64_t) lease_itr->rental_start : (uint64_t) current_time; check(new_rental_end - lease_start <= rental_itr->maximum_rental_duration, "The rental period would exceed the maximum rental duration of this listing"); @@ -2017,10 +2006,10 @@ ACTION atomicmarket::rentasset( uint64_t rental_counter_id = consume_counter(name("rental")); - // The royalty engine reads the asset's data from its CURRENT owner scope. Non-custodially - // that is whoever owns the asset right now (the inline lease actions below run only after - // this action returns): the current renter for an extension or an expired-unreclaimed - // re-rental, otherwise the listing owner. + // asset_scope must be the asset's current owner at payout time: the renter for an extension or + // expired re-rental, else the listing owner. This holds only because the inline reclaim/leasestart + // below run after this body. Don't reorder them before the payout - distribute_collection_fee uses + // find() (not require_find), so a wrong scope SILENTLY drops template/attribute royalties. name asset_scope = lease_exists ? lease_itr->renter : rental_itr->owner; internal_payout_sale( @@ -2048,12 +2037,7 @@ ACTION atomicmarket::rentasset( // A previous lease that expired but was never reclaimed still locks the asset. Clear it // first (permissionless reclaim) so leasestart can re-lease from the listing owner. if (lease_exists) { - action( - permission_level{get_self(), name("active")}, - atomicassets::ATOMICASSETS_ACCOUNT, - name("reclaim"), - make_tuple(asset_id) - ).send(); + send_aa_reclaim(asset_id); } // Make the renter the real AtomicAssets owner and lock the asset (the lister's reclaim // right is parked in the AtomicAssets leases table). @@ -2071,11 +2055,6 @@ ACTION atomicmarket::rentasset( ).send(); } - rentals.modify(rental_itr, same_payer, [&](auto &_rental) { - _rental.renter = renter; - _rental.rental_end = (uint32_t) new_rental_end; - }); - action( permission_level{get_self(), name("active")}, get_self(), @@ -2097,9 +2076,9 @@ ACTION atomicmarket::rentasset( /** * Wraps up an expired rental, making the listing rentable again * -* The authoritative return of the asset to the lister is the permissionless AtomicAssets -* `reclaim`. endrent triggers that reclaim if it has not happened yet (e.g. no keeper ran) and -* then resyncs atomicmarket's mirror row. It can be called by anyone. +* The asset is returned to the lister by the permissionless AtomicAssets `reclaim`. endrent is just +* the AtomicMarket-facing trigger for it once the rental is over; a keeper calling AA `reclaim` +* directly does the same thing. Can be called by anyone. * * @required_auth None */ @@ -2107,32 +2086,17 @@ ACTION atomicmarket::endrent( uint64_t asset_id ) { auto rentals = get_rentals(); - auto rental_itr = rentals.require_find(asset_id, - "No rental listing with this asset_id exists"); - - check(rental_itr->renter != name(""), "This asset is not currently rented out"); - - uint32_t current_time = current_time_point().sec_since_epoch(); + rentals.require_find(asset_id, "No rental listing with this asset_id exists"); - // If the lease still exists in AtomicAssets it must have expired; trigger the permissionless - // reclaim. If a keeper already reclaimed it, the row is gone and we just resync the mirror. + // AtomicAssets leases is the source of truth: a row means it's still rented; no row means a + // keeper already reclaimed it (nothing to do). atomicassets::leases_t aa_leases = atomicassets::get_leases(); auto lease_itr = aa_leases.find(asset_id); - if (lease_itr != aa_leases.end()) { - check(lease_itr->rental_end <= current_time, "The rental period is not over yet"); - - action( - permission_level{get_self(), name("active")}, - atomicassets::ATOMICASSETS_ACCOUNT, - name("reclaim"), - make_tuple(asset_id) - ).send(); - } + check(lease_itr != aa_leases.end(), "This asset is not currently rented out"); + check(lease_itr->rental_end <= current_time_point().sec_since_epoch(), + "The rental period is not over yet"); - rentals.modify(rental_itr, same_payer, [&](auto &_rental) { - _rental.renter = name(""); - _rental.rental_end = 0; - }); + send_aa_reclaim(asset_id); } @@ -3493,3 +3457,13 @@ void atomicmarket::internal_transfer_assets( ) ).send(); } + + +void atomicmarket::send_aa_reclaim(uint64_t asset_id) { + action( + permission_level{get_self(), name("active")}, + atomicassets::ATOMICASSETS_ACCOUNT, + name("reclaim"), + make_tuple(asset_id) + ).send(); +} diff --git a/tests/fixtures/atomicassets/atomicassets.abi b/tests/fixtures/atomicassets/atomicassets.abi index ac1334b..6c17ea1 100644 --- a/tests/fixtures/atomicassets/atomicassets.abi +++ b/tests/fixtures/atomicassets/atomicassets.abi @@ -635,6 +635,10 @@ "name": "renter", "type": "name" }, + { + "name": "collection_name", + "type": "name" + }, { "name": "rental_start", "type": "uint32" diff --git a/tests/fixtures/atomicassets/atomicassets.wasm b/tests/fixtures/atomicassets/atomicassets.wasm index c1bd3edcd6f874e0ee2e3953eaca039096eeb5f1..9f1a8883e84b21f421f844d21eb65a49f8ec21ee 100755 GIT binary patch delta 12713 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 EAEL|=4*&oF delta 13119 zcmcIL33OD&vQyPF$z&#k2?R(&fIE{w5)w9*fQBVQKu}aRL6ltxOIU&|vWQ6tARx$M zfR^`AP*IRI7~sMOkxfMujLNDYVNoD#K@pG-@~isZ$qb;+fBre2cT87zS6A0w-F@fD zU&Fq=5f*CcGjb?cEEdQU53dU?4~22?i|FWxaJ$2;@s5OA4yzR++&0Z>)71YGZQ<71 z4l9Ja-PZ6VASlW0PI5TV3Rbr@nXscBG-$0&Q`XwnSX&*d%^GD*%I|KANvvzFXNCIK zaJR!69H>>gz-Oz?V-YVZQUVc&uie!v3U2qRu~lA4e+bQg?;Ts zUflmjVZ$doI&tQ$!s(qefZNh8gH_w3tI_U#zB~8xr*}>*?Axujf=j!k7Vhm{U%@@P zrWQ8pX~27*Pc2;9%Re68$3LF^Vsl0J)4nSz@jipc%~R{)m9Lom{#U-?(Zv&PDy)Us zU#Yf#E~++jVvxcfk;iEH>5~+8#N?W4Hg!svM#B;oLyf}B4=yXz7naq9Gx^h(#WLMB z%hpHYCwKDuZEsCY;p*)Uz^(tCzj?=d_`&V`9y{OQ=Hkv+IF;|()rm1PcD3SW_4cUz z&vv~JrE*ChNSD3B!Bx0*cR#=xf9B8GTdlD9=NAx43zNR=%R_~8_WpppY{Nu6{cyRVaIY7B3IxW5eaD3=#Qnf8vic8ZLtP-3WpQ};HuvXvrcNjJIy^c8jj`% zpMD58m*%IQ{x^>uIlYLRm(SF}Ev5PP(^~##XR6YC;i)ql1@E6+GP|1dGq2d=bl5B6zr-6d-oj5Pr8&)VZn9kx6y(V zMQ?Wm4KKJ}m~rX4Fih2{aP8F?te|P=ZJhZy%j32ZMcU{1Z`JU<>#}JVh|GWLnms?} zT4mejQj4ptOzQ%b3ZJ^x8GIyu|NR3;mE4KMC@IVps#y|+rGwJV>WTB$!8AA_9|O1r zN9E8)SWR|BxC|?FuK-;sD);*+S_aT9{3i47< zX^%9gc(HgdA5?A~W`C_8PIli)$$jCg5)H zI1+{39YQ|oNWf;19x{EKa>TN?fOhb6znsvS{1P8okF$}O!yV%OEuiH=-Z^rO1GI*7 z?&X>(*Ri;iIm^NAplK~M1y=znBGE8?P$DVF zu)S%O`mO|zgSI3Mu~i(JH3eg+v9oO*8a5IxYDf)sYD6PJ0x^S1X(>3+*Gs{Eez3Qe z6Xdm};376qkhj=Eq~BuL;f*0MK|zrO#VN?8cIH_-K`}hWn+JI#S?p@UoV|=S726Dywo6C|7VGQA!*5NSdLNJ&K5(iR3U9^4rGLki*ueh8&jNj6~F^p~zx? zBCn;tjquf%U!~N4iZTm1>%e)2Pd6?3BhRR^v25 zfDwv9vw36W+Hh!MPoP5{(5(C|9RB1j3v!^QjI0QCDwiD$f>Nm9l4Fp%oTsFFn24uR zQqUJ_$>Db~Qo1^cVBf%;zEHz}6Xe=3QQ0n@RO#%foO2gzflF?!Dk9~TzCdL}KN$g^ z0=|thG>(c?oSYvAb^Ch28*Hw*t>g}-N}^oUM#YnyS8+o_nQl9o6ue_I?^_V0+5HPx zV5?@;Y_5+Wn#6XI`t* zs7`R@TF@}(&0s6!U^dc!RK#*|ut%^(G)%$U6hE%_z)i;!&bLNU5NwNcpb0Jt#nc)l z&87+vSwa;c!DWh8713Yt9;SF$1+MOH8%anfov*<|P8H(`CX425pWHNOSzD#m3tS`u z5GXVW`!=|dPfn->8)zRLE<+ zF1R%21W2{Xl&XNGB0;G18PcihC%kVdnU)wb5fe)l=|!OGsBafqBD}Y`It?eLgA^Nw z)rrE?2ZcUCrIt+BYSM34PSZSKrXU3vDToM}DM+Xk#O_YngGAP>oPq=kLdGyt5EC&| z5CgK$d8pokElHBpr-n*K0yUIrG-_0ep&eLzoQV`i^p~>@MF$b44rd#A<2*E}Y#446 zc!GU-fL3mmua&^Vt&ua3#k-qUlGE_Po2$2GNx(-u=3L_krV@Np$-b!t7a$^M6Syr- zVlOxax3c#sO0?BEQ246)Q1>EuXdgN-Yv{I6RS~qEcVMQ6_Oy_KEXt_FLlCnT#Hk)N zK3@}Qre*9w%w9D1AW?IXBFQO7t$`$YHw3H8_AwCZ)HHa!9H7UT40+%X0%zpEl2r~ngl%4wKUK0Da7AkOuav{X%Zamx}|J;K{N z(_^JjBu7m_J2g}>DDb(VK_ds!jaKDH3bL3?kB~#E!K1-(B&^k|$yL=L9TH?|HK<38 zRvjLKWbQ@E!PTKkPO_5>jYKIS(?w?xn|C|sV&^ft%{bVKY>`75VGd;kBsS1{P936* zctOqrxu_v$0XoZ)dWmrl)vr?SP)jKSy~PwOYGGg&5M&rk$iXwqCYMPa@!%4+`un~^P;aDKk+|a5DAB!-Jxlh0Yq(;rbvEY#Pvnct0{Ty~|N~|b< zCxjwkf2RVSLb$$|qVZhx*aPTHMv5sr)3M8 zR_5-1b7zyu9++6^gu2jabis*pxfSo=O^_u`u&O-nftsPonG_Npt2c+LrR-fDD(i8v zunee7eH{nYV4W<9BR@9l+swhjtZ(r$r>3fP3u;o4Yg-H6#1%*6iCPee6OYP2YC$d7 zC$)IU;KRVwcxch4>`L|44;+@HM0*{J4kWjYZ{qaFjNV%s=$c8p0kM<{p=`Q{g2%{HKmhC}o?68;|VX3>t7} zb~9+|zsl;Tn~}QUN4;Vi$;hj<77%4ydYojI?cT#GvU3YqfuA0ilq7n6DV zuYCO}s1`l{CsmOXMH{oKyBsR$mL8Woo`OBN|0g-)Y3RXv?VS$QnYiN_GH?L#eVr6X z_Gf{JFK!J_nQR>-DIV_#oq5gM9q(nxzeXr!l3TD;-SUEHld(xB5j&%^Ne=`I^lo$OlR52Dk zOfmM@FbdN$fgmFdt)mjeRq zncj1H@MK6-sp6q2aGst(nZ-)-*FRvCl3>s+sHyLp25(x4+1_t}T?8x#S5Xn<3tG|HzwQ@`q70@%SLa)I=vPr*>GN-UUw;E z0`8t9|MdYZBCRy~kn-12x$Q92kc$t4T`u}iC3YH!)en6LD@Z0eaV5MVoWIxvTLNe-+zfk} zt$tgeE7|*#Eie@4o|DO2Dc7t!C);j?ns7wEv=u6|Lr>od@lf;i^BhBDzwJop8j7GB zs$Pa9ifO{k+q=)}2S1_cWiPn14ThUOlWY3Sxb0BaapgR@!EhpTMPIia`T*R}EAE8f z0;-H&?K4Ppl6_kjK|bmw@>mg!AZFWr3me%|!QaujNjYO1iUjh-r@n(^#{t8=Y~BNU z-gmGWnDM3upoJhpxrcq;)Hgs0jgL@OrfN3+D97_Dx#UOa4khx=kIGLSV`0+V+^^;04kcUD~_{<+#g~eZM$Q#k_#8V!he4 z&zDd!E~~csiBfuakV%*5N&2K*afvd|4SD(!#Nn!oGUW1u^sCEGmm%i9O}Y~O?=CC) z`Xg5$65yJAx)dU{Ym@01%rAFT0qHgN#i zc%Q*mY)$E~(2C_~U>yK5zVj}YiOlX1SUIfvsMPuz-_&41ojH} zD5Muw!iVlF84v0^Q#Ogl%cL_h(NK=Hh1IcF#PrLYD~K~XDjD9r=CTZ}fw8#%vTRTT zYfzkZsDXD;UeP;hXb1f4itO&j%dD!W=yuO-`&#IsQ{3Kobj!hU=#cN!LKl6|k_EMJ zvK$kOFEQ4Vcub@ZUh=DWOf@H?bV6UGvU^A^KP^0~u(St=sa^ky4r7)`XCj_wU%j4) zubOpjARQtoJhPK9*}3{EX|O8|tiGymNJ5=67k!HwiJPy=Q;pE$cNE#7HU2DvW1zFM zO~6}1$460BAo(cHHqZO{Yp*)=$&XQO{ad$4rh61sp@U7>-kY!LKQzS%C30yqgWR8K z=)vn(#xj&6NR3k)|q z3;mMlSuJq`vV^r-2TG`ugsIGqoYe;7{rGZQ8|=e6bU%T8gUgj%HQWQ!ymNn*bK7HF z$lPD~ZKge4_X;LJjQpWJR>OtA%0JrUx8y5dbwE|dE_T2VamR1^yp9y7|0$d@LdO3g)`PD0<6gwT;3&$IM$6zMqF%E%|e=RhFGbV+XbV(J`VK{|4afXnaHs z6qo}&U&c@A>Qa3S`KpcR(yi0E5M%TfV{xg$EGu%scvFuJR$v=BU_9j$!j%tBb$;b< zr_v|%}50blumu(_C`Mq1CsCO)8^+%Oki z+|&2aFq)=%dLu^3LG#EBQ|4hMh5XDs)3@i%GhL==p2-LSb!FOoj8Jr*n~%$dT7nBr z9$GClmgo}~;vU2LS&K~fe#ZywTZEnDoquDNTKkvtJMW#^@ixY*kqd8|CaUrdURERa z#ioO|Bfvr8(K(yWU%TbigA~3)7o$^tx)>`e+=C{r9|{;ef&gCbzh393TS>4 zBlVA$V#wdv&j=#2NUYv_8Llhy@G`G!BWHZ%cgA}hUspRkkXJrc!>3o8R*PAM4R}Ov zzX}}&{T{0^Ng>TxZN`1UYOHT8TaC38aQ6U4+U(@YH)P@(Q@6BL0ACf>;w**w{#rw4 zddXT0H!Mhb!_YY=5E--{a}=iPeYM^%PXM`(H<;woH<~7ReIq`pmL1+`xR72=$JK@i z?|fr=6#~k{q}SPo$p#~3shfbxa?__+$64kzlR;kHi4}FR9p6PiYZGLz`GmWB zC&sFMN_Lv=UT2pH4BurM@8mA5CztHPAcaUh_28h#?8b(MRL_2HD(l}q;FJ$Y{K5>^ zfnS*RS^EW^H=>-Ftt0n*Z59HmIy($<52#f+gC1LiHw}d9-OP%rDW zIE$OgI?M&uZqt`+dN|~vSVdz#h!NCRoK71ks>&s9;hg`a zpr4d^Md`?ZNvs-8d(bmWuGb2b3*%i%oODYPZ?w+nF+AH*w2+>Ia zglI@YfLpGqBOLNhgfL1mEsK-AE8S0o);5sWD~V=`03o?$ZnCiI-7AaPeEsZ^OwL|D zQg{rwp&Sq?A_>lo6#a~$co`ZcvIzbw&y&Wu&)Z~i^#4|MTdEqY>ZJ0qHXnI*7oFT? zUx?zD%_Bdm2vIUO%ye15r=Ca>jcDDBIwZj>6-A8vDN?X&l1NWevocw8wd6-JSxKdb2U;a8p<%AZxV>PuxViLXtTgxR9ax~d9Qd#FdJ{Q z6fMI@TmSTd;IDqf!Y&^rzqyNMUQkKEA*(zqvWenfc}-1OzO&%pk5C5a#W?(nlfW-> z=C4mq4i@oxgD!%8gn3lf8$!Q3ncq#Vqls8~xSN2RTLpL(O0#2RLTUeOX{*hrPS;zx=B zG&FmpNTYI5G?Ki8x_`B01-WOmXsz~rW1MJK-hW4R(laH>g0W^*`C+VaN&WI=Vcai$ zx7KlE#J#uH(lKIq89|QE_s5zTJ{r;E?!(wO?mmo`$QDnO(Yu^oMz6Bo zHb>Aijd3AVuMPoJX=zVh)qIlZ=3A|QJW0&*KT{AJ5pvy>zzy%=W%Fx+Q;L@+Qv-N; zdujkLx2F1eF>X0ZxLGp{Hk2}~GyMx>pP3@U_QMo@E%ItRStUw0# z%_`GVv{a8coo0&>LAboN{>6NeHlSL%;KDEm&NakWeys(UIZDvNpzLtH;IMTHt812{{X}r+tUC5 diff --git a/tests/market-smoke.test.js b/tests/market-smoke.test.js index 9344d14..66066c9 100644 --- a/tests/market-smoke.test.js +++ b/tests/market-smoke.test.js @@ -74,6 +74,10 @@ describe('atomicmarket end to end', () => { await atomicassets.actions.init([]).send(`${AA}@active`); await atomicmarket.actions.init([]).send(`${MARKET}@active`); + // Leasing is opt-in in AtomicAssets (rentalcfg defaults to disabled); authorize + // this market so the non-custodial rental flow can drive leasestart/leaseextend. + await atomicassets.actions.setrentmkt([MARKET]).send(`${AA}@active`); + await atomicmarket.actions.addconftoken(['eosio.token', '8,WAX']).send(`${MARKET}@active`); await mintTokens(token, 'WAX', 8, 1000000000, 10000, [buyer, renter, renter2]); @@ -385,7 +389,7 @@ describe('atomicmarket end to end', () => { expect(balanceOf('fees.atomic')).toEqual([WAX(0.4)]); // 2% of 20 WAX const sellerTokens = token.tables.accounts(nameToBigInt(Name.from('seller'))).getTableRows(); expect(sellerTokens).toEqual([{ balance: WAX(17.6) }]); // 88% of 20 WAX - expect(marketTables.rentals()[0].renter).toBe('renter'); + expect(aaTables.leases()[0].renter).toBe('renter'); // non-custodial: the renter is the real owner now expect(aaTables.assets('renter').map((a) => a.asset_id)).toContain(ASSET1); }); @@ -697,12 +701,11 @@ describe('atomicmarket end to end', () => { test('full rental lifecycle: announce, rent, extend, expire, endrent, cancel', async () => { await listAndActivateRental(); - let rentals = marketTables.rentals(); + const rentals = marketTables.rentals(); expect(rentals.length).toBe(1); expect(rentals[0].owner).toBe('seller'); - expect(rentals[0].renter).toBe(''); - // non-custodial: the asset is still the seller's until it's rented + // non-custodial: the asset is still the seller's until it's rented, and nothing is leased expect(aaTables.assets('seller').map((a) => a.asset_id)).toContain(ASSET1); expect(aaTables.assets(MARKET).length).toBe(0); expect(aaTables.leases()).toEqual([]); @@ -713,19 +716,18 @@ describe('atomicmarket end to end', () => { 'renter', ASSET1, 2, WAX(0.5), 0, '', ]).send('renter@active'); - rentals = marketTables.rentals(); - expect(rentals[0].renter).toBe('renter'); - - // the renter is now the REAL atomicassets owner, the asset is locked, and the lister's - // reclaim right is parked in the leases table + // the renter is now the REAL atomicassets owner, the asset is locked, and the lease row (the + // single source of truth for lock state) holds the lister's reclaim right expect(aaTables.assets('renter').map((a) => a.asset_id)).toContain(ASSET1); expect(aaTables.assets('seller').map((a) => a.asset_id)).not.toContain(ASSET1); - expect(aaTables.leases()).toEqual([{ + const leased = aaTables.leases(); + expect(leased).toEqual([{ asset_id: ASSET1, title_owner: 'seller', renter: 'renter', - rental_start: Number(rentals[0].rental_end) - 2 * 3600, - rental_end: Number(rentals[0].rental_end), + collection_name: COL, + rental_start: Number(leased[0].rental_end) - 2 * 3600, + rental_end: Number(leased[0].rental_end), }]); // payout: 1.0 WAX -> 2% fees.atomic, 10% author, 88% straight to the seller @@ -753,23 +755,21 @@ describe('atomicmarket end to end', () => { ).rejects.toThrow(/not over yet/); // the same renter can extend (1 more hour = 0.5 WAX); no second ownership flip - const endBefore = Number(marketTables.rentals()[0].rental_end); + const endBefore = Number(aaTables.leases()[0].rental_end); await atomicmarket.actions.rentasset([ 'renter', ASSET1, 1, WAX(0.5), 0, '', ]).send('renter@active'); - expect(Number(marketTables.rentals()[0].rental_end)).toBe(endBefore + 3600); + expect(Number(aaTables.leases()[0].rental_end)).toBe(endBefore + 3600); expect(aaTables.assets('renter').map((a) => a.asset_id)).toContain(ASSET1); // jump past the rental end; anyone can wrap it up (endrent triggers the reclaim) blockchain.addTime(TimePoint.fromMilliseconds(4 * 3600 * 1000)); await atomicmarket.actions.endrent([ASSET1]).send('renter2@active'); - // asset returned to the lister; lock cleared; mirror reset + // asset returned to the lister; lock cleared; the listing stays (rentable again) expect(aaTables.leases()).toEqual([]); expect(aaTables.assets('seller').map((a) => a.asset_id)).toContain(ASSET1); - rentals = marketTables.rentals(); - expect(rentals[0].renter).toBe(''); - expect(Number(rentals[0].rental_end)).toBe(0); + expect(marketTables.rentals().length).toBe(1); // owner cancels the (now idle) listing await atomicmarket.actions.cancelrent([ASSET1]).send('seller@active'); @@ -796,14 +796,15 @@ describe('atomicmarket end to end', () => { expect(aaTables.assets('renter2').map((a) => a.asset_id)).toContain(ASSET1); expect(aaTables.assets('renter').map((a) => a.asset_id)).not.toContain(ASSET1); - expect(aaTables.leases()).toEqual([{ + const leased = aaTables.leases(); + expect(leased).toEqual([{ asset_id: ASSET1, title_owner: 'seller', renter: 'renter2', - rental_start: Number(marketTables.rentals()[0].rental_end) - 1 * 3600, - rental_end: Number(marketTables.rentals()[0].rental_end), + collection_name: COL, + rental_start: Number(leased[0].rental_end) - 1 * 3600, + rental_end: Number(leased[0].rental_end), }]); - expect(marketTables.rentals()[0].renter).toBe('renter2'); }); test('cancelrent on an expired-but-not-ended rental reclaims the asset to the lister', async () => { @@ -832,7 +833,7 @@ describe('atomicmarket end to end', () => { await atomicmarket.actions.rentasset([ 'renter', ASSET1, 20, WAX(0.5), 0, '', ]).send('renter@active'); - const end = Number(marketTables.rentals()[0].rental_end); + const end = Number(aaTables.leases()[0].rental_end); // advance 10h, still active. Extending by 5h would push the total to 25h // from the lease start (> 24h) even though only 15h would remain from @@ -848,7 +849,7 @@ describe('atomicmarket end to end', () => { await atomicmarket.actions.rentasset([ 'renter', ASSET1, 4, WAX(0.5), 0, '', ]).send('renter@active'); - expect(Number(marketTables.rentals()[0].rental_end)).toBe(end + 4 * 3600); + expect(Number(aaTables.leases()[0].rental_end)).toBe(end + 4 * 3600); }); test('rental payouts respect royalty splits', async () => { @@ -868,6 +869,31 @@ describe('atomicmarket end to end', () => { expect(balanceOf('author')).toEqual(['0.00000002 WAX']); }); + test('rental extension pays template/attribute royalties from the renter scope', async () => { + // Pins the asset_scope invariant for the EXTENSION branch (asset_scope = renter): + // by extension time the renter is the AA owner, so the royalty engine must read the + // asset's template_id/attributes from the renter's scope. If asset_scope were wrong + // (or the lease flip were reordered before payout), distribute_collection_fee would + // silently drop temproy1/attrroy1 and this test's doubled balances would fail. + await setupRoyaltySplits(); + await listAndActivateRental(); + + await deposit('renter', 2); + // fresh rental (asset_scope = owner): 1.0 WAX + await atomicmarket.actions.rentasset([ + 'renter', ASSET1, 2, WAX(0.5), 0, '', + ]).send('renter@active'); + // same-renter extension while still active (asset_scope = renter): another 1.0 WAX + await atomicmarket.actions.rentasset([ + 'renter', ASSET1, 2, WAX(0.5), 0, '', + ]).send('renter@active'); + + // template + attribute royalty recipients must be paid for BOTH payouts (2 x 0.03333333), + // proving the extension read the asset from the renter scope, not an empty one. + expect(balanceOf('temproy1')).toEqual(['0.06666666 WAX']); + expect(balanceOf('attrroy1')).toEqual(['0.06666666 WAX']); + }); + test('rental payouts use the discounted fee at execution time', async () => { await listAndActivateRental(); From 658ef1e059a3b557cc9336b62da46688218d6f38 Mon Sep 17 00:00:00 2001 From: Rob Konsdorf Date: Tue, 30 Jun 2026 16:57:04 -0400 Subject: [PATCH 11/15] fix(rentals): make endrent idempotent; correct the flow doc (Copilot review) - endrent now no-ops when the AtomicAssets lease row is already gone (a keeper beat it) instead of hard-failing, matching its wrapper intent and the comment. - The rental-flow doc no longer claims endrent "resyncs its mirror row" - there is no mirror; the AtomicAssets leases table is the single source of truth. --- include/atomicmarket.hpp | 5 +++-- src/atomicmarket.cpp | 9 ++++++--- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/include/atomicmarket.hpp b/include/atomicmarket.hpp index 989cd46..37ca3a2 100644 --- a/include/atomicmarket.hpp +++ b/include/atomicmarket.hpp @@ -328,8 +328,9 @@ CONTRACT atomicmarket : public contract { 3. An extension by the same renter only bumps the lease end (leaseextend); no second ownership flip. 4. After the rental period is over, the asset returns to the lister via the permissionless - AtomicAssets `reclaim` (a keeper, endrent, or cancelrent triggers it). atomicmarket's - endrent then resyncs its mirror row, making the listing rentable again. + AtomicAssets `reclaim` (a keeper, endrent, or cancelrent triggers it). The listing is + rentable again as soon as the lease row is gone; atomicmarket keeps no lock state of its + own (the AtomicAssets leases table is the single source of truth). 5. The owner can cancel the listing whenever no rental is actively running (cancelrent); an expired-but-unreclaimed lease is reclaimed as part of the cancel. */ diff --git a/src/atomicmarket.cpp b/src/atomicmarket.cpp index 74559c3..422d005 100644 --- a/src/atomicmarket.cpp +++ b/src/atomicmarket.cpp @@ -2088,11 +2088,14 @@ ACTION atomicmarket::endrent( auto rentals = get_rentals(); rentals.require_find(asset_id, "No rental listing with this asset_id exists"); - // AtomicAssets leases is the source of truth: a row means it's still rented; no row means a - // keeper already reclaimed it (nothing to do). + // AtomicAssets leases is the source of truth. No row means it was already reclaimed (by a keeper + // or otherwise), so endrent is a no-op - it is idempotent and safe to call again. atomicassets::leases_t aa_leases = atomicassets::get_leases(); auto lease_itr = aa_leases.find(asset_id); - check(lease_itr != aa_leases.end(), "This asset is not currently rented out"); + if (lease_itr == aa_leases.end()) { + return; + } + check(lease_itr->rental_end <= current_time_point().sec_since_epoch(), "The rental period is not over yet"); From a07f751a1e698d68b6646b9391cf62920668ab1e Mon Sep 17 00:00:00 2001 From: Rob Konsdorf Date: Thu, 2 Jul 2026 13:13:02 -0400 Subject: [PATCH 12/15] feat(rentals): editrent + delist-during-lease, richer logrental, rental_id linkage Design-review follow-ups. The one economic fix: listing terms were frozen for the whole lease (cancelrent rejected during an active lease, announce requires not-leased), so at expiry anyone could atomically snipe a fresh full window at a stale price. The listing row is the owner's OFFER of future rentals, separate from the renter's purchased lease: - editrent reprices / re-bounds / re-attributes a listing at any time; the listing and settlement symbols stay immutable (expected_price pins amount + listing symbol only). Extensions pay the new terms; renters are protected by the existing expected_price_per_hour pin - cancelrent during an active lease is now an owner-auth delist that leaves the lease untouched; endrent is purely lease-driven and no longer needs a listing row - logrental carries rental_start and is_extension (extension windows were not derivable from the log), and its off-canon rental_counter_id param is renamed rental_id to match sale_id/auction_id/buyoffer_id - rentasset threads rental_id into AA leasestart, where it is stored on the lease and echoed in loglock/logreclaim for structural indexer joins - AA fixture refreshed to the rental_id-aware build --- include/atomicassets-interface.hpp | 1 + include/atomicmarket.hpp | 20 ++- src/atomicmarket.cpp | 134 ++++++++++++++---- tests/fixtures/atomicassets/atomicassets.abi | 39 +++++ tests/fixtures/atomicassets/atomicassets.wasm | Bin 243991 -> 246642 bytes tests/market-smoke.test.js | 105 +++++++++++++- 6 files changed, 269 insertions(+), 30 deletions(-) diff --git a/include/atomicassets-interface.hpp b/include/atomicassets-interface.hpp index 3f3d11b..b457c87 100644 --- a/include/atomicassets-interface.hpp +++ b/include/atomicassets-interface.hpp @@ -158,6 +158,7 @@ namespace atomicassets { name collection_name; uint32_t rental_start; uint32_t rental_end; + uint64_t rental_id; uint64_t primary_key() const { return asset_id; }; uint64_t by_title_owner() const { return title_owner.value; }; diff --git a/include/atomicmarket.hpp b/include/atomicmarket.hpp index 37ca3a2..3a9bf78 100644 --- a/include/atomicmarket.hpp +++ b/include/atomicmarket.hpp @@ -344,6 +344,13 @@ CONTRACT atomicmarket : public contract { name maker_marketplace ); + ACTION editrent( + uint64_t asset_id, + asset new_price_per_hour, + uint32_t new_maximum_rental_duration, + name new_maker_marketplace + ); + ACTION cancelrent( uint64_t asset_id ); @@ -472,16 +479,27 @@ CONTRACT atomicmarket : public contract { ); ACTION logrental( - uint64_t rental_counter_id, + uint64_t rental_id, uint64_t asset_id, name lister, name renter, uint32_t rental_hours, asset paid_settlement_price, + uint32_t rental_start, uint32_t rental_end, + bool is_extension, name taker_marketplace ); + ACTION logeditrent( + uint64_t asset_id, + name owner, + asset new_price_per_hour, + symbol settlement_symbol, + uint32_t new_maximum_rental_duration, + name new_maker_marketplace + ); + /* Royalty distribution logs - emitted by every settlement (sale, auction, buyoffer, rental) that distributes a collection fee through a royalty split config. One action diff --git a/src/atomicmarket.cpp b/src/atomicmarket.cpp index 422d005..b40dfa2 100644 --- a/src/atomicmarket.cpp +++ b/src/atomicmarket.cpp @@ -1876,14 +1876,77 @@ ACTION atomicmarket::announcerent( } +/** +* Edits the terms of an existing rental listing +* +* Allowed at any time, including while a lease is running: the listing row is the owner's OFFER +* of future rentals, separate from the renter's purchased lease. Without this, the terms would be +* frozen for the whole lease and the expiry second could be sniped at a stale price for a fresh +* full window. An extension by the incumbent renter is a fresh purchase of the current offer, so +* it pays the NEW terms; renters are protected by the expected_price_per_hour pin in rentasset. +* +* The listing symbol and settlement_symbol cannot change (expected_price_per_hour pins amount + +* listing symbol only, so a mutable settlement symbol could charge a renter holding multiple +* deposited tokens in an unintended one). Relist to change symbols. +* +* @required_auth The listing's owner +*/ +ACTION atomicmarket::editrent( + uint64_t asset_id, + asset new_price_per_hour, + uint32_t new_maximum_rental_duration, + name new_maker_marketplace +) { + auto rentals = get_rentals(); + auto rental_itr = rentals.require_find(asset_id, + "No rental listing with this asset_id exists"); + + require_auth(rental_itr->owner); + + check(new_price_per_hour.is_valid(), "Invalid type new_price_per_hour"); + check(new_price_per_hour.amount > 0, "The price per hour must be greater than zero"); + check(new_price_per_hour.symbol == rental_itr->price_per_hour.symbol, + "The listing symbol cannot be changed. Cancel and announce a new listing instead"); + + check(new_maximum_rental_duration >= 3600, + "The maximum rental duration must be at least one hour (3600 seconds)"); + check(new_maximum_rental_duration <= 2419200, + "The maximum rental duration can't be longer than 28 days"); + + check(is_valid_marketplace(new_maker_marketplace), "The maker marketplace is not a valid marketplace"); + + rentals.modify(rental_itr, same_payer, [&](auto &_rental) { + _rental.price_per_hour = new_price_per_hour; + _rental.maximum_rental_duration = new_maximum_rental_duration; + _rental.maker_marketplace = new_maker_marketplace; + }); + + action( + permission_level{get_self(), name("active")}, + get_self(), + name("logeditrent"), + make_tuple( + asset_id, + rental_itr->owner, + new_price_per_hour, + rental_itr->settlement_symbol, + new_maximum_rental_duration, + new_maker_marketplace + ) + ).send(); +} + + /** * Cancels a rental listing * * Non-custodially the asset is never escrowed, so cancelling is just removing the listing row. +* Delisting is NOT termination: cancelling during an active rental only withdraws the offer of +* future rentals - the renter's purchased lease is untouched and expires normally (the reclaim +* then returns the asset to the owner; the lease-driven endrent works without a listing row). * If no rental is active, the owner can cancel - or anyone can, if the owner no longer owns the -* asset (which makes the listing invalid). An actively running rental cannot be cancelled; an -* expired-but-unreclaimed rental is reclaimed (returning the asset to the owner) as part of the -* cancel. +* asset (which makes the listing invalid). An expired-but-unreclaimed rental is reclaimed +* (returning the asset to the owner) as part of the cancel. * * @required_auth The listing's owner (see above for the invalid listing exception) */ @@ -1898,17 +1961,15 @@ ACTION atomicmarket::cancelrent( atomicassets::leases_t aa_leases = atomicassets::get_leases(); auto lease_itr = aa_leases.find(asset_id); - bool lease_exists = lease_itr != aa_leases.end(); - bool active_lease = lease_exists && lease_itr->rental_end > current_time; - - check(!active_lease, - "The asset is currently rented out. The listing can only be cancelled after the rental period is over"); - if (lease_exists) { - // Expired but never reclaimed: only the owner can cancel, and the asset returns to them - // via the permissionless reclaim. + if (lease_itr != aa_leases.end()) { + // A lease row means only the owner may cancel. Active: withdraw the offer only - the + // renter keeps the lease they paid for. Expired but never reclaimed: also return the + // asset to the owner via the permissionless reclaim. require_auth(rental_itr->owner); - send_aa_reclaim(asset_id); + if (lease_itr->rental_end <= current_time) { + send_aa_reclaim(asset_id); + } } else { // Not rented: the asset is still with the lister, nothing to return. Anyone may cancel an // invalid listing (the owner no longer owns the asset); otherwise the owner must authorize. @@ -2004,7 +2065,7 @@ ACTION atomicmarket::rentasset( internal_decrease_balance(renter, settlement_price); - uint64_t rental_counter_id = consume_counter(name("rental")); + uint64_t rental_id = consume_counter(name("rental")); // asset_scope must be the asset's current owner at payout time: the renter for an extension or // expired re-rental, else the listing owner. This holds only because the inline reclaim/leasestart @@ -2021,12 +2082,13 @@ ACTION atomicmarket::rentasset( vector {asset_id}, asset_scope, name("rental"), - rental_counter_id, - "AtomicMarket Rental Payout - ID #" + to_string(rental_counter_id) + rental_id, + "AtomicMarket Rental Payout - ID #" + to_string(rental_id) ); if (is_extension) { - // Same renter, already the owner: only extend the lease end in AtomicAssets. + // Same renter, already the owner: only extend the lease end in AtomicAssets. The lease + // keeps the rental_id that opened it; this extension is identified by its own logrental. action( permission_level{get_self(), name("active")}, atomicassets::ATOMICASSETS_ACCOUNT, @@ -2040,7 +2102,8 @@ ACTION atomicmarket::rentasset( send_aa_reclaim(asset_id); } // Make the renter the real AtomicAssets owner and lock the asset (the lister's reclaim - // right is parked in the AtomicAssets leases table). + // right is parked in the AtomicAssets leases table). rental_id is echoed in the AA lease + // logs so indexers can join loglock/logreclaim to this rental structurally. action( permission_level{get_self(), name("active")}, atomicassets::ATOMICASSETS_ACCOUNT, @@ -2050,7 +2113,8 @@ ACTION atomicmarket::rentasset( renter, asset_id, (uint32_t) new_rental_end, - string("AtomicMarket Rental - ID # ") + to_string(rental_counter_id) + rental_id, + string("AtomicMarket Rental - ID # ") + to_string(rental_id) ) ).send(); } @@ -2060,13 +2124,15 @@ ACTION atomicmarket::rentasset( get_self(), name("logrental"), make_tuple( - rental_counter_id, + rental_id, asset_id, rental_itr->owner, renter, rental_hours, settlement_price, + (uint32_t) lease_start, (uint32_t) new_rental_end, + is_extension, taker_marketplace ) ).send(); @@ -2074,20 +2140,20 @@ ACTION atomicmarket::rentasset( /** -* Wraps up an expired rental, making the listing rentable again +* Wraps up an expired rental, returning the asset to the lister * -* The asset is returned to the lister by the permissionless AtomicAssets `reclaim`. endrent is just -* the AtomicMarket-facing trigger for it once the rental is over; a keeper calling AA `reclaim` +* The asset is returned by the permissionless AtomicAssets `reclaim`. endrent is just the +* AtomicMarket-facing trigger for it once the rental is over; a keeper calling AA `reclaim` * directly does the same thing. Can be called by anyone. * +* Purely lease-driven: it does not require a rentals listing row, so it keeps working for +* leases whose listing was delisted (cancelrent) while they ran. +* * @required_auth None */ ACTION atomicmarket::endrent( uint64_t asset_id ) { - auto rentals = get_rentals(); - rentals.require_find(asset_id, "No rental listing with this asset_id exists"); - // AtomicAssets leases is the source of truth. No row means it was already reclaimed (by a keeper // or otherwise), so endrent is a no-op - it is idempotent and safe to call again. atomicassets::leases_t aa_leases = atomicassets::get_leases(); @@ -2428,13 +2494,15 @@ ACTION atomicmarket::lognewrent( } ACTION atomicmarket::logrental( - uint64_t rental_counter_id, + uint64_t rental_id, uint64_t asset_id, name lister, name renter, uint32_t rental_hours, asset paid_settlement_price, + uint32_t rental_start, uint32_t rental_end, + bool is_extension, name taker_marketplace ) { require_auth(get_self()); @@ -2443,6 +2511,20 @@ ACTION atomicmarket::logrental( require_recipient(renter); } + +ACTION atomicmarket::logeditrent( + uint64_t asset_id, + name owner, + asset new_price_per_hour, + symbol settlement_symbol, + uint32_t new_maximum_rental_duration, + name new_maker_marketplace +) { + require_auth(get_self()); + + require_recipient(owner); +} + // The royalty distribution logs intentionally notify nobody (see the header comment): // a require_recipient to an arbitrary payout recipient would let a recipient contract // assert in its notification handler and block the collection's settlements diff --git a/tests/fixtures/atomicassets/atomicassets.abi b/tests/fixtures/atomicassets/atomicassets.abi index 6c17ea1..72e4847 100644 --- a/tests/fixtures/atomicassets/atomicassets.abi +++ b/tests/fixtures/atomicassets/atomicassets.abi @@ -646,6 +646,10 @@ { "name": "rental_end", "type": "uint32" + }, + { + "name": "rental_id", + "type": "uint64" } ] }, @@ -669,6 +673,10 @@ "name": "rental_end", "type": "uint32" }, + { + "name": "rental_id", + "type": "uint64" + }, { "name": "memo", "type": "string" @@ -773,9 +781,17 @@ "name": "renter", "type": "name" }, + { + "name": "rental_start", + "type": "uint32" + }, { "name": "rental_end", "type": "uint32" + }, + { + "name": "rental_id", + "type": "uint64" } ] }, @@ -934,6 +950,10 @@ { "name": "renter", "type": "name" + }, + { + "name": "rental_id", + "type": "uint64" } ] }, @@ -1188,6 +1208,10 @@ { "name": "rental_market", "type": "name" + }, + { + "name": "max_lease_seconds", + "type": "uint32" } ] }, @@ -1269,6 +1293,16 @@ } ] }, + { + "name": "setleasecap", + "base": "", + "fields": [ + { + "name": "max_lease_seconds", + "type": "uint32" + } + ] + }, { "name": "setmarketfee", "base": "", @@ -1690,6 +1724,11 @@ "type": "setlastpayer", "ricardian_contract": "" }, + { + "name": "setleasecap", + "type": "setleasecap", + "ricardian_contract": "" + }, { "name": "setmarketfee", "type": "setmarketfee", diff --git a/tests/fixtures/atomicassets/atomicassets.wasm b/tests/fixtures/atomicassets/atomicassets.wasm index 9f1a8883e84b21f421f844d21eb65a49f8ec21ee..8fd8f45ce9c1e1ddfb52fe0e6bc7b9fd53a821fe 100755 GIT binary patch delta 24964 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 delta 23556 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 ANB{r; diff --git a/tests/market-smoke.test.js b/tests/market-smoke.test.js index 66066c9..2a07bea 100644 --- a/tests/market-smoke.test.js +++ b/tests/market-smoke.test.js @@ -728,6 +728,7 @@ describe('atomicmarket end to end', () => { collection_name: COL, rental_start: Number(leased[0].rental_end) - 2 * 3600, rental_end: Number(leased[0].rental_end), + rental_id: 1, // the AM rental counter, echoed on the lease for indexer joins }]); // payout: 1.0 WAX -> 2% fees.atomic, 10% author, 88% straight to the seller @@ -744,10 +745,11 @@ describe('atomicmarket end to end', () => { ]).send('renter2@active') ).rejects.toThrow(/currently rented out/); - // owner can't cancel while active + // cancelling during an active lease is the owner's delist right, nobody else's + // (the delist-during-lease semantics have their own dedicated test below) await expect( - atomicmarket.actions.cancelrent([ASSET1]).send('seller@active') - ).rejects.toThrow(/currently rented out/); + atomicmarket.actions.cancelrent([ASSET1]).send('renter2@active') + ).rejects.toThrow(/missing required authority/); // endrent before expiry fails await expect( @@ -804,9 +806,106 @@ describe('atomicmarket end to end', () => { collection_name: COL, rental_start: Number(leased[0].rental_end) - 1 * 3600, rental_end: Number(leased[0].rental_end), + rental_id: 2, // the re-lease is a NEW rental: fresh counter id on the fresh lease }]); }); + test('editrent reprices the listing mid-lease: extensions and re-rents pay the NEW terms', async () => { + await listAndActivateRental(); // 0.5 WAX/h, 24h max + + await deposit('renter', 10); + await atomicmarket.actions.rentasset([ + 'renter', ASSET1, 2, WAX(0.5), 0, '', + ]).send('renter@active'); + + // mid-lease the owner reprices to 2 WAX/h and shortens the max to 12h: the listing row + // is the owner's OFFER of future rentals, separate from the renter's purchased lease + await atomicmarket.actions.editrent([ + ASSET1, WAX(2), 12 * 3600, '', + ]).send('seller@active'); + expect(marketTables.rentals()[0].price_per_hour).toBe(WAX(2)); + expect(Number(marketTables.rentals()[0].maximum_rental_duration)).toBe(12 * 3600); + + // the running lease is untouched + expect(aaTables.leases()[0].renter).toBe('renter'); + + // an extension is a fresh purchase of the current offer: the old price is now rejected... + await expect( + atomicmarket.actions.rentasset([ + 'renter', ASSET1, 1, WAX(0.5), 0, '', + ]).send('renter@active') + ).rejects.toThrow(/differs from the expected price/); + // ...and the new price charges 2 WAX for 1h + const endBefore = Number(aaTables.leases()[0].rental_end); + await atomicmarket.actions.rentasset([ + 'renter', ASSET1, 1, WAX(2), 0, '', + ]).send('renter@active'); + expect(Number(aaTables.leases()[0].rental_end)).toBe(endBefore + 3600); + + // stale-price snipe regression: at expiry, a re-rent must pay the CURRENT terms, not + // the price the listing had when the previous lease started + blockchain.addTime(TimePoint.fromMilliseconds(4 * 3600 * 1000)); + await deposit('renter2', 10); + await expect( + atomicmarket.actions.rentasset([ + 'renter2', ASSET1, 1, WAX(0.5), 0, '', + ]).send('renter2@active') + ).rejects.toThrow(/differs from the expected price/); + await atomicmarket.actions.rentasset([ + 'renter2', ASSET1, 1, WAX(2), 0, '', + ]).send('renter2@active'); + expect(aaTables.leases()[0].renter).toBe('renter2'); + }); + + test('editrent validation: owner-only, same listing symbol, duration bounds', async () => { + await listAndActivateRental(); + + await expect( + atomicmarket.actions.editrent([ + ASSET1, WAX(2), 86400, '', + ]).send('buyer@active') + ).rejects.toThrow(/missing required authority/); + + await expect( + atomicmarket.actions.editrent([ + ASSET1, '2.00 USD', 86400, '', + ]).send('seller@active') + ).rejects.toThrow(/listing symbol cannot be changed/); + + await expect( + atomicmarket.actions.editrent([ + ASSET1, WAX(2), 1800, '', + ]).send('seller@active') + ).rejects.toThrow(/at least one hour/); + }); + + test('delisting during an active lease withdraws the offer without touching the lease', async () => { + await listAndActivateRental(); + + await deposit('renter', 1); + await atomicmarket.actions.rentasset([ + 'renter', ASSET1, 2, WAX(0.5), 0, '', + ]).send('renter@active'); + + // only the owner may delist while the lease runs (the listing is not "invalid": + // the owner not owning the asset is the normal rented state) + await expect( + atomicmarket.actions.cancelrent([ASSET1]).send('renter2@active') + ).rejects.toThrow(/missing required authority/); + await atomicmarket.actions.cancelrent([ASSET1]).send('seller@active'); + + // listing gone, lease (and the renter's ownership) untouched + expect(marketTables.rentals()).toEqual([]); + expect(aaTables.leases()[0].renter).toBe('renter'); + expect(aaTables.assets('renter').map((a) => a.asset_id)).toContain(ASSET1); + + // the lease still expires normally, and the lease-driven endrent needs no listing row + blockchain.addTime(TimePoint.fromMilliseconds(3 * 3600 * 1000)); + await atomicmarket.actions.endrent([ASSET1]).send('renter2@active'); + expect(aaTables.leases()).toEqual([]); + expect(aaTables.assets('seller').map((a) => a.asset_id)).toContain(ASSET1); + }); + test('cancelrent on an expired-but-not-ended rental reclaims the asset to the lister', async () => { await listAndActivateRental(); From ec183bc5f8d15e0a848685e08d55df08f2520c99 Mon Sep 17 00:00:00 2001 From: Rob Konsdorf Date: Thu, 2 Jul 2026 12:21:21 -0400 Subject: [PATCH 13/15] test(rentals): cover idempotent endrent, invalid-listing cancel, delphi extension Regression tests for three paths the security review flagged as unguarded: - endrent on an already-reclaimed asset is a no-op, not a throw - an invalid listing (owner moved the asset) is cancellable by anyone, while a still-valid one rejects a non-owner cancel - a delphi-priced rental extension settles the added hours at the oracle rate via the leaseextend path --- tests/market-smoke.test.js | 81 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) diff --git a/tests/market-smoke.test.js b/tests/market-smoke.test.js index 2a07bea..eef55af 100644 --- a/tests/market-smoke.test.js +++ b/tests/market-smoke.test.js @@ -394,6 +394,39 @@ describe('atomicmarket end to end', () => { expect(aaTables.assets('renter').map((a) => a.asset_id)).toContain(ASSET1); }); + test('delphi-priced rental extension settles the added hours at the oracle rate', async () => { + await setupDelphiPair(); + + await atomicmarket.actions.announcerent([ + 'seller', ASSET1, '0.50 USD', '8,WAX', 86400, '', + ]).send('seller@active'); + + await deposit('renter', 40); + + // fresh rental: 2h x 0.50 USD = 1.00 USD = 20 WAX (leasestart path) + await atomicmarket.actions.rentasset([ + 'renter', ASSET1, 2, '0.50 USD', 500, '', + ]).send('renter@active'); + const endAfterRent = Number(aaTables.leases()[0].rental_end); + + // same renter extends by 2h while active: exercises the leaseextend inline path under + // settlement conversion (another 1.00 USD = 20 WAX), no second ownership flip + await atomicmarket.actions.rentasset([ + 'renter', ASSET1, 2, '0.50 USD', 500, '', + ]).send('renter@active'); + + // lease end advanced by the added hours; renter is still the real owner (no re-lease) + expect(Number(aaTables.leases()[0].rental_end)).toBe(endAfterRent + 2 * 3600); + expect(aaTables.leases()[0].renter).toBe('renter'); + expect(aaTables.assets('renter').map((a) => a.asset_id)).toContain(ASSET1); + + // both payouts settled in WAX at the oracle rate (2 x 20 WAX total) + expect(balanceOf('author')).toEqual([WAX(4)]); // 10% of 40 WAX + expect(balanceOf('fees.atomic')).toEqual([WAX(0.8)]); // 2% of 40 WAX + const sellerTokens = token.tables.accounts(nameToBigInt(Name.from('seller'))).getTableRows(); + expect(sellerTokens).toEqual([{ balance: WAX(35.2) }]); // 88% of 40 WAX + }); + test('lowering the collection fee after listing discounts the executed sale', async () => { await listAndActivateSale([ASSET1], 1); await deposit('buyer', 1); @@ -779,6 +812,32 @@ describe('atomicmarket end to end', () => { expect(aaTables.assets('seller').length).toBe(2); // ASSET1 + ASSET2 }); + test('endrent is idempotent: a second call after the reclaim is a no-op', async () => { + await listAndActivateRental(); + + await deposit('renter', 1); + await atomicmarket.actions.rentasset([ + 'renter', ASSET1, 1, WAX(0.5), 0, '', + ]).send('renter@active'); + + // expire, then wrap up once: the first endrent reclaims (lease cleared, asset back to lister) + blockchain.addTime(TimePoint.fromMilliseconds(2 * 3600 * 1000)); + await atomicmarket.actions.endrent([ASSET1]).send('renter2@active'); + expect(aaTables.leases()).toEqual([]); + expect(marketTables.rentals().length).toBe(1); + + // a second endrent finds no lease to reclaim - it must no-op, not throw (a keeper reclaim + // followed by an endrent, or two endrent calls racing, must not brick). + await expect( + atomicmarket.actions.endrent([ASSET1]).send('renter2@active') + ).resolves.not.toThrow(); + + // state unchanged: still no lease, listing still present, asset still with the lister + expect(aaTables.leases()).toEqual([]); + expect(marketTables.rentals().length).toBe(1); + expect(aaTables.assets('seller').map((a) => a.asset_id)).toContain(ASSET1); + }); + test('renting after expiry without endrent re-leases from the old renter to the new one', async () => { await listAndActivateRental(); @@ -923,6 +982,28 @@ describe('atomicmarket end to end', () => { expect(aaTables.assets('seller').length).toBe(2); }); + test('an invalid rental listing (owner moved the asset) can be cancelled by anyone', async () => { + await listAndActivateRental(); // announce only - never rented, so no lease row + + // the lister moves the asset out of band; the listing is now invalid (owner no longer owns it) + await atomicassets.actions.transfer([ + 'seller', 'buyer', [ASSET1], 'moved after announcing', + ]).send('seller@active'); + + // a third party (not the owner) may cancel the invalid listing + await atomicmarket.actions.cancelrent([ASSET1]).send('renter2@active'); + expect(marketTables.rentals()).toEqual([]); + + // a still-valid listing, by contrast, needs the owner's authorization to cancel + await atomicassets.actions.transfer([ + 'buyer', 'seller', [ASSET1], 'return', + ]).send('buyer@active'); + await listAndActivateRental(); + await expect( + atomicmarket.actions.cancelrent([ASSET1]).send('renter2@active') + ).rejects.toThrow(/authorization of the owner is needed/); + }); + test('extensions are capped at the maximum duration measured from the lease start', async () => { // listAndActivateRental lists with maximum_rental_duration = 86400s (24h) await listAndActivateRental(); From 1e13bfd31bd8aeb126de193b26d0af75e6ca05cd Mon Sep 17 00:00:00 2001 From: Rob Konsdorf Date: Thu, 2 Jul 2026 12:21:21 -0400 Subject: [PATCH 14/15] docs(rentals): rewrite the wiki page for the non-custodial model The page still described the old custodial holder model, which the contract now rejects. Rewrite to renter-as-owner: leases is the source of truth, no rental-memo custody step, permissionless un-vetoable reclaim, idempotent endrent, and the current immutable rentals table shape. --- docs/wiki/Rentals.md | 119 ++++++++++++++++++++++--------------------- 1 file changed, 62 insertions(+), 57 deletions(-) diff --git a/docs/wiki/Rentals.md b/docs/wiki/Rentals.md index f30b247..2279ea3 100644 --- a/docs/wiki/Rentals.md +++ b/docs/wiki/Rentals.md @@ -1,19 +1,22 @@ # Rentals -AtomicMarket V2 supports **custodial per-hour rentals** built on the AtomicAssets V2 -holdership system: while an asset is rented, the renter is its **holder** (visible to games -and dApps through the AtomicAssets `holders` table) while the **ownership** stays with the -market contract, so the renter can never run away with the asset. +AtomicMarket supports **non-custodial per-hour rentals**. While an asset is rented, the renter +becomes the **real AtomicAssets owner** of it, so unmodified games, APIs and dApps grant the renter +the asset's utility with no special integration. The lister's right to get the asset back is parked +in the AtomicAssets `leases` table, the **single source of truth for the lock state**. The asset is +locked from transfer, burn and offer-out for the duration, and a permissionless `reclaim` force-returns +it to the lister at expiry. The asset is **never escrowed** in the market contract. ## Lifecycle ``` -announcerent ──> transfer (memo "rental") ──> rentasset ──> [extensions] ──> endrent ──┐ - │ (custody) (rented) (listed again)│ - └──────────────────────────── cancelrent (asset returned) <────────────────────────┘ +announcerent ──> rentasset ──> [extensions] ──> expiry ──> endrent / reclaim ──┐ + │ (renter = owner, locked) (returned, listed again)│ + └──────────────────────── cancelrent (when no active rental) <────────────────┘ ``` -1. **List** — the owner announces the listing with its terms: +1. **List** — the owner announces the listing with its terms. The asset stays in the owner's account; + announcing does not move or lock it. ```sh cleos push action atomicmarket announcerent '{ @@ -26,18 +29,15 @@ announcerent ──> transfer (memo "rental") ──> rentasset ──> [extensi }' -p alice@active ``` - - `price_per_hour` is denoted in the listing symbol. If it differs from - `settlement_symbol`, a delphi pair must be configured and the rental is paid in the - settlement symbol at the oracle rate at renting time (e.g. price in USD, paid in WAX). - - `maximum_rental_duration` (seconds) is the longest period one rental — including - extensions by the same renter — can cover. Minimum 3600 (one hour), maximum 28 days - (2,419,200 seconds). + - `price_per_hour` is denoted in the listing symbol. If it differs from `settlement_symbol`, a + delphi pair must be configured and the rental is paid in the settlement symbol at the oracle + rate at renting time (e.g. price in USD, paid in WAX). + - `maximum_rental_duration` (seconds) is the longest period one rental — including extensions by + the same renter, measured from the lease's original start — can cover. Minimum 3600 (one hour), + maximum 28 days (2,419,200 seconds). AtomicAssets enforces its own 28-day cap independently. + - Each asset is its own listing (one listing per asset id). -2. **Activate** — the owner transfers the asset to the market account with the memo - `rental`. The contract becomes the custodial owner. Each asset is its own listing - (one listing per asset id). - -3. **Rent** — a renter pays from their deposited balance: +2. **Rent** — a renter pays from their deposited balance: ```sh cleos push action atomicmarket rentasset '{ @@ -50,62 +50,67 @@ announcerent ──> transfer (memo "rental") ──> rentasset ──> [extensi }' -p bob@active ``` - - The total price (`price_per_hour × hours`, oracle-converted if applicable) is deducted - from the renter's balance and paid out like a sale: marketplace fees, the collection - fee (with [royalty splits](Royalty-Splits) and - [collection fee](V2-Changes)), remainder to the listing owner. - - The AtomicAssets holdership of the asset moves to the renter until `rental_end`. - - `expected_price_per_hour` protects the renter against listing changes between signing - and execution. - -4. **Extend** — the *current* renter can call `rentasset` again while their rental is - active; the purchased hours are appended to the current period. The combined remaining - period must stay within `maximum_rental_duration`. Rentals never renew automatically. - -5. **Wrap up** — after `rental_end`, **anyone** may call `endrent(asset_id)`. It moves the - holdership back to the market contract, making the listing rentable again. If nobody - calls it, the next `rentasset` moves holdership directly from the expired renter to the - new renter. - -6. **Cancel** — the owner reclaims the asset with `cancelrent(asset_id)` whenever no rental - is actively running (also handles expired-but-not-ended rentals by reclaiming holdership - first). A not-yet-activated listing whose owner no longer owns the asset is invalid and - may be cancelled by anyone. + - The total price (`price_per_hour × hours`, oracle-converted if applicable) is deducted from the + renter's balance and paid out like a sale: marketplace fees, the collection fee (with + [royalty splits](Royalty-Splits) and [collection fee](V2-Changes)), remainder to the listing owner. + - AtomicMarket drives AtomicAssets to make the renter the **real owner** of the asset until + `rental_end`, and the asset is locked (its lock state lives in the AtomicAssets `leases` row). + - `expected_price_per_hour` (and `intended_delphi_median` for oracle-priced listings) protect the + renter against listing or price changes between signing and execution. + +3. **Extend** — the *current* renter can call `rentasset` again while their rental is active; the + purchased hours are appended to the current period. The combined period, measured from the lease's + original start, must stay within `maximum_rental_duration`. Rentals never renew automatically. + +4. **Wrap up** — after `rental_end`, **anyone** may call `endrent(asset_id)`, which triggers the + permissionless AtomicAssets `reclaim`: it returns the asset to the lister and clears the lock, + making the listing rentable again. `endrent` is idempotent — a second call once the asset is + already reclaimed is a no-op. If nobody calls it, the next `rentasset` reclaims the expired lease + and re-leases to the new renter in the same transaction. + +5. **Cancel** — the owner removes the listing with `cancelrent(asset_id)` whenever no rental is + actively running. Because nothing is escrowed, cancelling is just removing the listing row; an + expired-but-unreclaimed rental is reclaimed to the owner as part of the cancel. A listing whose + owner no longer owns the asset is invalid and may be cancelled by anyone. ## Actions | Action | Auth | Effect | |---|---|---| -| `announcerent(lister, asset_id, price_per_hour, settlement_symbol, maximum_rental_duration, maker_marketplace)` | lister | Create a rental listing | -| `cancelrent(asset_id)` | owner (anyone if invalid) | Cancel the listing; return the asset if custodied | +| `announcerent(lister, asset_id, price_per_hour, settlement_symbol, maximum_rental_duration, maker_marketplace)` | lister | Create a rental listing (no escrow) | +| `cancelrent(asset_id)` | owner (anyone if invalid) | Remove the listing; reclaim first if expired-but-unreclaimed | | `rentasset(renter, asset_id, rental_hours, expected_price_per_hour, intended_delphi_median, taker_marketplace)` | renter | Rent or extend; pays from the renter's balance | -| `endrent(asset_id)` | anyone | Reset an expired rental back to its listed state | +| `endrent(asset_id)` | anyone | After expiry, reclaim the asset to the lister (idempotent) | | `payrentram(payer, asset_id)` | payer | Take over the RAM cost of the listing row | -Log actions: `lognewrent` (listing created), `logrentstart` (custody received, listing -active), `logrental` (rental executed: renter, hours, paid price, rental_end). +Market log actions: `lognewrent` (listing created), `logrental` (rental executed: renter, hours, +paid price, rental_end). The lock and return events are logged on the AtomicAssets side: `loglock` +(lease opened or extended) and `logreclaim` (asset returned). The reclaim path intentionally notifies +**no account that could veto it** (not the renter, not the title owner) so the guaranteed return +cannot be aborted — only the asset's collection is notified. ## The `rentals` table +The row is immutable listing config; the lock/renter/end state lives in the AtomicAssets `leases` +table, and the two join 1:1 on `asset_id`. + | Field | Meaning | |---|---| | `asset_id` | primary key — one listing per asset | | `owner` | the listing creator; receives the rental payouts | -| `holder` | the current renter; empty when not rented out | | `price_per_hour` | in the listing symbol | | `settlement_symbol` | what rentals are actually paid in | -| `maximum_rental_duration` | seconds; cap for a single rental incl. extensions | -| `rental_end` | seconds since epoch; 0 when not rented out (secondary index `rentalends`) | -| `asset_transferred` | true once the asset is in contract custody | -| `maker_marketplace`, `collection_name`, `collection_fee` | listing metadata | +| `maximum_rental_duration` | seconds; cap for a single rental incl. extensions, from the lease start | +| `maker_marketplace` | the marketplace that brokered the listing | +| `collection_name` | the asset's collection | +| `collection_fee` | the collection fee snapshot at listing time (the live fee is re-read at payout) | ## Integration notes for games and dApps -- To honor rentals, resolve an asset's *effective user* through the AtomicAssets `holders` - table: if a row exists for the asset, the `holder` is the active user; otherwise the - owner is. Rented assets have `owner = atomicmarket` and `holder = `. -- `rental_end` is tracked in the market's `rentals` table (with a secondary index by end - time for expiry sweeps). After expiry the holdership remains with the previous renter - until `endrent`, the next rental, or `cancelrent` resets it — treat - `rental_end <= now` as "rental over" regardless of the holders table. +- To honor rentals, treat the AtomicAssets **owner as the effective user**: during a lease the renter + *is* the owner, so no special resolution is needed for utility. The lock state (whether a returnable + lease is in force, and when it ends) is the AtomicAssets `leases` row for the asset. +- The `leases` row carries `title_owner` (the lister the asset returns to), `renter`, `rental_start` + and `rental_end`. Treat `rental_end <= now` as "rental over"; after expiry the renter remains the + owner until `endrent`, the next `rentasset`, or `cancelrent` triggers the reclaim. - Royalties apply to rentals exactly as to sales, including the royalty log actions. From 457584d2ac23d2d55d6918a72c8dd4434f85acb2 Mon Sep 17 00:00:00 2001 From: Rob Konsdorf Date: Thu, 2 Jul 2026 13:14:56 -0400 Subject: [PATCH 15/15] docs(rentals): document editrent, delist-vs-lease, rental_id linkage, keeper note --- docs/wiki/Rentals.md | 63 ++++++++++++++++++++++++++++---------------- 1 file changed, 41 insertions(+), 22 deletions(-) diff --git a/docs/wiki/Rentals.md b/docs/wiki/Rentals.md index 2279ea3..98bfb09 100644 --- a/docs/wiki/Rentals.md +++ b/docs/wiki/Rentals.md @@ -12,7 +12,8 @@ it to the lister at expiry. The asset is **never escrowed** in the market contra ``` announcerent ──> rentasset ──> [extensions] ──> expiry ──> endrent / reclaim ──┐ │ (renter = owner, locked) (returned, listed again)│ - └──────────────────────── cancelrent (when no active rental) <────────────────┘ + ├── editrent (reprice / re-bound, any time) │ + └── cancelrent (delist, any time; reclaim-if-expired) <────────────────────────┘ ``` 1. **List** — the owner announces the listing with its terms. The asset stays in the owner's account; @@ -60,39 +61,56 @@ announcerent ──> rentasset ──> [extensions] ──> expiry ──> endre 3. **Extend** — the *current* renter can call `rentasset` again while their rental is active; the purchased hours are appended to the current period. The combined period, measured from the lease's - original start, must stay within `maximum_rental_duration`. Rentals never renew automatically. + original start, must stay within `maximum_rental_duration`. An extension is a fresh purchase of + the listing's **current** terms (see `editrent`). Rentals never renew automatically. -4. **Wrap up** — after `rental_end`, **anyone** may call `endrent(asset_id)`, which triggers the +4. **Edit** — the owner can change the price, maximum duration and maker marketplace with + `editrent` at **any** time, including while a lease runs: the listing row is the owner's offer + of *future* rentals, separate from the renter's already-purchased lease (which is untouched). + The listing and settlement symbols cannot change — cancel and relist for that. Renters are + protected against repricing races by `expected_price_per_hour`. + +5. **Wrap up** — after `rental_end`, **anyone** may call `endrent(asset_id)`, which triggers the permissionless AtomicAssets `reclaim`: it returns the asset to the lister and clears the lock, making the listing rentable again. `endrent` is idempotent — a second call once the asset is - already reclaimed is a no-op. If nobody calls it, the next `rentasset` reclaims the expired lease - and re-leases to the new renter in the same transaction. - -5. **Cancel** — the owner removes the listing with `cancelrent(asset_id)` whenever no rental is - actively running. Because nothing is escrowed, cancelling is just removing the listing row; an - expired-but-unreclaimed rental is reclaimed to the owner as part of the cancel. A listing whose - owner no longer owns the asset is invalid and may be cancelled by anyone. + already reclaimed is a no-op — and purely lease-driven, so it works even for delisted leases. + If nobody calls it, the next `rentasset` reclaims the expired lease and re-leases to the new + renter in the same transaction. Until someone reclaims, the renter keeps the asset's utility + past `rental_end` for free — listers (or the platform's keeper cron) should call `endrent` + promptly. + +6. **Cancel** — the owner removes the listing with `cancelrent(asset_id)` at any time. Delisting is + **not** termination: during an active rental it only withdraws the offer of future rentals; the + lease runs to its end and is then reclaimed as usual. Because nothing is escrowed, cancelling is + just removing the listing row; an expired-but-unreclaimed rental is reclaimed to the owner as + part of the cancel. A listing whose owner no longer owns the asset (while unleased) is invalid + and may be cancelled by anyone. ## Actions | Action | Auth | Effect | |---|---|---| | `announcerent(lister, asset_id, price_per_hour, settlement_symbol, maximum_rental_duration, maker_marketplace)` | lister | Create a rental listing (no escrow) | -| `cancelrent(asset_id)` | owner (anyone if invalid) | Remove the listing; reclaim first if expired-but-unreclaimed | +| `editrent(asset_id, new_price_per_hour, new_maximum_rental_duration, new_maker_marketplace)` | owner | Change the offered terms, even mid-lease; symbols immutable | +| `cancelrent(asset_id)` | owner (anyone if invalid) | Delist (lease unaffected); reclaim first if expired-but-unreclaimed | | `rentasset(renter, asset_id, rental_hours, expected_price_per_hour, intended_delphi_median, taker_marketplace)` | renter | Rent or extend; pays from the renter's balance | -| `endrent(asset_id)` | anyone | After expiry, reclaim the asset to the lister (idempotent) | +| `endrent(asset_id)` | anyone | After expiry, reclaim the asset to the lister (idempotent, lease-driven) | | `payrentram(payer, asset_id)` | payer | Take over the RAM cost of the listing row | -Market log actions: `lognewrent` (listing created), `logrental` (rental executed: renter, hours, -paid price, rental_end). The lock and return events are logged on the AtomicAssets side: `loglock` -(lease opened or extended) and `logreclaim` (asset returned). The reclaim path intentionally notifies -**no account that could veto it** (not the renter, not the title owner) so the guaranteed return -cannot be aborted — only the asset's collection is notified. +Market log actions: `lognewrent` (listing created), `logeditrent` (terms changed), `logrental` +(rental executed: `rental_id`, renter, hours, paid price, `rental_start`, `rental_end`, +`is_extension`). The lock and return events are logged on the AtomicAssets side: `loglock` (lease +opened or extended — distinguishable via `rental_start`) and `logreclaim` (asset returned); both +echo the `rental_id` of the rental that opened the lease, so indexers can join them to the market's +rental without parsing memos. The reclaim path intentionally notifies **no account that could veto +it** (not the renter, not the title owner) so the guaranteed return cannot be aborted — only the +asset's collection is notified. (Corollary: a collection notify contract that throws on `loglock` +can selectively veto lease *creation* for its assets — a de facto per-collection opt-out.) ## The `rentals` table -The row is immutable listing config; the lock/renter/end state lives in the AtomicAssets `leases` -table, and the two join 1:1 on `asset_id`. +The row is the owner's offered terms (mutable via `editrent`); the lock/renter/end state lives in +the AtomicAssets `leases` table, and the two join 1:1 on `asset_id`. | Field | Meaning | |---|---| @@ -110,7 +128,8 @@ table, and the two join 1:1 on `asset_id`. - To honor rentals, treat the AtomicAssets **owner as the effective user**: during a lease the renter *is* the owner, so no special resolution is needed for utility. The lock state (whether a returnable lease is in force, and when it ends) is the AtomicAssets `leases` row for the asset. -- The `leases` row carries `title_owner` (the lister the asset returns to), `renter`, `rental_start` - and `rental_end`. Treat `rental_end <= now` as "rental over"; after expiry the renter remains the - owner until `endrent`, the next `rentasset`, or `cancelrent` triggers the reclaim. +- The `leases` row carries `title_owner` (the lister the asset returns to), `renter`, `rental_start`, + `rental_end` and the opening `rental_id`. Treat `rental_end <= now` as "rental over"; after expiry + the renter remains the owner until `endrent`, the next `rentasset`, or `cancelrent` triggers the + reclaim. - Royalties apply to rentals exactly as to sales, including the royalty log actions.