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
4 changes: 2 additions & 2 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,6 @@ NEXT_PUBLIC_API_URL=http://localhost:8293

# Address (not a key) granted OPERATOR_ROLE on newly created cap tables (createCapTable arg).
# Does not require that private key on the server unless the API will sign as operator.
# Wallet-first ADMIN already counts as operator onchain. Demo default often TAP Admin;
# self-hosted TA usually uses the server wallet address matching PRIVATE_KEY when set.
# Wallet-first ADMIN already counts as operator onchain.
# Demo: factory owner / deploy key 0x366a… (not TAP Admin 0x3601…).
NEXT_PUBLIC_OPERATOR_ADDRESS=0x366aA809015061C101983900d0c2ebf7d71B96AF
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ pnpm app:dev # http://localhost:3000/app (reads app/.en

### Factory mental model (do not confuse)

1. **Protocol builder** — ships BUSL contracts; owns the **shared demo** factory on Plume (`0xcd6…`, owner TAP Admin `0x366a…`). Beacon upgrades for that demo deployment.
1. **Protocol builder** — ships BUSL contracts; owns the **shared demo** factory on Plume (`0xcd6…`, factory owner `0x366a…`). TAP Admin (product wallet) is `0x3601…`. Beacon upgrades for that demo deployment.
2. **Transfer-agent business** — deploys **their own** `CapTableFactory` (`pnpm deploy-factory`). That factory is their book of business (many issuer cap tables).
3. **Issuer ADMIN** — calls `createCapTable` on a factory (permissionless); becomes admin of **that** cap table. Wallet manage UI is this path. Using the shared factory ≠ owning it.
4. **Mongo `factories`** — local mirror only (`factory:register` / deploy auto-register). Not onchain ownership.
Expand Down
4 changes: 2 additions & 2 deletions WARP.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ The protocol uses a three-tier access model:

- **ADMIN_ROLE** (asset manager's wallet): Grants/revokes roles, manages cap table governance. When created via the factory, `msg.sender` receives ADMIN. Admins are implicitly operators (`_checkOperatorRole` checks both roles).
- **OPERATOR_ROLE**: Optional extra address granted at mint (`NEXT_PUBLIC_OPERATOR_ADDRESS`). Same day-to-day writes as ADMIN. **Not** the server `PRIVATE_KEY`. The product default is the deployer (ADMIN) operates the instance; admins already satisfy operator checks.
- **Factory owner** (wallet that deployed that factory): Controls the `UpgradeableBeacon`, can upgrade the `CapTable` implementation for ALL proxies of **that** factory via `updateCapTableImplementation()`. Has no access to individual cap tables as admin. On the shared Plume demo factory this is the protocol builder (TAP Admin); a licensed transfer agent should deploy their own factory so they own upgrades.
- **Factory owner** (wallet that deployed that factory): Controls the `UpgradeableBeacon`, can upgrade the `CapTable` implementation for ALL proxies of **that** factory via `updateCapTableImplementation()`. Has no access to individual cap tables as admin. On the shared Plume demo factory this is `0x366a…` (deploy key). TAP Admin is `0x3601…`. A licensed transfer agent should deploy their own factory so they own upgrades.

**Three keys (do not conflate):** (1) **Factory owner** — cold/infrequent CLI for deploy + beacon upgrades; never the long-running Docker `PRIVATE_KEY`. (2) **Issuer ADMIN** — browser wallet in `/app` that called `createCapTable`. (3) **Server/operator** — optional root `.env` `PRIVATE_KEY` for server-signed API / `deploy-factory`; `NEXT_PUBLIC_OPERATOR_ADDRESS` is only the **address** granted at mint (no key required on server for wallet-first path). Local `PRIVATE_KEY` is **dev/demo only** (placeholder OK; poller read-only). Full write-up: `docs/src/content/development/setup.mdx` → “Three wallets / keys” (`#three-wallets-keys`).

Expand Down Expand Up @@ -504,7 +504,7 @@ Libraries:
8. **Mixing `/create` and `/register-onchain` semantics**: `/create` makes the server submit onchain; `/register-onchain` assumes the caller already did. Don't reintroduce a `suppliedId`-style overload on the `/create` route — that pattern was explicitly removed.
9. **Optimistic-state dedupe by stakeholder+stockclass**: Don't. Multiple issuances can exist for the same pair; deduping there hides legitimate in-flight rows. Use a TTL (current: 90s) and let the aggregated holding row absorb the new total once the poller catches up.
10. **MongoDB "Connection ended" log lines are not an error**: they're normal idle connection-pool churn (`connectionCount` ticks down as pooled sockets close). A real failure logs "Error connecting to Mongo". The poller printing `Processing for <issuer>: <block>` with an advancing block number means it is healthy.
11. **Factory config has two independent sources — don't conflate them**: the server reads the factory from the Mongo `factories` collection (`deployCapTable` uses `factories[0].factory_address`); the frontend reads `NEXT_PUBLIC_FACTORY_ADDRESS` from `app/.env.local`. The Docker app service gets `NEXT_PUBLIC_*` from compose env (root `.env`). `pnpm app:dev` reads only `app/.env.local` — keep both files aligned. The factory address is deployment-specific (deployer wallet + nonce) and the implementation is an **upgradeable** beacon target, so **never hardcode them**: `pnpm deploy-factory` auto-registers both from the real deploy, and `pnpm factory:register --factory <addr>` reads the current implementation from the factory onchain (`upsertFactory` keeps a single record — one operator factory, many cap tables). Keep the Mongo factory and `app/.env.local` on the same address. A factory's **owner** (the wallet that deployed it) controls beacon upgrades for all its cap tables. On the shared Plume demo factory that is TAP Admin (`0x366a…`), not your issuer wallet. Only reuse a factory whose owner wallet you control.
11. **Factory config has two independent sources — don't conflate them**: the server reads the factory from the Mongo `factories` collection (`deployCapTable` uses `factories[0].factory_address`); the frontend reads `NEXT_PUBLIC_FACTORY_ADDRESS` from `app/.env.local`. The Docker app service gets `NEXT_PUBLIC_*` from compose env (root `.env`). `pnpm app:dev` reads only `app/.env.local` — keep both files aligned. The factory address is deployment-specific (deployer wallet + nonce) and the implementation is an **upgradeable** beacon target, so **never hardcode them**: `pnpm deploy-factory` auto-registers both from the real deploy, and `pnpm factory:register --factory <addr>` reads the current implementation from the factory onchain (`upsertFactory` keeps a single record — one operator factory, many cap tables). Keep the Mongo factory and `app/.env.local` on the same address. A factory's **owner** (the wallet that deployed it) controls beacon upgrades for all its cap tables. On the shared Plume demo factory that is `0x366a…` (deploy key), not TAP Admin `0x3601…`. Only reuse a factory whose owner wallet you control.
12. **Docker Next rewrites vs browser**: `NEXT_PUBLIC_API_URL` drives Next **server-side** `/api/*` rewrites. In the Docker app container use `http://server:8293`. Host `pnpm app:dev` uses `http://localhost:8293` in `app/.env.local`. Wrong value → mint onchain succeeds but register shows Internal Server Error.
13. **Issuing a stakeholder's first stock**: the Issue Stock dropdown needs the issuer's stakeholders, so `GET /cap-table/holdings/stock` returns `stakeholders` (and `stockClasses`) — the manage UI can populate the dropdown before any issuance exists. Don't source the stakeholder list only from `holdings[]`; it's empty until stock is issued, which would make a fresh cap table unable to issue its first shares after a page reload.
14. **Nav issuer id**: company section links must use the real UUID from `router.query.issuerId` (or path), never a pattern string from `pathname` — otherwise users land on `/app/companies/%5BissuerId%5D`.
Expand Down
20 changes: 18 additions & 2 deletions app/src/pages/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,25 @@ export default function Home() {
</thead>
<tbody>
<tr>
<td>TAP Admin / factory owner (demo)</td>
<td>TAP Admin</td>
<td>
<a href="https://explorer.plume.org/address/0x366aA809015061C101983900d0c2ebf7d71B96AF">
<a
href="https://explorer.plume.org/address/0x3601a913fD3466f30f5ABb978E484d1B37Ce995D"
target="_blank"
rel="noopener noreferrer"
>
0x3601a913fD3466f30f5ABb978E484d1B37Ce995D
</a>
</td>
</tr>
<tr>
<td>Factory owner (beacon upgrades)</td>
<td>
<a
href="https://explorer.plume.org/address/0x366aA809015061C101983900d0c2ebf7d71B96AF"
target="_blank"
rel="noopener noreferrer"
>
0x366aA809015061C101983900d0c2ebf7d71B96AF
</a>
</td>
Expand Down
7 changes: 4 additions & 3 deletions docs/src/content/development/factory-deploy.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ A **CapTableFactory** is the onchain house for a transfer-agent business: it dep

| Role | Meaning |
| --- | --- |
| Factory owner | Wallet that deployed the factory (beacon upgrades). For the shared Plume demo this is TAP Admin — not every issuer wallet. |
| Factory owner | Wallet that deployed the factory (beacon upgrades). For the shared Plume demo this is `0x366a…`, not TAP Admin `0x3601…`. |
| Issuer ADMIN | Wallet that called `createCapTable` on a factory. |
| Mongo register | Local DB row so *this* API knows which factory address to use — not ownership. |

Expand Down Expand Up @@ -62,8 +62,9 @@ REUSE_TAP_FACTORY=1 pnpm bootstrap

| Actor / deployment | Address | Capability |
| --- | --- | --- |
| Shared demo `CapTableFactory` | `0xcd6Df14406b0569ceEABa884A18717774EdeaCA1` | Permissionless issuer cap-table creation |
| Factory owner (TAP Admin) | `0x366aA809015061C101983900d0c2ebf7d71B96AF` | Beacon upgrades for this factory |
| TAP Admin | `0x3601a913fD3466f30f5ABb978E484d1B37Ce995D` | Product / issuer wallet (mints cap tables through the demo factory) |
| Shared demo `CapTableFactory` | `0xcd6Df14406b0569ceEABa884A18717774EdeaCA1` | Permissionless issuer cap-table creation (32 tables; live impl `0xB63C…`) |
| Factory owner | `0x366aA809015061C101983900d0c2ebf7d71B96AF` | Beacon upgrades for this factory (deploy key, not TAP Admin) |

You can mint issuer cap tables through this factory (`createCapTable` is permissionless). **You do not own upgrades.** Production transfer agents should deploy their own factory.

Expand Down
6 changes: 3 additions & 3 deletions docs/src/content/development/setup.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ These are easy to conflate. They are **not** the same wallet.
- **Wallet-first `/app` UI** — ADMIN signs in the browser. Poller needs RPC only. Leave `PRIVATE_KEY=UPDATE_ME` (or unset). Server runs read-only for chain writes; that is expected. ADMIN alone is enough for the manage UI.
- **`NEXT_PUBLIC_OPERATOR_ADDRESS`** — an **address** granted `OPERATOR_ROLE` on **new** mints (for server/automation later). Setting it does **not** put a key on the server. You only need that private key in `.env` if the API itself will sign as operator.
- **Factory owner key** — never put this in the long-running Docker/API env. Compromise would let an attacker upgrade every cap table on that factory. Inject only for a one-shot `pnpm deploy-factory` or upgrade script (separate env file / CI secret), then remove.
- **Shared Plume demo factory** (`0xcd6…`) — owner is TAP Admin (`0x366a…`), not your issuer wallet. Reusing it does not make you factory owner.
- **Shared Plume demo factory** (`0xcd6…`) — factory owner is `0x366a…` (deploy key). TAP Admin is `0x3601…`. Reusing the factory does not make you factory owner.

<Callout type="warning">
`PRIVATE_KEY` in local `.env` is **dev/demo only**. Optional for the wallet-first path. Never put production or factory-owner keys here. Do not commit real keys.
Expand Down Expand Up @@ -56,7 +56,7 @@ Wallet connection is first-party (EIP-6963). Install a browser extension (Rabby,
## Bootstrap the stack

```bash
# Demo / issuer-dev: register TAP’s shared Plume factory in Mongo (owner is TAP Admin, not you)
# Demo / issuer-dev: register TAP’s shared Plume factory in Mongo (owner is 0x366a…, not TAP Admin 0x3601…)
REUSE_TAP_FACTORY=1 pnpm bootstrap

# Transfer-agent path: after bootstrap with no factory, deploy your own instead
Expand All @@ -77,7 +77,7 @@ Prefer **host** `pnpm app:dev` for wallet work. Docker app is optional.

| Path | Command | You own beacon upgrades? |
| --- | --- | --- |
| Shared demo factory | `REUSE_TAP_FACTORY=1 pnpm bootstrap` | **No** (protocol builder / TAP Admin) |
| Shared demo factory | `REUSE_TAP_FACTORY=1 pnpm bootstrap` | **No** (factory owner `0x366a…`) |
| Your TA factory | `pnpm deploy-factory` | **Yes** |

Issuers can mint cap tables on either factory (`createCapTable` is permissionless). Using the demo factory is normal for local product work; production transfer agents should deploy their own.
Expand Down
8 changes: 4 additions & 4 deletions scripts/bootstrap-plume.sh
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ set -e
# 7. Bring up mongodb + server (+ app) via docker compose
# 8. Wait for API health
# 9. Factory: never hardcode impl. Deploy your own (pnpm deploy-factory) or
# REUSE_TAP_FACTORY=1 to register the shared demo factory (owner = TAP Admin).
# REUSE_TAP_FACTORY=1 to register the shared demo factory (owner = 0x366a deploy key).
#
# Overrides: API_URL, REUSE_TAP_FACTORY=1, SKIP_APP=1 (mongo+server only).
#
Expand All @@ -26,7 +26,7 @@ ROOT_DIR="$(dirname "$SCRIPT_DIR")"
cd "$ROOT_DIR"

# Shared demo CapTableFactory on Plume (protocol-builder demo deployment).
# Factory owner (beacon upgrades) is TAP Admin 0x366a… — NOT your issuer wallet.
# Factory owner (beacon upgrades) is 0x366a… (deploy key). TAP Admin is 0x3601….
# createCapTable is permissionless; issuers mint cap tables through this factory
# without owning it. Licensed TAs should deploy their OWN factory instead.
TAP_FACTORY_ADDRESS="0xcd6Df14406b0569ceEABa884A18717774EdeaCA1"
Expand Down Expand Up @@ -130,15 +130,15 @@ if [ "${FACTORY_COUNT:-0}" -gt 0 ]; then
echo "✅ Factory already registered in Mongo (count=$FACTORY_COUNT) — leaving as-is."
elif [ "${REUSE_TAP_FACTORY:-0}" = "1" ]; then
echo "🌱 REUSE_TAP_FACTORY=1 — registering shared demo factory $TAP_FACTORY_ADDRESS (impl read onchain)..."
echo " Beacon upgrades stay with that factory's owner (TAP Admin / protocol builder)."
echo " Beacon upgrades stay with that factory's owner (0x366a… deploy key)."
echo " This is the demo/issuer-dev path — not the same as owning a transfer-agent factory."
pnpm factory:register --factory "$TAP_FACTORY_ADDRESS"
else
echo "ℹ️ No factory registered yet."
echo " Transfer-agent / production path — deploy YOUR factory (you become owner):"
echo " pnpm deploy-factory # CapTable + CapTableFactory + auto Mongo register"
echo " Then set NEXT_PUBLIC_FACTORY_ADDRESS in app/.env.local to the printed factory address."
echo " Demo/issuer-dev path — reuse TAP's shared Plume factory (owner is TAP Admin, not you):"
echo " Demo/issuer-dev path — reuse TAP's shared Plume factory (owner is 0x366a…, not TAP Admin 0x3601…):"
echo " REUSE_TAP_FACTORY=1 pnpm bootstrap"
fi

Expand Down
Loading