Skip to content

feat: BIP-322 P2WSH Multisig Wallet-Connector (Sparrow-Integration) #1079

Description

@TaprootFreak

Kontext

Mit DFXswiss/packages#165 existiert das npm-Package @dfx.swiss/bip322-multisig, das BIP-322 PSBTs für P2WSH Multisig-Wallets baut und Signaturen extrahiert. Das Backend (api#3637) kann BIP-322 P2WSH Signaturen bereits verifizieren.

Dieses Issue beschreibt die Integration in das services-Frontend, damit User mit Sparrow (oder anderem PSBT-fähigen Tool) ihre Multisig-Wallet authentifizieren können.

Anforderung

User mit einer P2WSH Multisig-Wallet (bc1q..., 62 Zeichen) sollen sich über die services-Oberfläche authentifizieren können. Der Flow:

  1. User gibt seinen wsh(sortedmulti(...)) Descriptor und seine Adresse ein
  2. Frontend holt Challenge-Message vom Backend (getSignMessage)
  3. Frontend baut den BIP-322 PSBT (via Package)
  4. User lädt PSBT herunter → signiert in Sparrow mit M-of-N Cosignern
  5. User lädt signierten PSBT hoch (File-Upload oder Base64-Paste)
  6. Frontend extrahiert die BIP-322 Simple Signature (via Package)
  7. Normale Auth mit { address, signature }login()

Technische Umsetzung

1. Dependency installieren

npm install @dfx.swiss/bip322-multisig

Benötigte Imports:

import { parseDescriptor, buildBip322Psbt, extractBip322Signature } from '@dfx.swiss/bip322-multisig';

2. Neuer WalletType

Datei: src/contexts/wallet.context.tsx

Drei Einträge hinzufügen (nach dem Muster der bestehenden Wallet-Typen):

// Im enum WalletType:
MULTISIG_BTC = 'MultiSigBtc',

// In WalletBlockchains:
[WalletType.MULTISIG_BTC]: [Blockchain.BITCOIN],

// In WalletTypeMap:
[WalletType.MULTISIG_BTC]: AuthWalletType.CLI,

AuthWalletType.CLI weil das Backend keinen separaten Multisig-Typ braucht — es erkennt BIP-322 P2WSH automatisch als 4. Versuch in crypto.service.ts:verifyBitcoinBased.

3. Neuer Connector: connect-multisig.tsx

Datei: src/components/home/wallet/connect-multisig.tsx

Folgt dem CLI-Pattern (connect-cli.tsx): ConnectBase + Form + getAccount() liefert { address, signature }. Kein separater Hook nötig.

Form-Interface:

interface FormData {
  address: string;
  descriptor: string;
  signature: string;
}

Interner State-Flow (im Content-Component):

[Step 1: Input]
  - Descriptor: Textarea (mehrzeilig, Sparrow exportiert oft mit Zeilenumbrüchen)
    → Validierung: parseDescriptor() aus dem Package (wirft bei ungültigem Format)
  - Address: Input, Regex /^bc1q[a-z0-9]{58}$/ (nur native P2WSH)
    → Validierung: findAddress() aus dem Package prüft, ob Adresse zum Descriptor gehört

[Step 2: PSBT Download]
  - Wenn Descriptor + Address valide:
    → getSignMessage(address) vom Backend holen
    → buildBip322Psbt({ message, descriptor, address }) aufrufen
    → PSBT Base64 als Datei zum Download anbieten (Filename: bip322-tosign.psbt)
    → Anleitung anzeigen: "Öffne die PSBT-Datei in Sparrow, signiere mit den erforderlichen Cosignern, speichere die signierte PSBT"

[Step 3: Upload + Extract]
  - File-Upload (.psbt) ODER Base64-Textarea (wie loadPsbtBase64 in bin/finalize.ts)
  - Bei Upload: extractBip322Signature(psbtBase64) aufrufen
  - Extrahierte Signatur ins Form-Feld `signature` setzen (hidden)
  - Submit-Button aktivieren

[Step 4: Submit]
  - getAccount() liefert { address, signature }
  - ConnectBase ruft login(wallet, address, blockchain, () => Promise.resolve(signature))
  - Normaler Auth-Flow, kein Unterschied zum CLI-Login

ConnectBase-Integration:

export default function ConnectMultisig(props: ConnectProps): JSX.Element {
  const { session } = useAuthContext();
  const { activeWallet } = useWalletContext();
  const form = useForm<FormData>({ mode: 'onTouched' });

  async function getAccount(_w: WalletType, _b: Blockchain, isReconnect: boolean): Promise<Account> {
    if (isReconnect && session?.address) return { address: session.address };
    const values = form.getValues();
    return values; // { address, signature }
  }

  async function getSignature(): Promise<never> {
    throw new Error('Invalid signature call'); // Wird nie aufgerufen — Signatur ist pre-provided
  }

  return (
    <ConnectBase
      isSupported={() => true}
      getAccount={getAccount}
      signMessage={getSignature}
      renderContent={(p) => <Content form={form} {...p} />}
      autoConnect={activeWallet === props.wallet}
      {...props}
    />
  );
}

UI-Elemente (im Content-Component):

  1. Descriptor-Input: <StyledTextArea> mit Validierung via parseDescriptor()
  2. Address-Input: <StyledInput> mit Regex-Validierung
  3. PSBT-Download-Button: Erzeugt Blob → URL.createObjectURL → Download-Link
  4. PSBT-Upload: <input type="file" accept=".psbt"> + Base64-Konvertierung
  5. Status-Anzeige: Zeigt an, ob Signatur erfolgreich extrahiert wurde
  6. Submit-Button: Standard connect() aus ConnectContentProps

Error Handling:

  • parseDescriptor() wirft bei ungültigem Descriptor → Fehlermeldung beim Descriptor-Feld
  • buildBip322Psbt() wirft wenn Adresse nicht ableitbar → Fehlermeldung beim Address-Feld
  • extractBip322Signature() wirft wenn PSBT nicht finalisiert → "PSBT ist nicht vollständig signiert"

4. Routing registrieren

Datei: src/components/home/connect-wrapper.tsx

// Import hinzufügen:
import ConnectMultisig from './wallet/connect-multisig';

// Im switch-Statement:
case WalletType.MULTISIG_BTC:
  return <ConnectMultisig {...props} />;

5. Feature-Tree

Datei: src/config/feature-tree.ts

Neues Tile in die Wallet-Auswahl einfügen (z.B. neben dem CLI-Tile):

{
  id: 'multisig',
  img: 'multisig', // Icon muss erstellt werden
  wallet: { type: WalletType.MULTISIG_BTC, blockchain: Blockchain.BITCOIN },
},

Hinweis: Ein passendes Icon (z.B. Vault/Tresor-Symbol) muss als Asset hinzugefügt werden.

6. Translations

In den Sprachdateien (translations/de.json, en.json, fr.json, it.json) Texte hinzufügen:

{
  "multisig_descriptor": "Multisig Descriptor",
  "multisig_descriptor_hint": "wsh(sortedmulti(...)) aus Sparrow/Coldcard exportieren",
  "multisig_download_psbt": "PSBT herunterladen",
  "multisig_upload_psbt": "Signierte PSBT hochladen",
  "multisig_signature_extracted": "Signatur extrahiert",
  "multisig_psbt_not_finalized": "PSBT ist nicht vollständig signiert — alle erforderlichen Cosigner müssen unterschreiben"
}

Referenzimplementation

  • CLI-Flow (identischer Ablauf, anderes Interface): packages/bip322-multisig/src/bin/sign.ts + finalize.ts
  • CLI-Connector-Pattern: src/components/home/wallet/connect-cli.tsx — das Muster, dem der neue Connector folgt
  • Package Public API: parseDescriptor(), buildBip322Psbt(), extractBip322Signature()

Geschätzter Umfang

Datei Änderung Zeilen ca.
package.json +1 Dependency 1
wallet.context.tsx +3 Einträge 5
connect-wrapper.tsx +1 Case 3
feature-tree.ts +1 Tile 5
connect-multisig.tsx Neuer Component ~200
Translation-Files +6 Keys × 4 Sprachen 24
Total ~240

Abhängigkeiten

Akzeptanzkriterien

  • User kann sich mit Multisig-Wallet über Sparrow authentifizieren
  • Descriptor-Validierung zeigt verständliche Fehlermeldungen
  • PSBT-Download funktioniert als Datei
  • PSBT-Upload akzeptiert .psbt-Dateien und Base64-Text
  • Auth-Flow funktioniert end-to-end (mit deployed Backend-Verifier)
  • Kein neuer Krypto-Code im services-Repo (alles aus dem Package)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions