Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
540b51a
WIP: re-pin vendored libsession to 73e4d1e6 (Pro wire-format refactor)
jagerman Jul 19, 2026
2537441
WIP: nodejs glue — drop LIBSESSION_PRO_PROVIDERS metadata table
jagerman Jul 19, 2026
e516278
WIP: nodejs glue — rework Pro N-API surface to the redesigned C++ API
jagerman Jul 19, 2026
ecdb807
WIP: nodejs glue — re-pin to FINAL PIN 857783e6 (status strings + URL…
jagerman Jul 19, 2026
43ebbed
WIP: nodejs glue — Pro response parsers take RAW bytes, not a JSON st…
jagerman Jul 19, 2026
b75c132
nodejs glue: revocations — emit retryAtMs (absolute ms) + retainForMs…
jagerman Jul 20, 2026
4584fee
nodejs glue: re-pin to 418cdaba (*_at renames, ProRequest content_typ…
jagerman Jul 20, 2026
1a63d9b
pro glue: re-pin a9e790a8 + Delta #12 response envelope
jagerman Jul 20, 2026
1543c31
Pro: expose visible_platforms() (N-API); re-pin libsession c6e5a210; …
jagerman Jul 21, 2026
f9cf828
Pro glue: rename JS key genIndexHashB64 -> revocationTagB64 (match li…
jagerman Jul 22, 2026
2adde99
pro: normalise timestamp emission through a single toJsMs() helper
jagerman Jul 22, 2026
78f8f41
chore: lint
Bilb Jul 22, 2026
922a33d
pro: note that desktop persists GetProDetailsResponse (required-field…
jagerman Jul 22, 2026
08a1f1e
pro: re-pin to 3f8aace0, wire get_pro_status split, drop string8, pla…
jagerman Jul 23, 2026
b809e68
Drop stale Delta-N spec citations; reference current § sections
jagerman Jul 23, 2026
242e6af
Re-pin libsession-util to merged dev (d63deea5)
jagerman Jul 24, 2026
1ec3179
chore: lint
jagerman Jul 27, 2026
6babc6d
Bump libsession-util pin d63deea5 -> 7e4d7641 (revocation retry/retai…
jagerman Jul 28, 2026
ee2cf6f
fix: rely on revocation retry_in from libsession
Bilb Jul 27, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
371 changes: 285 additions & 86 deletions include/pro/pro.hpp

Large diffs are not rendered by default.

6 changes: 4 additions & 2 deletions include/pro/types.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,11 @@ struct toJs_impl<session::ProProof> {
auto obj = Napi::Object::New(env);

obj["version"] = toJs(env, pro_proof.version);
obj["genIndexHashB64"] = toJs(env, oxenc::to_base64(pro_proof.gen_index_hash));
// `revocation_tag` (renamed from `revocation_tag`); kept the JS key `revocationTagB64` to
// avoid churning every desktop consumer.
obj["revocationTagB64"] = toJs(env, oxenc::to_base64(pro_proof.revocation_tag));
obj["rotatingPubkeyHex"] = toJs(env, oxenc::to_hex(pro_proof.rotating_pubkey));
obj["expiryMs"] = toJs(env, pro_proof.expiry_unix_ts.time_since_epoch().count());
obj["expiryMs"] = toJsMs(env, pro_proof.expiry_at);
obj["signatureHex"] = toJs(env, oxenc::to_hex(pro_proof.sig));

return obj;
Expand Down
21 changes: 14 additions & 7 deletions include/utilities.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
#include "session/session_protocol.hpp"
#include "session/types.h"
#include "session/types.hpp"
#include "session/util.hpp"

namespace session::nodeapi {

Expand Down Expand Up @@ -162,13 +163,6 @@ struct toJs_impl<T, std::enable_if_t<std::is_convertible_v<T, std::string_view>>
}
};

template <>
struct toJs_impl<string8> {
auto operator()(const Napi::Env& env, string8 s) const {
return Napi::String::New(env, s.data, s.size);
}
};

template <typename T>
struct toJs_impl<
T,
Expand Down Expand Up @@ -275,6 +269,19 @@ struct toJs_impl<std::chrono::sys_time<std::chrono::milliseconds>> {
}
};

// Normalise any system-clock time_point or duration to the JS domain as epoch-milliseconds. The
// wire mixes whole-second (`sys_seconds` / `std::chrono::seconds`) and millisecond-resolution
// (`sys_ms`) values; both widen implicitly and losslessly to the ms parameter types below, so
// callers never hand-multiply by 1000 (and a finer-than-ms source fails to compile rather than
// silently truncating). Prefer these over passing a `sys_seconds` straight to `toJs`, whose
// specialisation emits *seconds* and is an easy way to land the wrong unit in a `...Ms` field.
inline auto toJsMs(const Napi::Env& env, session::sys_ms tp) {
return toJs(env, tp);
}
inline auto toJsMs(const Napi::Env& env, std::chrono::milliseconds d) {
return toJs(env, d);
}

// Returns {"url": "...", "key": buffer} object; both values will be Null if the pic is not set.

template <>
Expand Down
2 changes: 1 addition & 1 deletion libsession-util
Submodule libsession-util updated 49 files
+74 −15 .drone.jsonnet
+6 −2 CMakeLists.txt
+7 −2 external/CMakeLists.txt
+2 −2 include/session/attachments.hpp
+10 −8 include/session/config/convo_info_volatile.h
+7 −7 include/session/config/convo_info_volatile.hpp
+1 −1 include/session/config/pro.hpp
+9 −9 include/session/config/user_profile.h
+7 −8 include/session/config/user_profile.hpp
+12 −13 include/session/network/network_config.hpp
+97 −37 include/session/network/network_opt.hpp
+1 −2 include/session/network/session_network.hpp
+332 −501 include/session/pro_backend.h
+355 −517 include/session/pro_backend.hpp
+6 −3 include/session/random.hpp
+118 −159 include/session/session_protocol.h
+52 −24 include/session/session_protocol.hpp
+0 −22 include/session/types.h
+0 −5 include/session/types.hpp
+8 −3 include/session/util.hpp
+21 −21 proto/SessionProtos.pb.cc
+47 −47 proto/SessionProtos.pb.h
+1 −1 proto/SessionProtos.proto
+26 −14 src/attachments.cpp
+42 −45 src/config/convo_info_volatile.cpp
+8 −9 src/config/pro.cpp
+22 −27 src/config/user_profile.cpp
+10 −2 src/network/backends/session_file_server.cpp
+25 −8 src/network/backends/session_open_group_server.cpp
+4 −58 src/network/network_config.cpp
+22 −15 src/network/routing/onion_request_router.cpp
+45 −18 src/network/session_network.cpp
+1,030 −1,447 src/pro_backend.cpp
+60 −0 src/pro_message.hpp
+125 −198 src/session_protocol.cpp
+0 −38 src/types.cpp
+77 −0 tests/pro_backend/gen_kat.py
+109 −0 tests/pro_backend/run-dev-backend.sh
+165 −0 tests/pro_backend/seed_payment.py
+99 −0 tests/test_attachment_encrypt.cpp
+18 −0 tests/test_backed_session_open_group_server.cpp
+5 −0 tests/test_backend_session_file_server.cpp
+11 −10 tests/test_config_convo_info_volatile.cpp
+25 −30 tests/test_config_pro.cpp
+9 −10 tests/test_config_userprofile.cpp
+1,041 −1,040 tests/test_pro_backend.cpp
+20 −0 tests/test_random.cpp
+51 −52 tests/test_session_protocol.cpp
+56 −0 utils/test-bigendian.sh
61 changes: 22 additions & 39 deletions src/constants.cpp
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
#include "constants.hpp"

#include <oxenc/hex.h>

#include "js_native_api_types.h"
#include "session/config/contacts.hpp"
#include "session/config/groups/info.hpp"
#include "session/config/user_groups.hpp"
#include "session/pro_backend.h"
#include "session/pro_backend.hpp"
#include "session/session_protocol.h"
#include "session/version.h"
#include "utilities.hpp"
Expand All @@ -24,43 +26,10 @@ Napi::Object ConstantsWrapper::Init(Napi::Env env, Napi::Object exports) {
pro_urls["pro_access_not_found"] = toJs(env, SESSION_PROTOCOL_STRINGS.url_pro_access_not_found);
pro_urls["support_url"] = toJs(env, SESSION_PROTOCOL_STRINGS.url_pro_support);

auto make_provider = [&](int provider, int other_provider) {
const auto& meta = SESSION_PRO_BACKEND_PAYMENT_PROVIDER_METADATA[provider];
auto obj = Napi::Object::New(env);
obj["device"] = toJs(env, meta.device);
obj["store"] = toJs(env, meta.store);
obj["platform"] = toJs(env, meta.platform);
obj["platform_account"] = toJs(env, meta.platform_account);
obj["refund_support_url"] = toJs(env, meta.refund_support_url);
obj["refund_status_url"] = toJs(env, meta.refund_status_url);
obj["refund_platform_url"] = toJs(env, meta.refund_platform_url);
obj["update_subscription_url"] = toJs(env, meta.update_subscription_url);
obj["cancel_subscription_url"] = toJs(env, meta.cancel_subscription_url);
obj["store_other"] =
toJs(env, SESSION_PRO_BACKEND_PAYMENT_PROVIDER_METADATA[other_provider].store);
return obj;
};

auto pro_provider_nil = make_provider(
SESSION_PRO_BACKEND_PAYMENT_PROVIDER_NIL, SESSION_PRO_BACKEND_PAYMENT_PROVIDER_NIL);
auto pro_provider_google = make_provider(
SESSION_PRO_BACKEND_PAYMENT_PROVIDER_GOOGLE_PLAY_STORE,
SESSION_PRO_BACKEND_PAYMENT_PROVIDER_IOS_APP_STORE);
auto pro_provider_ios = make_provider(
SESSION_PRO_BACKEND_PAYMENT_PROVIDER_IOS_APP_STORE,
SESSION_PRO_BACKEND_PAYMENT_PROVIDER_GOOGLE_PLAY_STORE);

auto pro_provider_rangeproof = make_provider(
SESSION_PRO_BACKEND_PAYMENT_PROVIDER_RANGEPROOF,
// Use NIL as the second provider for Rangeproof so that it does not define an alternate
// store label (i.e., no explicit "other" store for Rangeproof in these constants).
SESSION_PRO_BACKEND_PAYMENT_PROVIDER_NIL);

auto pro_providers = Napi::Object::New(env);
pro_providers["Nil"] = toJs(env, pro_provider_nil);
pro_providers["Google"] = toJs(env, pro_provider_google);
pro_providers["iOS"] = toJs(env, pro_provider_ios);
pro_providers["Rangeproof"] = toJs(env, pro_provider_rangeproof);
// Provider display metadata (store/platform/account NAMES) is no longer shipped by libsession —
// those are translation data owned by each client (keyed on the provider slug). The
// per-provider support/management URLs are still libsession-owned but are now fetched on demand
// via ProWrapper.providerUrls(code) rather than baked into a constants table here.

// construct javascript constants object
Napi::Function cls = DefineClass(
Expand All @@ -87,7 +56,21 @@ Napi::Object ConstantsWrapper::Init(Napi::Env env, Napi::Object exports) {
Napi::Number::New(env, session::config::community::FULL_URL_MAX_LENGTH),
napi_enumerable),
ObjectWrap::StaticValue("LIBSESSION_PRO_URLS", pro_urls, napi_enumerable),
ObjectWrap::StaticValue("LIBSESSION_PRO_PROVIDERS", pro_providers, napi_enumerable),
// Session Pro backend identity — the single source of truth clients read instead of
// hand-carrying their own copies (URL is the overridable prod/default; pubkey is hex).
ObjectWrap::StaticValue(
"LIBSESSION_PRO_BACKEND_URL",
Napi::String::New(env, std::string(session::pro_backend::URL)),
napi_enumerable),
ObjectWrap::StaticValue(
"LIBSESSION_PRO_BACKEND_PUBKEY_HEX",
Napi::String::New(env, oxenc::to_hex(session::pro_backend::PUBKEY)),
napi_enumerable),
// X25519 form of the backend pubkey (for onion routing), so clients needn't derive it
ObjectWrap::StaticValue(
"LIBSESSION_PRO_BACKEND_PUBKEY_X25519_HEX",
Napi::String::New(env, oxenc::to_hex(session::pro_backend::PUBKEY_X25519)),
napi_enumerable),
ObjectWrap::StaticValue(
"LIBSESSION_UTIL_VERSION",
Napi::String::New(env, LIBSESSION_UTIL_VERSION_FULL),
Expand Down
36 changes: 20 additions & 16 deletions src/convo_info_volatile_config.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -58,13 +58,15 @@ struct toJs_impl<convo::one_to_one> {
obj["pubkeyHex"] = toJs(env, info_1o1.session_id);
addBaseValues(env, obj, info_1o1);

if (info_1o1.pro_gen_index_hash->empty() ||
!info_1o1.pro_expiry_unix_ts.time_since_epoch().count()) {
obj["proGenIndexHashB64"] = env.Null();
if (info_1o1.pro_revocation_tag->empty() ||
!info_1o1.pro_expiry_at.time_since_epoch().count()) {
obj["proRevocationTagB64"] = env.Null();
obj["proExpiryTsMs"] = env.Null();
} else {
obj["proGenIndexHashB64"] = toJs(env, to_base64(*info_1o1.pro_gen_index_hash));
obj["proExpiryTsMs"] = toJs(env, info_1o1.pro_expiry_unix_ts);
obj["proRevocationTagB64"] = toJs(env, to_base64(*info_1o1.pro_revocation_tag));
// config field is now whole seconds; the JS `proExpiryTsMs` key stays milliseconds
obj["proExpiryTsMs"] =
toJs(env, info_1o1.pro_expiry_at.time_since_epoch().count() * 1000);
}

return obj;
Expand Down Expand Up @@ -176,10 +178,10 @@ void ConvoInfoVolatileWrapper::set1o1(const Napi::CallbackInfo& info) {
convo.unread = parsed.forcedUnread;

// 1o1 also have a pro gen index hash & pro expiry
auto proGenIndexHashB64Js = parsed.obj.Get("proGenIndexHashB64");
assertIsStringOrNull(proGenIndexHashB64Js, fnName + "proGenIndexHashB64Js");
auto proGenIndexHashB64Cpp =
maybeNonemptyString(proGenIndexHashB64Js, fnName + "proGenIndexHashB64Cpp");
auto proRevocationTagB64Js = parsed.obj.Get("proRevocationTagB64");
assertIsStringOrNull(proRevocationTagB64Js, fnName + "proRevocationTagB64Js");
auto proRevocationTagB64Cpp =
maybeNonemptyString(proRevocationTagB64Js, fnName + "proRevocationTagB64Cpp");

auto proExpiryUnixTsMsJs = parsed.obj.Get("proExpiryTsMs");
assertIsNumberOrNull(proExpiryUnixTsMsJs, fnName + "proExpiryUnixTsMsJs");
Expand All @@ -188,19 +190,21 @@ void ConvoInfoVolatileWrapper::set1o1(const Napi::CallbackInfo& info) {
// Note: null is used to ignore an update. i.e. if the field is unset, we do not want to
// overwrite the current value.
// To reset it, set it to empty string
if (proGenIndexHashB64Cpp.has_value()) {
if (proGenIndexHashB64Cpp->empty()) {
if (proRevocationTagB64Cpp.has_value()) {
if (proRevocationTagB64Cpp->empty()) {
// if the first is set, but empty, we want to reset the field
convo.pro_gen_index_hash = std::nullopt;
convo.pro_revocation_tag = std::nullopt;
} else {
// this throws if the size is wrong
convo.pro_gen_index_hash = from_base64_to_array<32>(*proGenIndexHashB64Cpp);
convo.pro_revocation_tag = from_base64_to_array<32>(*proRevocationTagB64Cpp);
}
}
if (proExpiryUnixTsMsCpp.has_value()) {
// if the field is set (not null), we want to write the change as is
convo.pro_expiry_unix_ts = std::chrono::sys_time<std::chrono::milliseconds>(
std::chrono::milliseconds(*proExpiryUnixTsMsCpp));
// if the field is set (not null), we want to write the change as is (ms -> whole
// seconds)
convo.pro_expiry_at = std::chrono::floor<std::chrono::seconds>(
std::chrono::sys_time<std::chrono::milliseconds>(
std::chrono::milliseconds(*proExpiryUnixTsMsCpp)));
}

config.set(convo);
Expand Down
4 changes: 3 additions & 1 deletion src/encrypt_decrypt/encrypt_decrypt.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -637,7 +637,9 @@ Napi::Value MultiEncryptWrapper::decryptForCommunity(const Napi::CallbackInfo& i
auto contentOrEnvelope =
extractContentOrEnvelope(obj, "decryptForCommunity.obj.contentOrEnvelope");
decrypted.push_back(session::decode_for_community(
contentOrEnvelope, nowMs, proBackendPubkeyHex));
contentOrEnvelope,
std::chrono::floor<std::chrono::seconds>(nowMs),
proBackendPubkeyHex));
decryptedServerIds.push_back(serverId);

} catch (const std::exception& e) {
Expand Down
100 changes: 4 additions & 96 deletions src/pro/pro.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,102 +2,10 @@

namespace session::nodeapi {

std::string_view ProBackendEnumToString(SESSION_PRO_BACKEND_PAYMENT_PROVIDER v) {
switch (v) {
case SESSION_PRO_BACKEND_PAYMENT_PROVIDER_NIL: return "NIL";
case SESSION_PRO_BACKEND_PAYMENT_PROVIDER_GOOGLE_PLAY_STORE: return "GOOGLE_PLAY_STORE";
case SESSION_PRO_BACKEND_PAYMENT_PROVIDER_IOS_APP_STORE: return "IOS_APP_STORE";
case SESSION_PRO_BACKEND_PAYMENT_PROVIDER_RANGEPROOF: return "RANGEPROOF";
case SESSION_PRO_BACKEND_PAYMENT_PROVIDER_COUNT:
throw std::invalid_argument("SESSION_PRO_BACKEND_PAYMENT_PROVIDER_COUNT");
}
UNREACHABLE();
}

std::string_view ProBackendEnumToString(SESSION_PRO_BACKEND_PAYMENT_STATUS v) {
switch (v) {
case SESSION_PRO_BACKEND_PAYMENT_STATUS_NIL: return "NIL";
case SESSION_PRO_BACKEND_PAYMENT_STATUS_UNREDEEMED: return "UNREDEEMED";
case SESSION_PRO_BACKEND_PAYMENT_STATUS_REDEEMED: return "REDEEMED";
case SESSION_PRO_BACKEND_PAYMENT_STATUS_EXPIRED: return "EXPIRED";
case SESSION_PRO_BACKEND_PAYMENT_STATUS_REFUNDED: return "REFUNDED";
case SESSION_PRO_BACKEND_PAYMENT_STATUS_COUNT:
throw std::invalid_argument("SESSION_PRO_BACKEND_PAYMENT_STATUS_COUNT");
}
UNREACHABLE();
}

std::string_view proBackendEnumToString(SESSION_PRO_BACKEND_PLAN v) {
switch (v) {
case SESSION_PRO_BACKEND_PLAN_NIL: return "NIL";
case SESSION_PRO_BACKEND_PLAN_ONE_MONTH: return "ONE_MONTH";
case SESSION_PRO_BACKEND_PLAN_THREE_MONTHS: return "THREE_MONTHS";
case SESSION_PRO_BACKEND_PLAN_TWELVE_MONTHS: return "TWELVE_MONTHS";
case SESSION_PRO_BACKEND_PLAN_COUNT:
throw std::invalid_argument("SESSION_PRO_BACKEND_PLAN_COUNT");
}
UNREACHABLE();
}

std::string_view proBackendEnumToString(SESSION_PRO_BACKEND_USER_PRO_STATUS v) {
switch (v) {
case SESSION_PRO_BACKEND_USER_PRO_STATUS_NEVER_BEEN_PRO: return "NEVER_BEEN_PRO";
case SESSION_PRO_BACKEND_USER_PRO_STATUS_ACTIVE: return "ACTIVE";
case SESSION_PRO_BACKEND_USER_PRO_STATUS_EXPIRED: return "EXPIRED";
case SESSION_PRO_BACKEND_USER_PRO_STATUS_COUNT:
throw std::invalid_argument("SESSION_PRO_BACKEND_USER_PRO_STATUS_COUNT");
}
UNREACHABLE();
}

std::string_view ProBackendEnumToString(SESSION_PRO_BACKEND_GET_PRO_DETAILS_ERROR_REPORT v) {
switch (v) {
case SESSION_PRO_BACKEND_GET_PRO_DETAILS_ERROR_REPORT_SUCCESS: return "SUCCESS";
case SESSION_PRO_BACKEND_GET_PRO_DETAILS_ERROR_REPORT_GENERIC_ERROR: return "GENERIC_ERROR";
case SESSION_PRO_BACKEND_GET_PRO_DETAILS_ERROR_REPORT_COUNT:
throw std::invalid_argument("SESSION_PRO_BACKEND_GET_PRO_DETAILS_ERROR_REPORT");
}
UNREACHABLE();
}

std::string_view proBackendEnumPlanToString(SESSION_PRO_BACKEND_PLAN v) {
switch (v) {
case SESSION_PRO_BACKEND_PLAN_NIL: return "NIL";
case SESSION_PRO_BACKEND_PLAN_ONE_MONTH: return "ONE_MONTH";
case SESSION_PRO_BACKEND_PLAN_THREE_MONTHS: return "THREE_MONTHS";
case SESSION_PRO_BACKEND_PLAN_TWELVE_MONTHS: return "TWELVE_MONTHS";
case SESSION_PRO_BACKEND_PLAN_COUNT:
throw std::invalid_argument("SESSION_PRO_BACKEND_PLAN_COUNT");
}
UNREACHABLE();
}

std::string_view proBackendEnumPaymentProviderToString(SESSION_PRO_BACKEND_PAYMENT_PROVIDER v) {
switch (v) {
// Note: we want those to map ProOriginatingPlatform keys
case SESSION_PRO_BACKEND_PAYMENT_PROVIDER_NIL: return "Nil";
case SESSION_PRO_BACKEND_PAYMENT_PROVIDER_GOOGLE_PLAY_STORE: return "GooglePlayStore";
case SESSION_PRO_BACKEND_PAYMENT_PROVIDER_IOS_APP_STORE: return "iOSAppStore";
case SESSION_PRO_BACKEND_PAYMENT_PROVIDER_RANGEPROOF: return "Rangeproof";
case SESSION_PRO_BACKEND_PAYMENT_PROVIDER_COUNT:
throw std::invalid_argument("SESSION_PRO_BACKEND_PAYMENT_PROVIDER_COUNT");
}
UNREACHABLE();
}

std::string_view proBackendEnumPaymentStatusToString(SESSION_PRO_BACKEND_PAYMENT_STATUS v) {
switch (v) {
case SESSION_PRO_BACKEND_PAYMENT_STATUS_NIL: return "NIL";
case SESSION_PRO_BACKEND_PAYMENT_STATUS_UNREDEEMED: return "UNREDEEMED";
case SESSION_PRO_BACKEND_PAYMENT_STATUS_REDEEMED: return "REDEEMED";
case SESSION_PRO_BACKEND_PAYMENT_STATUS_EXPIRED: return "EXPIRED";
case SESSION_PRO_BACKEND_PAYMENT_STATUS_REFUNDED: return "REFUNDED";
case SESSION_PRO_BACKEND_PAYMENT_STATUS_COUNT:
throw std::invalid_argument("SESSION_PRO_BACKEND_PAYMENT_STATUS_COUNT");
}
UNREACHABLE();
}

// Provider, plan and payment-status are no longer fixed enums on the wire: provider/plan are opaque
// strings (emitted verbatim) and the response structs expose payment/user status as small enums
// that we surface to JS as their raw integer values (they line up 1:1 with the desktop numeric
// enums), so the only enum->string mapping still needed is for the pro-features result status.
std::string_view proBackendEnumToString(session::ProFeaturesForMsgStatus v) {
switch (v) {
case session::ProFeaturesForMsgStatus::Success: return "SUCCESS";
Expand Down
Loading
Loading