mirror of
https://github.com/Such-Software/smirk-backend-core.git
synced 2026-10-22 01:38:43 +02:00
feat: Grin wallet handlers (view-only, non-custodial)
Third wallet-handler slice. Authenticated, view-only proxies over the
GrinClient — the backend holds no Grin spend key; the wallet exports its
rewind_hash (a public-derived view credential) per scan, the backend forwards
it to grin-wallet's scan_rewind_hash and stores no secret. Spending happens in
the wallet; the backend only broadcasts the finalized tx.
Endpoints (under /api/v1/wallet/grin):
- POST scan {rewind_hash, start_height?} -> {outputs, total_balance,
last_pmmr_index} (incremental resume index)
- GET height -> {height}
- POST broadcast {tx} -> {ok} (node push_transaction)
- JWT-gated. rewind_hash is a view credential: GrinScanRequest omits Debug and
is skipped from every span (never logged), and is validated as 64 hex before
any grin call (applying the wallet-handler review's secret-DTO lesson).
- Feature-gated: grin disabled -> 400.
- Shared validate_hex64 in wallet/mod.rs (view_key + rewind_hash); 3 paths +
schemas + grin tag registered; spec regenerated, drift-clean.
4 L1 integration tests (auth gate on scan/height/broadcast; malformed
rewind_hash 400).
Also: dropped Debug from the xmr_wow view_key-bearing request DTOs
(ViewRequest/RegisterRequest) per the review — defense-in-depth against a
cleartext view_key dump.
This commit is contained in:
+241
@@ -584,6 +584,116 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/wallet/grin/broadcast": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"grin"
|
||||
],
|
||||
"summary": "Broadcast a finalized, signed Grin transaction.",
|
||||
"operationId": "broadcast",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/GrinBroadcastRequest"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Transaction broadcast",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/GrinBroadcastResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Grin disabled or malformed transaction"
|
||||
},
|
||||
"401": {
|
||||
"description": "Missing or invalid token"
|
||||
},
|
||||
"503": {
|
||||
"description": "Upstream node unavailable"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/wallet/grin/height": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"grin"
|
||||
],
|
||||
"summary": "Current Grin chain tip height (for confirmation counting).",
|
||||
"operationId": "height",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Chain tip height",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/GrinHeightResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Grin disabled"
|
||||
},
|
||||
"401": {
|
||||
"description": "Missing or invalid token"
|
||||
},
|
||||
"503": {
|
||||
"description": "Upstream node unavailable"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/wallet/grin/scan": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"grin"
|
||||
],
|
||||
"summary": "View-only scan for the outputs a `rewind_hash` recognizes.",
|
||||
"operationId": "scan",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/GrinScanRequest"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Recognized outputs + balance + resume index",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/GrinScanResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Grin disabled or malformed rewind_hash"
|
||||
},
|
||||
"401": {
|
||||
"description": "Missing or invalid token"
|
||||
},
|
||||
"503": {
|
||||
"description": "Upstream node unavailable"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/wallet/lws/balance": {
|
||||
"post": {
|
||||
"tags": [
|
||||
@@ -1537,6 +1647,133 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"GrinBroadcastRequest": {
|
||||
"type": "object",
|
||||
"description": "Broadcast a finalized, signed Grin transaction (built + signed by the wallet).",
|
||||
"required": [
|
||||
"tx"
|
||||
],
|
||||
"properties": {
|
||||
"tx": {
|
||||
"type": "object",
|
||||
"description": "The finalized transaction object (grin node `push_transaction` input)."
|
||||
}
|
||||
}
|
||||
},
|
||||
"GrinBroadcastResponse": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"ok"
|
||||
],
|
||||
"properties": {
|
||||
"ok": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
},
|
||||
"GrinHeightResponse": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"height"
|
||||
],
|
||||
"properties": {
|
||||
"height": {
|
||||
"type": "integer",
|
||||
"format": "int64",
|
||||
"minimum": 0
|
||||
}
|
||||
}
|
||||
},
|
||||
"GrinOutput": {
|
||||
"type": "object",
|
||||
"description": "A single output recovered by a view-only scan. Amounts are nanogrin.",
|
||||
"required": [
|
||||
"commit",
|
||||
"value",
|
||||
"height",
|
||||
"mmr_index",
|
||||
"is_coinbase",
|
||||
"lock_height"
|
||||
],
|
||||
"properties": {
|
||||
"commit": {
|
||||
"type": "string"
|
||||
},
|
||||
"height": {
|
||||
"type": "integer",
|
||||
"format": "int64",
|
||||
"minimum": 0
|
||||
},
|
||||
"is_coinbase": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"lock_height": {
|
||||
"type": "integer",
|
||||
"format": "int64",
|
||||
"minimum": 0
|
||||
},
|
||||
"mmr_index": {
|
||||
"type": "integer",
|
||||
"format": "int64",
|
||||
"minimum": 0
|
||||
},
|
||||
"value": {
|
||||
"type": "integer",
|
||||
"format": "int64",
|
||||
"minimum": 0
|
||||
}
|
||||
}
|
||||
},
|
||||
"GrinScanRequest": {
|
||||
"type": "object",
|
||||
"description": "A view-only scan request. Carries the `rewind_hash` view credential, so it\ndeliberately omits `Debug` (never logged).",
|
||||
"required": [
|
||||
"rewind_hash"
|
||||
],
|
||||
"properties": {
|
||||
"rewind_hash": {
|
||||
"type": "string",
|
||||
"description": "The wallet's `rewind_hash` (64 hex). Forwarded to grin-wallet; not stored."
|
||||
},
|
||||
"start_height": {
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
],
|
||||
"format": "int64",
|
||||
"description": "Scan from this block height (wallet birthday / last scanned). Omit for full.",
|
||||
"minimum": 0
|
||||
}
|
||||
}
|
||||
},
|
||||
"GrinScanResponse": {
|
||||
"type": "object",
|
||||
"description": "Result of a view-only scan: recognized outputs, total, and the resume index.",
|
||||
"required": [
|
||||
"outputs",
|
||||
"total_balance",
|
||||
"last_pmmr_index"
|
||||
],
|
||||
"properties": {
|
||||
"last_pmmr_index": {
|
||||
"type": "integer",
|
||||
"format": "int64",
|
||||
"description": "Resume point (`last_pmmr_index`) for the next incremental scan.",
|
||||
"minimum": 0
|
||||
},
|
||||
"outputs": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/GrinOutput"
|
||||
}
|
||||
},
|
||||
"total_balance": {
|
||||
"type": "integer",
|
||||
"format": "int64",
|
||||
"minimum": 0
|
||||
}
|
||||
}
|
||||
},
|
||||
"HealthResponse": {
|
||||
"type": "object",
|
||||
"description": "Liveness response.",
|
||||
@@ -2467,6 +2704,10 @@
|
||||
{
|
||||
"name": "xmr_wow",
|
||||
"description": "Monero/Wownero chain access via LWS (register, balance, history, unspent/random outs, submit, height, confirmations)."
|
||||
},
|
||||
{
|
||||
"name": "grin",
|
||||
"description": "Grin view-only chain access (rewind_hash scan, tip height, broadcast)."
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user