Skip to content

feat: multi-chain sign-in — Substrate + Ethereum + Solana#84

Merged
sacha-l merged 6 commits into
developfrom
feat/multichain-signin-phase-a
May 19, 2026
Merged

feat: multi-chain sign-in — Substrate + Ethereum + Solana#84
sacha-l merged 6 commits into
developfrom
feat/multichain-signin-phase-a

Conversation

@sacha-l
Copy link
Copy Markdown
Collaborator

@sacha-l sacha-l commented May 19, 2026

Summary

Adds multi-chain sign-in to Stadium. Anyone — admins and team members — can authenticate with the wallet ecosystem they use: Substrate (SIWS), Ethereum (SIWE / EIP-4361), or Solana (Sign-In With Solana). Team-member and payout addresses become multi-chain too.

Backward-compatible throughout: the x-siws-auth payload gains an optional chain field defaulting to substrate, and every new DB column / address field defaults to substrate, so existing flows are unchanged.

Shipped in four phases (one logical commit group each):

  • Phase A — server verifier abstraction (server/api/auth/): per-chain verifier registry, shared statement validation, payload parsing, address normalization. auth.middleware.js refactored to one chain-aware pipeline.
  • Phase B — Ethereum (SIWE) end-to-end: viem-based verifier (parseSiweMessage + offline recoverMessageAddress), chain-tagged AUTHORIZED_SIGNERS, client wallet-provider abstraction (client/src/lib/auth/), EIP-6963 discovery, useWalletAuth hook, ChainPicker. AdminPage + ProjectDetailsPage migrated off the Polkadot-only wallet code.
  • Phase C — Solana (Sign-In With Solana): tweetnacl/bs58 verifier, window.solana provider.
  • Phase D — multi-chain addresses: migration adds team_members.wallet_chain, projects.donation_chain, and a composite PK on wallet_contacts; repository / validation / wallet_contacts API are chain-aware; TeamPaymentSection gains chain selectors on team wallets and the payout address.

Test plan

  • ✅ Server: npm test161 tests pass (21 files), including new verifier / authorizedSigners / parsePayload / statements suites and updated auth.middleware + wallet-contact suites.
  • ✅ Client: npm run build (tsc) — green.
  • ✅ Client: npm run lint (--max-warnings 0) — green.
  • ⚠️ Interactive wallet flows not automatable here — MetaMask / Phantom / Polkadot-JS popups need a manual local run. Verify all three sign-in paths + a multi-chain payout-address save.

⚠️ Requires a human before merge

  1. Apply the migration supabase/migrations/20260520000000_multichain_addresses.sql to Supabase per docs/PRODUCTION_DEPLOYMENT.md. The wallet_contacts composite-PK swap is the highest-risk DDL — run on a Supabase branch first. All existing rows backfill to substrate via column defaults before any constraint is applied.
  2. Add test signers — to use a non-Substrate admin, add ethereum:0x… / solana:… entries to AUTHORIZED_SIGNERS (server) and VITE_ADMIN_ADDRESSES (client).

Notes / follow-ups

  • Admins can authenticate on any chain, but the treasury multisig is Polkadot — co-signing treasury transactions still requires a Substrate signer.
  • Automated payout execution stays Substrate-only; ETH/SOL payout addresses can be registered/recorded but are settled manually. A follow-up issue should cover automated multi-chain payout execution.
  • Display-only admin views (TeamAddressList, ConfirmPaymentModal) render multi-chain addresses but don't yet badge the chain. Legacy unused modals (UpdatePayoutModal, EditTeamModal, UpdateTeamModal) were left as-is.
  • Pre-existing weakness, not introduced here: the server does not track nonces, so a captured auth header is replayable. SIWE/Solana messages now carry expirationTime (10-min) as partial mitigation; a server-side nonce store is recommended as a follow-up.

🤖 Generated with Claude Code

sacha-l added 5 commits May 19, 2026 16:36
Phase A — server verifier abstraction:
- Add server/api/auth/ with a per-chain verifier registry, shared
  statement validation, payload parsing, and address normalization.
- Refactor auth.middleware.js to one chain-aware pipeline. The
  x-siws-auth payload gains an optional `chain` field defaulting to
  'substrate', so existing SIWS clients are unaffected.

Phase B — Ethereum (SIWE / EIP-4361):
- Add an Ethereum verifier (viem parseSiweMessage + offline
  recoverMessageAddress); rejects expired / not-yet-valid messages.
- Chain-tagged AUTHORIZED_SIGNERS via authorizedSigners.js
  (`chain:address` or bare => substrate); admins authorized per chain.
- Client: client/src/lib/auth/ wallet-provider abstraction (Substrate +
  Ethereum/EIP-6963), useWalletAuth hook, and a ChainPicker component.
- AdminPage and ProjectDetailsPage migrated to useWalletAuth;
  WalletConnectionBanner gains the chain picker.
- addressUtils / constants made chain-aware (backward compatible).

Server: 153 tests pass. Client build + lint green.
- Add a Solana (Sign-In With Solana) verifier: ed25519 signature
  verification via tweetnacl, base58 address handling via bs58, and a
  parser for the fixed SIWS-style message format. Rejects expired
  messages.
- normalize.js gains a solana case (canonical base58 of the 32-byte
  public key); the verifier registry now includes solana.
- Client: solanaProvider (window.solana / Phantom) builds and signs the
  matching message format; registry + ChainPicker now offer Solana.
- addressUtils normalizes solana addresses for chain-aware comparison.

Server: 158 tests pass. Client build + lint green.
Add a `chain` discriminator to every address-bearing table so team
members and payout addresses can be Substrate, Ethereum or Solana.

- Migration 20260520000000: team_members.wallet_chain,
  projects.donation_chain (both with a CHECK constraint), and a
  composite (wallet_chain, wallet_address) primary key on
  wallet_contacts. Existing rows backfill to 'substrate' via the column
  default before any constraint/PK is applied.
- validation.js: validateAddress(chain, address) with per-chain format
  rules; validateTeamMember / validateProject validate the
  (chain, address) pair.
- project.repository: transform + insert walletChain / donationChain;
  findByTeamWallet filters on (wallet_chain, wallet_address).
- updatePayoutAddress accepts donationChain and validates per chain.
- wallet_contacts repo/service/controller are chain-aware; requireOwnWallet
  exposes the signer's chain on req.user.

Server: 161 tests pass.
Wire the client API layer to the multi-chain server endpoints:
- updateTeam / updatePayoutAddress forward donationChain and per-member
  walletChain (both default to 'substrate').
- getWalletContact accepts an optional chain and sends ?chain=.

Client build + lint green.
- TeamPaymentSection: a chain selector on each team-member wallet and on
  the payout address; read-only views show a chain badge.
  handleTeamPaymentSave forwards walletChain / donationChain through
  api.updateTeam.
- ProjectDetailsPage: ApiProject / ApiTeamMember carry the chain fields;
  donationChain is passed to TeamPaymentSection.
- NotificationsCard: signs via useWalletAuth (multi-chain) instead of an
  inline substrate-only SIWS block; getWalletContact passes the connected
  chain.

Client build + lint green.
@vercel
Copy link
Copy Markdown

vercel Bot commented May 19, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
stadium Ready Ready Preview, Comment May 19, 2026 5:11pm

@sacha-l sacha-l marked this pull request as ready for review May 19, 2026 16:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant