Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Empty file added multi_wallet_session/Cargo.toml
Empty file.
18 changes: 18 additions & 0 deletions multi_wallet_session/src/data_types.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
#![no_std]
use soroban_sdk::{contracttype, Address, Symbol};

#[contracttype]
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum DataKey {
ActiveSession(Address), // Maps a user profile to their current active signing wallet
SessionNonce(Address), // Tracks transaction replay prevention nonces per wallet
IsFlowLocked(Address), // Mid-flow guard: locks a wallet if an action is incomplete
}

#[contracttype]
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct SessionProfile {
pub wallet_address: Address,
pub network_passphrase: Symbol, // Re-verifies network safety (e.g., TESTNET vs PUBLIC)
pub last_active_ledger: u32,
}
77 changes: 77 additions & 0 deletions multi_wallet_session/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
#![no_std]
use soroban_sdk::{contractimpl, log, Address, Env, Symbol};

mod data_types;
use crate::data_types::{DataKey, SessionProfile};

pub struct MultiWalletSessionContract;

#[contractimpl]
impl MultiWalletSessionContract {
/// Connects or switches to a new wallet address for an active on-chain session state wrapper.
pub fn switch_wallet(
env: Env,
user: Address,
new_wallet: Address,
network: Symbol
) {
// Strict Task Validation: Ensure the transaction call is authentically signed by the target wallet
new_wallet.require_auth();

// 1. Mid-flow Guard Check: If the previous wallet was locked mid-transaction (e.g., incomplete multi-sig split escrow)
if env.storage().persistent().has(&DataKey::IsFlowLocked(user.clone())) {
let is_locked: bool = env.storage().persistent().get(&DataKey::IsFlowLocked(user.clone())).unwrap();
if is_locked {
log!(&env, "CRITICAL ERROR: Cannot switch wallets. Active transaction pipeline is locked mid-flow.");
panic!("Session is currently pipeline-locked. Complete or revert active flows first.");
}
}

// 2. Clear previous wallet-specific states (State Isolation Cleanup)
if env.storage().persistent().has(&DataKey::ActiveSession(user.clone())) {
env.storage().persistent().remove(&DataKey::ActiveSession(user.clone()));
log!(&env, "Previous wallet session purged successfully.");
}

// 3. Network Re-verification: Enforce network boundary rules
let expected_network = Symbol::new(&env, "testnet");
if network != expected_network {
panic!("Network verification handshake failed. Target network passphrase misaligned.");
}

// 4. Save the new isolated structural session mapping data profile
let session = SessionProfile {
wallet_address: new_wallet.clone(),
network_passphrase: network,
last_active_ledger: env.ledger().sequence(),
};

env.storage().persistent().set(&DataKey::ActiveSession(user), &session);
log!(&env, "Successfully switched session execution context to new wallet address.");
}

/// Explicitly locks or unlocks a session state during high-risk multi-stage pipelines (e.g., donation pool splits)
pub fn set_flow_lock(env: Env, user: Address, wallet: Address, lock_state: bool) {
wallet.require_auth();

// Assert that the request vector aligns perfectly with the logged session data instance
if let Some(session) = env.storage().persistent().get::<_, SessionProfile>(&DataKey::ActiveSession(user.clone())) {
if session.wallet_address != wallet {
panic!("Mismatched executing wallet validation parameters.");
}
} else {
panic!("No active wallet connection session found for user.");
}

env.storage().persistent().set(&DataKey::IsFlowLocked(user), &lock_state);
}

/// Clear all active sessions explicitly (Disconnect Workflow)
pub fn disconnect_wallet(env: Env, user: Address, wallet: Address) {
wallet.require_auth();

env.storage().persistent().remove(&DataKey::ActiveSession(user.clone()));
env.storage().persistent().remove(&DataKey::IsFlowLocked(user));
log!(&env, "Session dropped cleanly. All volatile storage flags cleared.");
}
}
6 changes: 6 additions & 0 deletions sdk/docs/wallet/SEP7.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# SEP-0007 Integration Framework Specification

This project fully supports the **SEP-0007** ecosystem configuration matrix, allowing seamless, friction-free donation mechanics using external decentralized browser modules or companion hardware nodes.

## Deep-Link URI Protocol Schema
All payment operations generate a standard deep-link layout following the structured criteria below:
33 changes: 33 additions & 0 deletions sdk/utils/__tests__/sep7.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { buildDonationPaymentLink } from '../sep7';

describe('Issue #373: SEP-0007 Payment Link Serialization Tests', () => {
const mockParams = {
destination: 'GBALBEDO76V6K67X4PZ72NC556XU54K75QNZP2Z5F4O7V6Y456QWERTY',
amount: '150.5000',
memo: 'CAMPAIGN_99',
campaignId: 'c10293',
};

it('should successfully output a properly prefixed web+stellar:pay protocol link', () => {
const result = buildDonationPaymentLink(mockParams);
expect(result).toBeDefined();
expect(result.startsWith('web+stellar:pay?')).toBe(true);
});

it('should explicitly URI-encode query tracking text metrics and match key specifications', () => {
const result = buildDonationPaymentLink(mockParams);
const urlParams = new URLSearchParams(result.split('?')[1]);

expect(urlParams.get('destination')).toBe(mockParams.destination);
expect(urlParams.get('amount')).toBe(mockParams.amount);
expect(urlParams.get('memo')).toBe(mockParams.memo);
expect(urlParams.get('memo_type')).toBe('MEMO_TEXT');
expect(urlParams.get('msg')).toContain('c10293');
});

it('should drop execution routines if the destination string breaks structural integrity rules', () => {
expect(() => {
buildDonationPaymentLink({ ...mockParams, destination: 'INVALID_STELLAR_ADDRESS' });
}).toThrow();
});
});
44 changes: 44 additions & 0 deletions sdk/utils/sep7.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/**
* Utility library for constructing standardized Stellar SEP-0007 payment links.
* Reference Specification: https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0007.md
*/

interface DonationLinkParams {
destination: string;
amount: string;
memo: string;
campaignId: string;
}

/**
* Builds a valid, URI-encoded web+stellar:pay link matching SEP-0007 specifications.
* * @param destination Valid Stellar G... public address receiving the funds
* @param amount String representation of asset amount to prevent floating-point precision loss
* @param memo Explanatory text string encoded as a MEMO_TEXT type parameter
* @param campaignId Custom tracking identifier embedded alongside operational paths
* @returns Fully qualified web+stellar:pay deep-link scheme string
*/
export function buildDonationPaymentLink({
destination,
amount,
memo,
campaignId,
}: DonationLinkParams): string {
// Validate basic address structural formats
if (!destination.startsWith('G') || destination.length !== 56) {
throw new Error('Invalid Stellar destination public key formatting configuration.');
}

const baseUrl = 'web+stellar:pay';

const queryParams = new URLSearchParams({
destination: destination.trim(),
amount: amount.trim(),
memo: memo.trim(),
memo_type: 'MEMO_TEXT',
msg: `Donation for Campaign ID: ${campaignId}`.trim(),
});

// Returns the formatted deep link protocol string
return `${baseUrl}?${queryParams.toString()}`;
}
Empty file added wallet_sign_timeout/Cargo.toml
Empty file.
15 changes: 15 additions & 0 deletions wallet_sign_timeout/src/data_types.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
#![no_std]
use soroban_sdk::{contracttype, Address};

#[contracttype]
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum DataKey {
PendingExecution(Address), // Tracks active execution states to prevent orphaned data pools
}

#[contracttype]
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct DelayedState {
pub value: u32,
pub initiator: Address,
}
58 changes: 58 additions & 0 deletions wallet_sign_timeout/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
#![no_std]
use soroban_sdk::{contractimpl, log, Address, Env};

mod data_types;
use crate::data_types::{DataKey, DelayedState};

pub struct WalletTimeoutContract;

#[contractimpl]
impl WalletTimeoutContract {
/// Pre-authorizes an intent ledger checkpoint.
/// If the matching signature execution block fails to materialize inside the deadline, it allows retries.
pub fn initiate_action(env: Env, user: Address) {
user.require_auth();

// Clear any previous dangling state if it exists, allowing clean retry loops
if env.storage().temporary().has(&DataKey::PendingExecution(user.clone())) {
env.storage().temporary().remove(&DataKey::PendingExecution(user.clone()));
}

let state = DelayedState {
value: 1, // Represents baseline transaction configuration metadata
initiator: user.clone(),
};

// Use temporary storage so expired, un-executed inputs naturally decay from the ledger state
env.storage().temporary().set(&DataKey::PendingExecution(user), &state);
log!(&env, "Action state recorded. Awaiting final signature verification.");
}

/// Completes the execution loop, guarded strictly by a maximum ledger sequence threshold.
/// In Soroban, a 60-second window roughly equates to an offset of 12 ledger blocks (~5s per ledger closing).
pub fn execute_with_timeout(
env: Env,
user: Address,
max_ledger_sequence: u32
) {
user.require_auth();

// 1. Evaluate Timeout Condition: Check if current ledger height exceeds the client-side signature boundary
let current_ledger = env.ledger().sequence();
if current_ledger > max_ledger_sequence {
// Revert state change. This prevents orphaned pending structures from freezing the user flow.
env.storage().temporary().remove(&DataKey::PendingExecution(user.clone()));
log!(&env, "CRITICAL: Wallet signature collection window exceeded standard timeout limits.");
panic!("Signing timed out. Transaction window has expired. Please retry the operation.");
}

// 2. Fetch and confirm matching state primitives exist
if !env.storage().temporary().has(&DataKey::PendingExecution(user.clone())) {
panic!("No pending action matches this execution parameter block or state was dropped.");
}

// 3. Complete the execution pipeline safely
env.storage().temporary().remove(&DataKey::PendingExecution(user));
log!(&env, "Signature validated within deadline constraints. Pipeline executed successfully.");
}
}