Bootstrap project, add protocol
This commit is contained in:
@@ -0,0 +1,2 @@
|
|||||||
|
node_modules
|
||||||
|
.todo
|
||||||
+357
@@ -0,0 +1,357 @@
|
|||||||
|
NIP-XX
|
||||||
|
======
|
||||||
|
|
||||||
|
FROST Multisig Quorum Protocol
|
||||||
|
-------------------------------
|
||||||
|
|
||||||
|
`draft` `optional`
|
||||||
|
|
||||||
|
## Abstract
|
||||||
|
|
||||||
|
This NIP defines a protocol for creating and operating FROST threshold signature quorums over Nostr keys. A quorum is a group of n participants who collectively control a shared Nostr keypair via a (t,n) threshold signing scheme, where any t members can produce a valid signature but no fewer. The quorum's private key is never known to any single party.
|
||||||
|
|
||||||
|
## Participant Indexing
|
||||||
|
|
||||||
|
Each member is assigned a positive integer index. Indices are derived deterministically by sorting member pubkeys lexicographically as lowercase hex strings and assigning 1-based positions. This ordering must be consistent across all participants and all rounds of a session.
|
||||||
|
|
||||||
|
When a new member set is established (rotation), indices are re-derived from the new member list.
|
||||||
|
|
||||||
|
## Event Kinds
|
||||||
|
|
||||||
|
All events in this protocol are sent as the `content` payload of a NIP-59 gift wrap (kind 1059). The `content` is encrypted to the recipient's pubkey and the wrap is sent to the recipients inbox relays (kind 10050) per NIP 17.
|
||||||
|
|
||||||
|
## Quorum Creation
|
||||||
|
|
||||||
|
Creates a new quorum and derives its shared Nostr keypair. The private key is never known to any party.
|
||||||
|
|
||||||
|
### Phase 0 — Invitation (kind 7050)
|
||||||
|
|
||||||
|
The initiator sends each proposed member a gift-wrapped event:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"kind": 7050,
|
||||||
|
"content": "<optional human-readable message>",
|
||||||
|
"tags": [
|
||||||
|
["session_id", "<32-byte random hex>"],
|
||||||
|
["threshold", "<signing threshold>"],
|
||||||
|
["member", "<pk_1>"],
|
||||||
|
["member", "<pk_n>"],
|
||||||
|
],
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`session_id` is chosen by the initiator and identifies all subsequent events for this DKG session. The quorum's keypair does not yet exist; no key material is present in the invite.
|
||||||
|
|
||||||
|
Participation in Round 1 signals acceptance. A member may send a decline event, but is not required to.
|
||||||
|
|
||||||
|
### Round 1 — Commitments (kind 7051)
|
||||||
|
|
||||||
|
Each accepting participant Pᵢ:
|
||||||
|
|
||||||
|
1. Samples a random polynomial `fᵢ(x)` of degree `t−1` over the secp256k1 scalar field, with random coefficients `aᵢ₀, aᵢ₁, …, aᵢ,ₜ₋₁`
|
||||||
|
2. Computes Feldman commitments: `Cᵢ = [aᵢ₀·G, aᵢ₁·G, …, aᵢ,ₜ₋₁·G]` (compressed 33-byte EC points as hex)
|
||||||
|
3. Computes a Schnorr proof of knowledge of `aᵢ₀`:
|
||||||
|
- Sample ephemeral scalar `k`; compute `R = k·G`
|
||||||
|
- `c = H("frost/dkg/round1" || session_id || pkᵢ || Cᵢ[0])`
|
||||||
|
- `s = k + aᵢ₀ · c (mod q)`
|
||||||
|
4. Sends to all other members (n−1 gift wraps, identical payload):
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"kind": 7051,
|
||||||
|
"content": "",
|
||||||
|
"tags": [
|
||||||
|
["session_id", "<32-byte hex>"],
|
||||||
|
["commit", "<aᵢ₀·G hex>"],
|
||||||
|
["commit", "<aᵢ₁·G hex>"],
|
||||||
|
["proof", "<R hex>", "<s hex>"]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Round 2 — Share Distribution (kind 7052)
|
||||||
|
|
||||||
|
After receiving Round 1 from all other participants, Pᵢ:
|
||||||
|
|
||||||
|
1. Verifies each Pⱼ's PoK: `s·G == R + c·Cⱼ[0]` where `c` is recomputed from the proof
|
||||||
|
2. For each Pⱼ (j ≠ i), evaluates `sᵢⱼ = fᵢ(j)` (polynomial evaluated at Pⱼ's 1-based index)
|
||||||
|
3. Sends encrypted to Pⱼ only (one gift wrap per recipient):
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"kind": 7052,
|
||||||
|
"content": "",
|
||||||
|
"tags": [
|
||||||
|
["session_id", "<32-byte hex>"],
|
||||||
|
["share", "<sᵢⱼ hex scalar>"]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Finalization
|
||||||
|
|
||||||
|
Each Pⱼ, after receiving shares from all other participants:
|
||||||
|
|
||||||
|
1. Verifies each received share against Pᵢ's commitments:
|
||||||
|
|
||||||
|
`sᵢⱼ·G == Σₖ₌₀ᵗ⁻¹ ( j^k · Cᵢₖ )`
|
||||||
|
|
||||||
|
Abort if any check fails.
|
||||||
|
|
||||||
|
2. Computes secret shard: `xⱼ = Σᵢ sᵢⱼ (mod q)`
|
||||||
|
|
||||||
|
3. Computes group public key: `Y = Σᵢ Cᵢ₀` (sum of all participants' first commitments)
|
||||||
|
|
||||||
|
4. Computes own verification share: `Yⱼ = Σᵢ Σₖ₌₀ᵗ⁻¹ ( j^k · Cᵢₖ )`; verify `xⱼ·G == Yⱼ`
|
||||||
|
|
||||||
|
5. **BIP-340 normalization**: if `Y` has odd y-coordinate, negate `xⱼ` and `Yⱼ` and use the even-y form of `Y` as the quorum pubkey. This negation must be applied consistently across all subsequent operations.
|
||||||
|
|
||||||
|
6. Stores `(xⱼ, Y, Yⱼ, members, threshold, session_id, Round-1 commitments)` durably in IndexedDB. The Round-1 commitments are retained because they are needed to verify resharing participants' shards in Protocol 2.
|
||||||
|
|
||||||
|
7. Publishes a DKG confirmation event (kind 7053).
|
||||||
|
|
||||||
|
### Confirmation (kind 7053)
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"kind": 7053,
|
||||||
|
"content": "",
|
||||||
|
"tags": [
|
||||||
|
["session_id", "<32-byte hex>"],
|
||||||
|
["quorum", "<Y x-only hex>"],
|
||||||
|
["transcript", "<H(session_id || sorted Round-1 commitments)>"]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`transcript_hash` enables equivocation detection: a malicious participant may send different Round-1 commitments to different members. If any two confirmations carry the same `session_id` but different `transcript_hash` or `quorum_pubkey`, all participants must abort.
|
||||||
|
|
||||||
|
The quorum is considered live once `t` confirmations with matching `transcript_hash` and `quorum_pubkey` have been observed.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Protocol 2: Key Redistribution (Rotation)
|
||||||
|
|
||||||
|
Redistributes the existing quorum key to a new member set and/or threshold without reconstructing the private key. The quorum's Nostr pubkey `Y` is preserved, so the quorum's identity, profile, and event history are unaffected.
|
||||||
|
|
||||||
|
**Prerequisite**: A contributing set `S` of at least `t` current members must participate.
|
||||||
|
|
||||||
|
### Phase 0 — Resharing Proposal (kind 7054)
|
||||||
|
|
||||||
|
The initiator (a current member) sends to all current and prospective new members:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"kind": 7054,
|
||||||
|
"content": "<optional human-readable message>",
|
||||||
|
"tags": [
|
||||||
|
["session_id", "<32-byte hex>"],
|
||||||
|
["quorum", "<Y x-only hex>"],
|
||||||
|
["threshold", "<t'>"],
|
||||||
|
["old_member", "<pk_1>"],
|
||||||
|
["old_member", "<pk_n>"],
|
||||||
|
["member", "<pk_1'>"],
|
||||||
|
["member", "<pk_n'>"]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
New member indices are derived by sorting `new_members` lexicographically (1-based), independently of the old index assignment.
|
||||||
|
|
||||||
|
Old members signal participation by contributing in Round 1. The contributing set `S` **must be fixed before shares are combined**, because each old member's Lagrange coefficient depends on the full set `S`. Implementations should establish `S` via a timeout or an explicit "I'm participating" acknowledgment step before proceeding to Round 2.
|
||||||
|
|
||||||
|
### Round 1 — Old Member Commitments (kind 7055)
|
||||||
|
|
||||||
|
Each participating old member Pᵢ (with index `i` from the original DKG and shard `xᵢ`):
|
||||||
|
|
||||||
|
1. Computes Lagrange coefficient over contributing set `S` at point 0:
|
||||||
|
|
||||||
|
`λᵢ = Πⱼ∈S, j≠i (-j) / (i - j) (mod q)`
|
||||||
|
|
||||||
|
2. Samples a new random polynomial `hᵢ(x)` of degree `t'−1` with constant term `hᵢ(0) = λᵢ · xᵢ`
|
||||||
|
|
||||||
|
3. Computes commitments: `Dᵢ = [λᵢ·xᵢ·G, …]` (same structure as Round-1 commitments in Protocol 1)
|
||||||
|
|
||||||
|
4. The first commitment `Dᵢ[0]` must equal `λᵢ · Yᵢ`, where `Yᵢ` is Pᵢ's verification share from the original DKG (publicly computable from stored Round-1 commitments). This proves Pᵢ is resharing their actual shard.
|
||||||
|
|
||||||
|
5. Computes Schnorr PoK of `λᵢ · xᵢ` (same construction as Protocol 1 Round 1, using `"frost/resharing/round1"` as domain tag)
|
||||||
|
|
||||||
|
6. Sends to all new members (m gift wraps, identical payload):
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"kind": 7055,
|
||||||
|
"content": "",
|
||||||
|
"tags": [
|
||||||
|
["session_id", "<32-byte hex>"],
|
||||||
|
["quorum", "<Y x-only hex>"],
|
||||||
|
["commit", "<λᵢ·xᵢ·G hex>"],
|
||||||
|
["commit", "<bᵢ₁·G hex>"],
|
||||||
|
["proof", "<R hex>", "<s hex>"]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
New members verify before proceeding: `Σᵢ∈S Dᵢ[0] == Y`. Abort if this check fails.
|
||||||
|
|
||||||
|
### Round 2 — Share Distribution (kind 7056)
|
||||||
|
|
||||||
|
Each participating old member Pᵢ evaluates `hᵢ(j)` at each new member Qⱼ's index `j` and sends encrypted to Qⱼ only:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"kind": 7056,
|
||||||
|
"content": "",
|
||||||
|
"tags": [
|
||||||
|
["session_id", "<32-byte hex>"],
|
||||||
|
["quorum", "<Y x-only hex>"],
|
||||||
|
["share", "<hᵢ(j) hex scalar>"]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Finalization
|
||||||
|
|
||||||
|
Each new member Qⱼ, after receiving shares from all members of `S`:
|
||||||
|
|
||||||
|
1. Verifies each received share against Dᵢ commitments:
|
||||||
|
|
||||||
|
`hᵢ(j)·G == Σₖ₌₀ᵗ'⁻¹ ( j^k · Dᵢₖ )`
|
||||||
|
|
||||||
|
Abort if any check fails.
|
||||||
|
|
||||||
|
2. Computes new shard: `x'ⱼ = Σᵢ∈S hᵢ(j) (mod q)`
|
||||||
|
|
||||||
|
3. Computes new verification share: `Y'ⱼ = Σᵢ∈S Σₖ ( j^k · Dᵢₖ )`; verify `x'ⱼ·G == Y'ⱼ`
|
||||||
|
|
||||||
|
4. BIP-340 normalization: `Y` is unchanged, so the same even-y convention applies. If `xᵢ` was negated during the original DKG finalization, `hᵢ(0) = λᵢ · xᵢ` already incorporates that negation. Qⱼ verifies against the same `Y` and does not re-negate.
|
||||||
|
|
||||||
|
5. Replaces stored quorum state with `(x'ⱼ, Y, Y'ⱼ, new_members, new_threshold, session_id, Round-1 commitments from this session)`.
|
||||||
|
|
||||||
|
6. Publishes resharing confirmation (kind 7057).
|
||||||
|
|
||||||
|
### Resharing Confirmation (kind 7057)
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"kind": 7057,
|
||||||
|
"content": "",
|
||||||
|
"tags": [
|
||||||
|
["session_id", "<32-byte hex>"],
|
||||||
|
["quorum", "<Y x-only hex>"],
|
||||||
|
["transcript", "<H(session_id || sorted Round-1 commitments)>"]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Once `t'` new members have published matching confirmations, the rotation is considered complete. The quorum then publishes a signed `kind 0` metadata event under `Y` recording the new member list. This event is the on-chain rotation record and is used to gate NIP-17 chat display by membership at time of message.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Protocol 3: Collaborative Signing (FROST)
|
||||||
|
|
||||||
|
Any quorum member may initiate a signing session. At least t members must participate to produce a valid signature.
|
||||||
|
|
||||||
|
### Sign Request (kind 7058)
|
||||||
|
|
||||||
|
The initiator sends to all members:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"kind": 7058,
|
||||||
|
"content": "<JSON-stringified unsigned nostr event>",
|
||||||
|
"tags": [
|
||||||
|
["session_id", "<32-byte hex>"],
|
||||||
|
["quorum", "<Y x-only hex>"]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Round 1 — Nonce Commitments (kind 7059)
|
||||||
|
|
||||||
|
Each willing signer Pᵢ:
|
||||||
|
|
||||||
|
1. Samples ephemeral nonce pair `(dᵢ, eᵢ)` uniformly at random (must never be reused)
|
||||||
|
2. Computes commitments `Dᵢ = dᵢ·G`, `Eᵢ = eᵢ·G`
|
||||||
|
3. Sends to all other participating signers:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"kind": 7059,
|
||||||
|
"content": "",
|
||||||
|
"tags": [
|
||||||
|
["session_id", "<32-byte hex>"],
|
||||||
|
["quorum", "<Y x-only hex>"],
|
||||||
|
["D", "<dᵢ·G hex>"],
|
||||||
|
["E", "<eᵢ·G hex>"]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Round 2 — Signature Shares (kind 7060)
|
||||||
|
|
||||||
|
After collecting Round 1 from at least `t` signers, each Pᵢ:
|
||||||
|
|
||||||
|
1. Finalizes the signing set `S` (the participants whose commitments were collected)
|
||||||
|
2. Computes binding factors: `ρᵢ = H("frost/sign/rho" || i || msg || {(Dⱼ, Eⱼ)}ⱼ∈S)` for each `i ∈ S`
|
||||||
|
3. Computes group nonce: `R = Σᵢ∈S (Dᵢ + ρᵢ·Eᵢ)`; if `R` has odd y-coordinate, negate `dᵢ, eᵢ` and use the even-y `R`
|
||||||
|
4. Computes BIP-340 challenge: `c = H_BIP340("BIP0340/challenge" || R.x || Y || msg)`
|
||||||
|
5. Computes Lagrange coefficient `λᵢ` over `S` at point 0 (same formula as Protocol 2)
|
||||||
|
6. Computes signature share: `zᵢ = dᵢ + eᵢ·ρᵢ + λᵢ·xᵢ·c (mod q)` (using the BIP-340-normalized `xᵢ`)
|
||||||
|
7. Sends to coordinator:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"kind": 7060,
|
||||||
|
"content": "",
|
||||||
|
"tags": [
|
||||||
|
["session_id", "<32-byte hex>"],
|
||||||
|
["quorum", "<Y x-only hex>"],
|
||||||
|
["z", "<zᵢ hex scalar>"]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Aggregation
|
||||||
|
|
||||||
|
The coordinator (any member) aggregates:
|
||||||
|
|
||||||
|
- `z = Σᵢ∈S zᵢ (mod q)`
|
||||||
|
- Final signature: `(R.x, z)`
|
||||||
|
|
||||||
|
The coordinator verifies the signature against `Y` and `msg` using standard BIP-340 verification before publishing the event. The resulting signature is a valid BIP-340 Schnorr signature, indistinguishable from a single-key signature.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Storage
|
||||||
|
|
||||||
|
Quorum state must be stored durably in IndexedDB per-quorum:
|
||||||
|
|
||||||
|
| Field | Description |
|
||||||
|
|-------|-------------|
|
||||||
|
| `quorum_pubkey` | `Y` as x-only hex — the quorum's Nostr pubkey |
|
||||||
|
| `shard` | `xⱼ` — this member's secret share (encrypted at rest) |
|
||||||
|
| `verification_share` | `Yⱼ` — public verification share for this member |
|
||||||
|
| `members` | Current member pubkeys and indices |
|
||||||
|
| `threshold` | Current signing threshold `t` |
|
||||||
|
| `dkg_commitments` | All participants' Round-1 commitments from the most recent DKG or resharing session |
|
||||||
|
| `rotation_records` | All signed rotation `kind 0` events in order |
|
||||||
|
|
||||||
|
`dkg_commitments` are retained permanently because they are required to verify shard authenticity during resharing (step 4 of Protocol 2 Round 1).
|
||||||
|
|
||||||
|
`rotation_records` are used to determine which members were active at a given time, which gates NIP-17 chat message display.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Security Notes
|
||||||
|
|
||||||
|
**Equivocation**: A malicious participant can send different Round-1 commitments to different members, causing different members to derive different group keys. The `transcript_hash` in confirmation events provides detection. Implementations must abort if any two confirmations for the same session have different hashes.
|
||||||
|
|
||||||
|
**Abort and restart**: If any participant fails to complete their round within a timeout, the session must be fully aborted. Partial state (nonces, sub-shares) must be discarded. A new session with a new `session_id` must be started from Phase 0.
|
||||||
|
|
||||||
|
**Nonce reuse in signing**: Reusing `(dᵢ, eᵢ)` across two signing sessions leaks the shard `xᵢ`. Implementations must use fresh randomness for every session and must not persist signing nonces.
|
||||||
|
|
||||||
|
**Contributing set integrity**: In Protocol 2, the Lagrange coefficients and the integrity check `Σ Dᵢ[0] == Y` are only meaningful over the same set `S`. The set must be fixed and agreed upon before shares are combined. Any late-joining or aborting member after `S` is finalized requires a full session restart.
|
||||||
|
|
||||||
|
**BIP-340 y-coordinate normalization**: Nostr uses x-only public keys. Both the group key `Y` (finalized in DKG) and the signing nonce `R` (per signing session) require even-y normalization, which affects the sign of `xⱼ` and `(dᵢ, eᵢ)` respectively. These negations are independent and must both be applied correctly.
|
||||||
@@ -16,29 +16,11 @@ Rotating membership is a special case of signing an event; this option opens a f
|
|||||||
|
|
||||||
The sign event button opens a modal/drawer which asks the user to choose which event kind: public note (kind 1), relay selections (kind 10002), profile (kind 0).
|
The sign event button opens a modal/drawer which asks the user to choose which event kind: public note (kind 1), relay selections (kind 10002), profile (kind 0).
|
||||||
|
|
||||||
Use solidjs/tailwind/preline and applesauce for nostr functionality. Use toast for error messages, not inline errors.
|
Use solidjs/tailwind/preline and applesauce for nostr functionality. Use `@noble/curves` for FROST cryptographic primitives (DKG, Feldman VSS, threshold signing) and `@noble/hashes` for hash functions. Use toast for error messages, not inline errors.
|
||||||
|
|
||||||
# Protocol
|
# Protocol
|
||||||
|
|
||||||
A quorum is a standard FROST multisig over a nostr key.
|
See [PROTOCOL.md](PROTOCOL.md) for the full protocol specification.
|
||||||
|
|
||||||
## Creating a quorum
|
|
||||||
|
|
||||||
When creating a quorum, the initiating user creates a `kind 7050` payload with the following fields:
|
|
||||||
|
|
||||||
- members: a list of quorum member hex nostr pubkeys
|
|
||||||
- threshold: proposed threshold for signing
|
|
||||||
- message: a message for all invitees
|
|
||||||
- events: a list of events to sign
|
|
||||||
- [key material]
|
|
||||||
|
|
||||||
This is wrapped and sent to all members using kind 1059 (NIP 59) with t=encrypt(quorum pubkey, recipient).
|
|
||||||
|
|
||||||
## Rotating keys
|
|
||||||
|
|
||||||
## Collaborative signing
|
|
||||||
|
|
||||||
When signing collaboratively, a round-robin-style FROST multisig signing round takes place in which the initiator sends a .
|
|
||||||
|
|
||||||
# Other details
|
# Other details
|
||||||
|
|
||||||
|
|||||||
+12
@@ -0,0 +1,12 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Quorum</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/index.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
{
|
||||||
|
"name": "quorum",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "tsc && vite build",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@noble/curves": "^2.2.0",
|
||||||
|
"@noble/hashes": "^2.2.0",
|
||||||
|
"applesauce-accounts": "^6.0.0",
|
||||||
|
"applesauce-actions": "^6.1.0",
|
||||||
|
"applesauce-core": "^6.1.0",
|
||||||
|
"applesauce-factory": "^4.0.0",
|
||||||
|
"applesauce-relay": "^6.0.3",
|
||||||
|
"applesauce-signers": "^6.0.1",
|
||||||
|
"applesauce-solidjs": "^4.0.0",
|
||||||
|
"solid-js": "^1.9.13",
|
||||||
|
"solid-toast": "^0.5.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@tailwindcss/vite": "^4.3.0",
|
||||||
|
"autoprefixer": "^10.5.0",
|
||||||
|
"postcss": "^8.5.15",
|
||||||
|
"preline": "^4.2.0",
|
||||||
|
"tailwindcss": "^4.3.0",
|
||||||
|
"typescript": "^6.0.3",
|
||||||
|
"vite": "^8.0.16",
|
||||||
|
"vite-plugin-solid": "^2.11.12"
|
||||||
|
}
|
||||||
|
}
|
||||||
Generated
+2327
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,3 @@
|
|||||||
|
export default function App() {
|
||||||
|
return <div class="p-4">Quorum</div>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
@import "tailwindcss";
|
||||||
|
@plugin "preline/plugin";
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import { render } from "solid-js/web";
|
||||||
|
import { Toaster } from "solid-toast";
|
||||||
|
import App from "./App";
|
||||||
|
import "./index.css";
|
||||||
|
|
||||||
|
render(
|
||||||
|
() => (
|
||||||
|
<>
|
||||||
|
<App />
|
||||||
|
<Toaster />
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
document.getElementById("root")!,
|
||||||
|
);
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ESNext",
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"jsx": "preserve",
|
||||||
|
"jsxImportSource": "solid-js",
|
||||||
|
"strict": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"isolatedModules": true
|
||||||
|
},
|
||||||
|
"include": ["src"]
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import tailwindcss from "@tailwindcss/vite";
|
||||||
|
import { defineConfig } from "vite";
|
||||||
|
import solidPlugin from "vite-plugin-solid";
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [solidPlugin(), tailwindcss()],
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user