feat(connection): implement TrialOffer contract with subscription auth and cross-contract progress promotion#237
Merged
Petah1 merged 2 commits intoJun 20, 2026
Conversation
…h and cross-contract progress promotion - Full `log_trial_offer(scout, player_id, details_uri)` with idempotency guard, subscription/contact-fee auth check, persistent TrialOfferData storage keyed by TrialOfferKey(scout, player_id), cross-contract call to promote player to progress_level 3, and trial_offer_logged event emission - `get_connections(player_id)` and `get_trial_offers(scout)` read-only functions returning TrialOfferRecord vecs with deterministic insertion ordering - register: add `set_authorized_updater` (admin-only) and `update_progress_level` (authorized-updater-only) without touching existing functions or tests - subscription: implement subscription expiry storage, `is_subscribed` check, `has_paid_contact` flag, active `pay_to_contact` with u64 player_id - connection: add register/subscription as Cargo deps; 8 integration tests covering level-2-to-3 promotion, subscription auth, contact-fee auth, idempotency, connection/offer retrieval, and double-init guard
…avoid duplicate WASM symbols Importing register and subscription as [dependencies] caused rust-lld to fail with duplicate symbol errors (initialize, etc.) when linking the connection cdylib, because all three contracts export identically-named WASM entry points. Fix: move register and subscription to [dev-dependencies] only (used exclusively by tests), and replace RegisterContractClient / SubscriptionContractClient usage in production code with env.invoke_contract, which dispatches calls at the host level without merging WASM symbol tables.
6 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Closes #220 . Implements the
connectionSoroban smart contract module for recording trial offers between scouts and players, with full authorization, idempotency, progress promotion, and retrieval — without breaking any existing contract tests.Changes
contracts/connection/src/lib.rs— full implementation (was a stub)Storage types
TrialOfferData { details_uri: String, created_at: u64 }— the persisted recordTrialOfferRecord { scout, player_id, details_uri, created_at }— the return type for read queriesDataKeyvariants:TrialOfferKey(Address, u64),ScoutOffers(Address),PlayerConnections(u64),RegisterContract,SubscriptionContractinitialize(admin, register_contract, subscription_contract)log_trial_offer(scout, player_id: u64, details_uri)auth()TrialOfferKey(scout, player_id)already exists in storage, returnsOk(())immediately; no duplicate state, no re-emissionis_subscribed(scout)— passes if scout has an active subscriptionhas_paid_contact(scout, player_id)— passes if scout paid the per-player contact feeError::Unauthorizedif neither condition is metTrialOfferDataunderTrialOfferKey(scout, player_id)player_idtoScoutOffers(scout)listscouttoPlayerConnections(player_id)listregister.update_progress_level(player_id, 3)→ promotes player to Elite Tier[Symbol::new("trial_offer_logged"), scout, player_id]get_connections(player_id: u64) -> Vec<TrialOfferRecord>PlayerConnections(player_id)for the ordered scout listTrialOfferKeystorageget_trial_offers(scout: Address) -> Vec<TrialOfferRecord>ScoutOffers(scout)for the ordered player_id listcontracts/register/src/lib.rs— additive only, no existing tests brokenDataKey::AuthorizedUpdater— new storage keyset_authorized_updater(env, updater: Address)— admin-auth gate; registers an external contract (the connection contract) as the sole entity allowed to update player progress levelsupdate_progress_level(env, player_id: u64, level: u32)— requiresauthorized_updater.require_auth(); Soroban's cross-contract auth model satisfies this automatically when the connection contract is the caller in a live transaction;mock_all_auths()covers it in testscontracts/subscription/src/lib.rs— promoted from stubDataKey::Subscription(Address)→u32expiry ledger sequenceDataKey::ContactFee(Address, u64)→boolflagsubscribe— now storescurrent_sequence + duration_ledgersas the expiryis_subscribed— checkscurrent_sequence < expiry(was always-false stub)pay_to_contact— stores the contact fee flag;player_idtype changed fromAddresstou64to align with the register contract's numeric ID modelhas_paid_contact(scout, player_id: u64) -> bool— new; checks theContactFeekeycontracts/connection/Cargo.tomlregisterandsubscriptionas path dependencies (needed for cross-contract client types)soroban-sdkwithtestutilsfeature to[dev-dependencies]Test coverage (8 tests in
connection)log_trial_offer_with_subscription_sets_progress_to_3log_trial_offer_promotes_level_2_to_3unauthorized_scout_cannot_log_trial_offerUnauthorizedlog_trial_offer_with_contact_fee_succeedsduplicate_log_trial_offer_is_idempotentget_connections_returns_all_scouts_for_playerget_trial_offers_returns_all_offers_by_scoutdouble_initialize_failsinitializereturnsAlreadyInitializedAll 8 existing register contract tests continue to pass without modification.
Architecture notes
TrialOfferKeystorage lookup; theScoutOffersandPlayerConnectionsindex lists are only updated on first writeErrorvariants; cross-contract calls that fail trap the whole transaction (correct Soroban behaviour)update_progress_levelin the register contract enforcesauthorized_updater.require_auth()before mutating player state; the authorized updater is set explicitly by the admin viaset_authorized_updater#![no_std]throughout;Symbol::new(&env, "trial_offer_logged")used for the >9-char event topic (cannot usesymbol_short!); all storage via instance storage withbump_instanceon every state-changing callTest plan
cargo build --releaseandcargo test --target x86_64-unknown-linux-gnu