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
215 changes: 214 additions & 1 deletion docs/api-endpoints.md
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,219 @@ Queue depth metrics for indexer workers.

## Admin Endpoints

### POST /admin/proposals

Create a new multisig proposal requiring multi-sig approval.

- **Auth:** Admin required
- **Body:**

```json
{
"changeType": "update_fee",
"payload": { "feeBps": 500, "treasuryAddress": "GATREASURYADDRESSFORACCESSLAYERTESTING123456789" },
"threshold": 2,
"totalSigners": 3
}
```

- **Response:** `201 Created`

```json
{
"success": true,
"data": {
"id": "cm123...",
"proposalId": "msig-1234567890-abc123",
"changeType": "update_fee",
"payload": { "feeBps": 500, "treasuryAddress": "GATREASURYADDRESSFORACCESSLAYERTESTING123456789" },
"status": "pending",
"threshold": 2,
"totalSigners": 3,
"proposedBy": "GAADMIN1WALLETADDRESSFORACCESSLAYERTESTING123456789",
"proposedAt": "2026-09-27T10:30:00.000Z",
"executedAt": null,
"rejectedAt": null,
"rejectedBy": null,
"rejectionReason": null,
"createdAt": "2026-09-27T10:30:00.000Z",
"updatedAt": "2026-09-27T10:30:00.000Z",
"signatures": [],
"approvalCount": 0
}
}
```

### GET /admin/proposals

List all multisig proposals with pagination and optional status filter.

- **Auth:** Admin required
- **Query Params:**
- `status` (optional): `pending`, `executed`, or `rejected`
- `page` (number, default: 1)
- `limit` (number, default: 20, max: 100)
- **Response:** `200 OK`

```json
{
"success": true,
"data": {
"items": [
{
"id": "cm123...",
"proposalId": "msig-1234567890-abc123",
"changeType": "update_fee",
"payload": { "feeBps": 500 },
"status": "pending",
"threshold": 2,
"totalSigners": 3,
"proposedBy": "GAADMIN1WALLETADDRESSFORACCESSLAYERTESTING123456789",
"proposedAt": "2026-09-27T10:30:00.000Z",
"executedAt": null,
"rejectedAt": null,
"rejectedBy": null,
"rejectionReason": null,
"createdAt": "2026-09-27T10:30:00.000Z",
"updatedAt": "2026-09-27T10:30:00.000Z",
"signatures": [
{
"id": "cm456...",
"proposalId": "msig-1234567890-abc123",
"signer": "GAADMIN1WALLETADDRESSFORACCESSLAYERTESTING123456789",
"signedAt": "2026-09-27T10:31:00.000Z"
}
],
"approvalCount": 1
}
],
"meta": {
"page": 1,
"limit": 20,
"totalCount": 1,
"totalPages": 1,
"hasNextPage": false,
"hasPrevPage": false
}
}
}
```

### GET /admin/proposals/:id

Get detailed information for a single multisig proposal including all signatures.

- **Auth:** Admin required
- **Response:** `200 OK`

```json
{
"success": true,
"data": {
"id": "cm123...",
"proposalId": "msig-1234567890-abc123",
"changeType": "update_fee",
"payload": { "feeBps": 500 },
"status": "pending",
"threshold": 2,
"totalSigners": 3,
"proposedBy": "GAADMIN1WALLETADDRESSFORACCESSLAYERTESTING123456789",
"proposedAt": "2026-09-27T10:30:00.000Z",
"executedAt": null,
"rejectedAt": null,
"rejectedBy": null,
"rejectionReason": null,
"createdAt": "2026-09-27T10:30:00.000Z",
"updatedAt": "2026-09-27T10:30:00.000Z",
"signatures": [
{
"id": "cm456...",
"proposalId": "msig-1234567890-abc123",
"signer": "GAADMIN1WALLETADDRESSFORACCESSLAYERTESTING123456789",
"signedAt": "2026-09-27T10:31:00.000Z"
}
],
"approvalCount": 1
}
}
```

- **Error Response:** `404 Not Found` if proposal does not exist

### POST /admin/proposals/:id/sign

Submit a signature/approval for a multisig proposal. When the threshold is reached, the proposal is automatically executed.

- **Auth:** Admin required (signer must be in ADMIN_MULTISIG_WALLETS)
- **Body:**

```json
{
"signer": "GAADMIN1WALLETADDRESSFORACCESSLAYERTESTING123456789"
}
```

- **Response:** `200 OK`

```json
{
"success": true,
"data": {
"proposalId": "msig-1234567890-abc123",
"status": "executed",
"approvalCount": 2,
"threshold": 2,
"executed": true,
"signature": {
"id": "cm789...",
"proposalId": "msig-1234567890-abc123",
"signer": "GAADMIN2WALLETADDRESSFORACCESSLAYERTESTING987654321",
"signedAt": "2026-09-27T10:32:00.000Z"
}
}
}
```

- **Error Responses:**
- `400 Bad Request` if proposal is not in `pending` state
- `403 Forbidden` if signer is not authorized
- `404 Not Found` if proposal does not exist
- `409 Conflict` if signer has already signed

### POST /admin/proposals/:id/reject

Reject a multisig proposal.

- **Auth:** Admin required (rejector must be in ADMIN_MULTISIG_WALLETS)
- **Body:**

```json
{
"rejector": "GAADMIN1WALLETADDRESSFORACCESSLAYERTESTING123456789",
"reason": "Optional rejection reason"
}
```

- **Response:** `200 OK`

```json
{
"success": true,
"data": {
"proposalId": "msig-1234567890-abc123",
"status": "rejected",
"rejectedAt": "2026-09-27T10:33:00.000Z",
"rejectedBy": "GAADMIN1WALLETADDRESSFORACCESSLAYERTESTING123456789",
"rejectionReason": "Optional rejection reason"
}
}
```

- **Error Responses:**
- `400 Bad Request` if proposal is not in `pending` state
- `403 Forbidden` if rejector is not authorized
- `404 Not Found` if proposal does not exist

### PATCH /admin/creators/:id/metadata

Update creator metadata.
Expand Down Expand Up @@ -340,4 +553,4 @@ window, with alert and auto-suspension state.

---

See [Local Setup](./local-setup.md) for development environment configuration.
See [Local Setup](./local-setup.md) for development environment configuration.
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
-- Multisig proposal queue for admin operations requiring multi-sig approval.
-- Supports configurable threshold (default 2-of-3) with signature tracking.

CREATE TABLE "multisig_proposals" (
"id" TEXT NOT NULL,
"proposalId" TEXT NOT NULL,
"changeType" TEXT NOT NULL,
"payload" JSONB NOT NULL,
"status" TEXT NOT NULL DEFAULT 'pending',
"threshold" INTEGER NOT NULL DEFAULT 2,
"totalSigners" INTEGER NOT NULL DEFAULT 3,
"proposedBy" TEXT NOT NULL,
"proposedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"executedAt" TIMESTAMP(3),
"rejectedAt" TIMESTAMP(3),
"rejectedBy" TEXT,
"rejectionReason" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,

CONSTRAINT "multisig_proposals_pkey" PRIMARY KEY ("id")
);

CREATE UNIQUE INDEX "multisig_proposals_proposalId_key" ON "multisig_proposals"("proposalId");
CREATE INDEX "multisig_proposals_status_idx" ON "multisig_proposals"("status");
CREATE INDEX "multisig_proposals_proposedBy_idx" ON "multisig_proposals"("proposedBy");

CREATE TABLE "multisig_signatures" (
"id" TEXT NOT NULL,
"proposalId" TEXT NOT NULL,
"signer" TEXT NOT NULL,
"signedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,

CONSTRAINT "multisig_signatures_pkey" PRIMARY KEY ("id")
);

CREATE UNIQUE INDEX "multisig_signatures_proposalId_signer_key" ON "multisig_signatures"("proposalId", "signer");
CREATE INDEX "multisig_signatures_signer_idx" ON "multisig_signatures"("signer");
CREATE INDEX "multisig_signatures_proposalId_idx" ON "multisig_signatures"("proposalId");

ALTER TABLE "multisig_signatures"
ADD CONSTRAINT "multisig_signatures_proposalId_fkey"
FOREIGN KEY ("proposalId")
REFERENCES "multisig_proposals"("proposalId")
ON DELETE CASCADE
ON UPDATE CASCADE;
38 changes: 38 additions & 0 deletions prisma/schema/multisig.prisma
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// prisma/schema/multisig.prisma

model MultisigProposal {
id String @id @default(cuid())
proposalId String @unique
changeType String
payload Json
status String @default("pending") // pending | executed | rejected
threshold Int @default(2)
totalSigners Int @default(3)
proposedBy String
proposedAt DateTime @default(now())
executedAt DateTime?
rejectedAt DateTime?
rejectedBy String?
rejectionReason String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt

signatures MultisigSignature[]

@@index([status])
@@index([proposedBy])
@@map("multisig_proposals")
}

model MultisigSignature {
id String @id @default(cuid())
proposalId String
signer String
signedAt DateTime @default(now())

proposal MultisigProposal @relation(fields: [proposalId], references: [proposalId], onDelete: Cascade)

@@unique([proposalId, signer])
@@index([signer])
@@map("multisig_signatures")
}
Loading
Loading