@seasonkoh/webaz 0.1.8 → 0.1.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +48 -0
- package/README.md +156 -20
- package/dist/layer0-foundation/L0-1-database/schema.js +5 -4
- package/dist/layer0-foundation/L0-2-state-machine/engine.js +228 -7
- package/dist/layer0-foundation/L0-2-state-machine/order-chain.js +156 -0
- package/dist/layer0-foundation/L0-2-state-machine/transitions.js +53 -12
- package/dist/layer0-foundation/L0-5-manifest/manifest.js +14 -1
- package/dist/layer1-agent/L1-1-mcp-server/auth.js +1 -1
- package/dist/layer1-agent/L1-1-mcp-server/server.js +3543 -852
- package/dist/layer1-agent/L1-2-external-anchor/anchor-engine.js +324 -0
- package/dist/layer1-agent/L1-2-identity/agent-passport.js +100 -0
- package/dist/layer2-business/L2-6-notifications/notification-engine.js +72 -5
- package/dist/layer2-business/L2-7-snf/snf-engine.js +287 -0
- package/dist/layer2-business/L2-anchor-registry/anchor-registry.js +396 -0
- package/dist/layer2-business/L2-notes/note-photo-storage.js +133 -0
- package/dist/layer3-trust/L3-1-dispute-engine/dispute-engine.js +6 -6
- package/dist/layer3-trust/L3-1-dispute-engine/evidence-storage.js +246 -0
- package/dist/layer4-economics/L4-3-reputation/reputation-engine.js +95 -1
- package/dist/layer4-economics/L4-4-skill-market/skill-engine.js +31 -2
- package/dist/layer4-economics/L4-4-skill-market/skill-listing-engine.js +358 -0
- package/dist/pwa/public/app.js +31230 -2345
- package/dist/pwa/public/i18n.js +5282 -111
- package/dist/pwa/public/icon.svg +11 -0
- package/dist/pwa/public/index.html +4 -1
- package/dist/pwa/public/manifest.json +39 -4
- package/dist/pwa/public/openapi.json +5946 -0
- package/dist/pwa/public/style.css +278 -5
- package/dist/pwa/public/sw.js +41 -2
- package/dist/pwa/public/vendor/jsQR.js +10102 -0
- package/dist/pwa/public/webaz-logo.png +0 -0
- package/dist/pwa/routes/account-deletion.js +53 -0
- package/dist/pwa/routes/addresses.js +105 -0
- package/dist/pwa/routes/admin-admins.js +151 -0
- package/dist/pwa/routes/admin-analytics.js +253 -0
- package/dist/pwa/routes/admin-atomic.js +21 -0
- package/dist/pwa/routes/admin-catalog.js +64 -0
- package/dist/pwa/routes/admin-editor-picks.js +45 -0
- package/dist/pwa/routes/admin-events.js +60 -0
- package/dist/pwa/routes/admin-health.js +66 -0
- package/dist/pwa/routes/admin-moderation.js +120 -0
- package/dist/pwa/routes/admin-ops.js +179 -0
- package/dist/pwa/routes/admin-protocol-params.js +79 -0
- package/dist/pwa/routes/admin-reports.js +154 -0
- package/dist/pwa/routes/admin-tokenomics.js +113 -0
- package/dist/pwa/routes/admin-users-lifecycle.js +237 -0
- package/dist/pwa/routes/admin-users-query.js +390 -0
- package/dist/pwa/routes/admin-verifier-flow.js +126 -0
- package/dist/pwa/routes/admin-verifier-whitelist.js +111 -0
- package/dist/pwa/routes/admin-wallet-ops.js +66 -0
- package/dist/pwa/routes/agent-buy.js +215 -0
- package/dist/pwa/routes/agent-governance.js +341 -0
- package/dist/pwa/routes/agent-reputation.js +34 -0
- package/dist/pwa/routes/ai.js +101 -0
- package/dist/pwa/routes/analytics.js +272 -0
- package/dist/pwa/routes/anchors.js +169 -0
- package/dist/pwa/routes/announcements.js +110 -0
- package/dist/pwa/routes/arbitrator.js +117 -0
- package/dist/pwa/routes/auction.js +436 -0
- package/dist/pwa/routes/auth-login.js +40 -0
- package/dist/pwa/routes/auth-read.js +66 -0
- package/dist/pwa/routes/auth-register.js +138 -0
- package/dist/pwa/routes/auth-sessions.js +62 -0
- package/dist/pwa/routes/blocklist.js +60 -0
- package/dist/pwa/routes/buyer-feeds.js +224 -0
- package/dist/pwa/routes/cart.js +155 -0
- package/dist/pwa/routes/charity.js +816 -0
- package/dist/pwa/routes/chat.js +318 -0
- package/dist/pwa/routes/checkin-tasks.js +122 -0
- package/dist/pwa/routes/checkout-helpers.js +85 -0
- package/dist/pwa/routes/claim-initiators.js +88 -0
- package/dist/pwa/routes/claim-verify.js +615 -0
- package/dist/pwa/routes/claim-voting.js +114 -0
- package/dist/pwa/routes/claim-withdrawals.js +20 -0
- package/dist/pwa/routes/coupons.js +165 -0
- package/dist/pwa/routes/dashboards.js +99 -0
- package/dist/pwa/routes/dispute-cases.js +267 -0
- package/dist/pwa/routes/disputes-read.js +358 -0
- package/dist/pwa/routes/disputes-write.js +475 -0
- package/dist/pwa/routes/evidence.js +86 -0
- package/dist/pwa/routes/external-anchors.js +107 -0
- package/dist/pwa/routes/feedback.js +270 -0
- package/dist/pwa/routes/flash-sales.js +130 -0
- package/dist/pwa/routes/follows.js +103 -0
- package/dist/pwa/routes/group-buys.js +208 -0
- package/dist/pwa/routes/growth.js +199 -0
- package/dist/pwa/routes/import-product.js +153 -0
- package/dist/pwa/routes/kyc.js +40 -0
- package/dist/pwa/routes/leaderboard.js +149 -0
- package/dist/pwa/routes/listings.js +281 -0
- package/dist/pwa/routes/logistics.js +35 -0
- package/dist/pwa/routes/manifests.js +126 -0
- package/dist/pwa/routes/me-data.js +101 -0
- package/dist/pwa/routes/notifications.js +48 -0
- package/dist/pwa/routes/offers.js +96 -0
- package/dist/pwa/routes/orders-action.js +285 -0
- package/dist/pwa/routes/orders-create.js +339 -0
- package/dist/pwa/routes/orders-read.js +180 -0
- package/dist/pwa/routes/p2p-products.js +178 -0
- package/dist/pwa/routes/payments-governance.js +311 -0
- package/dist/pwa/routes/peers.js +34 -0
- package/dist/pwa/routes/pin-receipts.js +39 -0
- package/dist/pwa/routes/products-aliases.js +119 -0
- package/dist/pwa/routes/products-claims.js +60 -0
- package/dist/pwa/routes/products-create.js +206 -0
- package/dist/pwa/routes/products-crud.js +73 -0
- package/dist/pwa/routes/products-links.js +129 -0
- package/dist/pwa/routes/products-list.js +424 -0
- package/dist/pwa/routes/products-meta.js +155 -0
- package/dist/pwa/routes/products-update.js +125 -0
- package/dist/pwa/routes/profile-credentials.js +105 -0
- package/dist/pwa/routes/profile-identity.js +174 -0
- package/dist/pwa/routes/profile-location.js +35 -0
- package/dist/pwa/routes/profile-placement.js +70 -0
- package/dist/pwa/routes/profile-prefs.js +93 -0
- package/dist/pwa/routes/promoter.js +208 -0
- package/dist/pwa/routes/public-utils.js +170 -0
- package/dist/pwa/routes/push.js +54 -0
- package/dist/pwa/routes/ratings.js +220 -0
- package/dist/pwa/routes/recover-key.js +100 -0
- package/dist/pwa/routes/referral.js +58 -0
- package/dist/pwa/routes/reputation.js +34 -0
- package/dist/pwa/routes/returns.js +493 -0
- package/dist/pwa/routes/reviews.js +81 -0
- package/dist/pwa/routes/rfqs.js +443 -0
- package/dist/pwa/routes/search.js +172 -0
- package/dist/pwa/routes/secondhand.js +278 -0
- package/dist/pwa/routes/seller-quota.js +225 -0
- package/dist/pwa/routes/share-redirects.js +164 -0
- package/dist/pwa/routes/shareables-interactions.js +212 -0
- package/dist/pwa/routes/shareables.js +470 -0
- package/dist/pwa/routes/shops.js +98 -0
- package/dist/pwa/routes/signaling.js +43 -0
- package/dist/pwa/routes/skill-market.js +173 -0
- package/dist/pwa/routes/skills.js +174 -0
- package/dist/pwa/routes/snf.js +126 -0
- package/dist/pwa/routes/tags.js +47 -0
- package/dist/pwa/routes/trial.js +333 -0
- package/dist/pwa/routes/trusted-kpi.js +87 -0
- package/dist/pwa/routes/url-claim.js +113 -0
- package/dist/pwa/routes/users-public.js +317 -0
- package/dist/pwa/routes/variants.js +156 -0
- package/dist/pwa/routes/verifier-user.js +107 -0
- package/dist/pwa/routes/verify-tasks.js +120 -0
- package/dist/pwa/routes/waitlist.js +65 -0
- package/dist/pwa/routes/wallet-read.js +218 -0
- package/dist/pwa/routes/wallet-write.js +273 -0
- package/dist/pwa/routes/webauthn.js +188 -0
- package/dist/pwa/routes/webhooks.js +162 -0
- package/dist/pwa/routes/welcome.js +226 -0
- package/dist/pwa/routes/wishlist-qa.js +135 -0
- package/dist/pwa/security/ssrf.js +110 -0
- package/dist/pwa/server.js +9247 -2097
- package/package.json +8 -3
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
export function registerVerifyTasksRoutes(app, deps) {
|
|
2
|
+
const { db, auth, assignVerifiers, settleTask, getVerifierStats } = deps;
|
|
3
|
+
// 卖家确认:已在原平台添加验证码 → 任务进入分配池
|
|
4
|
+
app.post('/api/verify-tasks/:id/confirm', (req, res) => {
|
|
5
|
+
const user = auth(req, res);
|
|
6
|
+
if (!user)
|
|
7
|
+
return;
|
|
8
|
+
const task = db.prepare(`SELECT * FROM verify_tasks WHERE id = ? AND status IN ('code_issued','open')`).get(req.params.id);
|
|
9
|
+
if (!task)
|
|
10
|
+
return void res.json({ error: '任务不存在或已结束' });
|
|
11
|
+
const product = db.prepare('SELECT seller_id FROM products WHERE id = ?').get(task.product_id);
|
|
12
|
+
if (!product || product.seller_id !== user.id)
|
|
13
|
+
return void res.status(403).json({ error: '无权限' });
|
|
14
|
+
if (task.status === 'open') {
|
|
15
|
+
return void res.json({ success: true, already_open: true, message: '任务已在验证中,无需重复确认' });
|
|
16
|
+
}
|
|
17
|
+
db.prepare(`UPDATE verify_tasks SET status='open' WHERE id=?`).run(req.params.id);
|
|
18
|
+
try {
|
|
19
|
+
assignVerifiers(req.params.id);
|
|
20
|
+
}
|
|
21
|
+
catch { }
|
|
22
|
+
res.json({ success: true, message: '任务已提交到验证池,等待审核员确认' });
|
|
23
|
+
});
|
|
24
|
+
// 卖家:查询某商品的进行中验证任务(供编辑页展示验证码)
|
|
25
|
+
app.get('/api/verify-tasks/by-product/:productId', (req, res) => {
|
|
26
|
+
const user = auth(req, res);
|
|
27
|
+
if (!user)
|
|
28
|
+
return;
|
|
29
|
+
const product = db.prepare('SELECT seller_id FROM products WHERE id = ?').get(req.params.productId);
|
|
30
|
+
if (!product || product.seller_id !== user.id)
|
|
31
|
+
return void res.status(403).json({ error: '无权限' });
|
|
32
|
+
const tasks = db.prepare(`
|
|
33
|
+
SELECT id, type, url, code, status, expires_at, created_at,
|
|
34
|
+
(SELECT COUNT(*) FROM verify_submissions WHERE task_id = verify_tasks.id AND submitted_at IS NOT NULL) as submissions_done
|
|
35
|
+
FROM verify_tasks WHERE product_id = ? AND status IN ('code_issued','open') ORDER BY created_at DESC
|
|
36
|
+
`).all(req.params.productId);
|
|
37
|
+
res.json(tasks);
|
|
38
|
+
});
|
|
39
|
+
// 卖家:查询我发起的所有认领任务(用于"查看任务进度"页)
|
|
40
|
+
app.get('/api/verify-tasks/my-claims', (req, res) => {
|
|
41
|
+
const user = auth(req, res);
|
|
42
|
+
if (!user)
|
|
43
|
+
return;
|
|
44
|
+
const tasks = db.prepare(`
|
|
45
|
+
SELECT vt.id, vt.type, vt.url, vt.code, vt.status, vt.result,
|
|
46
|
+
vt.verifiers_needed, vt.expires_at, vt.created_at, vt.settled_at,
|
|
47
|
+
p.title as product_title, p.id as product_id,
|
|
48
|
+
(SELECT COUNT(*) FROM verify_submissions WHERE task_id=vt.id AND submitted_at IS NOT NULL) as submissions_done
|
|
49
|
+
FROM verify_tasks vt
|
|
50
|
+
JOIN products p ON vt.product_id = p.id
|
|
51
|
+
WHERE p.seller_id = ?
|
|
52
|
+
ORDER BY vt.created_at DESC
|
|
53
|
+
LIMIT 30
|
|
54
|
+
`).all(user.id);
|
|
55
|
+
res.json(tasks);
|
|
56
|
+
});
|
|
57
|
+
app.get('/api/verify-tasks/mine', (req, res) => {
|
|
58
|
+
const user = auth(req, res);
|
|
59
|
+
if (!user)
|
|
60
|
+
return;
|
|
61
|
+
const tasks = db.prepare(`
|
|
62
|
+
SELECT vt.id, vt.type, vt.url, vt.verifiers_needed, vt.reward_per_verifier, vt.expires_at,
|
|
63
|
+
vs.id as sub_id, vs.submitted_at, vs.verdict,
|
|
64
|
+
(SELECT COUNT(*) FROM verify_submissions WHERE task_id = vt.id AND submitted_at IS NOT NULL) as submissions_done
|
|
65
|
+
FROM verify_tasks vt
|
|
66
|
+
JOIN verify_submissions vs ON vs.task_id = vt.id AND vs.verifier_id = ?
|
|
67
|
+
WHERE vt.status = 'open'
|
|
68
|
+
ORDER BY vt.created_at DESC
|
|
69
|
+
`).all(user.id);
|
|
70
|
+
const stats = getVerifierStats(user.id);
|
|
71
|
+
res.json({ tasks, stats });
|
|
72
|
+
});
|
|
73
|
+
// 验证者:提交验证结果(填入式)
|
|
74
|
+
app.post('/api/verify-tasks/:id/submit', (req, res) => {
|
|
75
|
+
const user = auth(req, res);
|
|
76
|
+
if (!user)
|
|
77
|
+
return;
|
|
78
|
+
const { submission } = req.body;
|
|
79
|
+
const sub = db.prepare(`SELECT * FROM verify_submissions WHERE task_id = ? AND verifier_id = ?`)
|
|
80
|
+
.get(req.params.id, user.id);
|
|
81
|
+
if (!sub)
|
|
82
|
+
return void res.json({ error: '未分配到此任务' });
|
|
83
|
+
if (sub.submitted_at)
|
|
84
|
+
return void res.json({ error: '已提交过' });
|
|
85
|
+
const task = db.prepare('SELECT * FROM verify_tasks WHERE id = ? AND status = ?').get(req.params.id, 'open');
|
|
86
|
+
if (!task)
|
|
87
|
+
return void res.json({ error: '任务已结束或不存在' });
|
|
88
|
+
if (new Date(task.expires_at) < new Date())
|
|
89
|
+
return void res.json({ error: '任务已过期' });
|
|
90
|
+
db.prepare(`UPDATE verify_submissions SET submission=?, submitted_at=datetime('now') WHERE task_id=? AND verifier_id=?`)
|
|
91
|
+
.run((submission ?? '').trim(), req.params.id, user.id);
|
|
92
|
+
const doneCount = db.prepare(`SELECT COUNT(*) as n FROM verify_submissions WHERE task_id = ? AND submitted_at IS NOT NULL`).get(req.params.id).n;
|
|
93
|
+
if (doneCount >= task.verifiers_needed)
|
|
94
|
+
settleTask(req.params.id);
|
|
95
|
+
res.json({ success: true, message: '提交成功,等待其他验证者完成后自动结算' });
|
|
96
|
+
});
|
|
97
|
+
// 公开验证大厅 — 仅显示分配给我的未提交任务
|
|
98
|
+
app.get('/api/verify-tasks/open', (req, res) => {
|
|
99
|
+
const user = auth(req, res);
|
|
100
|
+
if (!user)
|
|
101
|
+
return;
|
|
102
|
+
const tasks = db.prepare(`
|
|
103
|
+
SELECT vt.id, vt.type, vt.url, vt.reward_per_verifier, vt.expires_at,
|
|
104
|
+
(SELECT COUNT(*) FROM verify_submissions WHERE task_id=vt.id AND submitted_at IS NOT NULL) as done,
|
|
105
|
+
vt.verifiers_needed
|
|
106
|
+
FROM verify_tasks vt
|
|
107
|
+
JOIN verify_submissions vs ON vs.task_id = vt.id AND vs.verifier_id = ? AND vs.submitted_at IS NULL
|
|
108
|
+
WHERE vt.status = 'open'
|
|
109
|
+
ORDER BY vt.created_at ASC
|
|
110
|
+
LIMIT 10
|
|
111
|
+
`).all(user.id);
|
|
112
|
+
res.json(tasks);
|
|
113
|
+
});
|
|
114
|
+
app.get('/api/verify-stats', (req, res) => {
|
|
115
|
+
const user = auth(req, res);
|
|
116
|
+
if (!user)
|
|
117
|
+
return;
|
|
118
|
+
res.json(getVerifierStats(user.id));
|
|
119
|
+
});
|
|
120
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
export function registerWaitlistRoutes(app, deps) {
|
|
2
|
+
const { db, auth, isTrustedRole, errorRes } = deps;
|
|
3
|
+
app.post('/api/products/:product_id/waitlist', (req, res) => {
|
|
4
|
+
const user = auth(req, res);
|
|
5
|
+
if (!user)
|
|
6
|
+
return;
|
|
7
|
+
if (isTrustedRole(user))
|
|
8
|
+
return void errorRes(res, 403, 'TRUSTED_ROLE_NO_TRADE', '受信角色无购物功能');
|
|
9
|
+
const p = db.prepare('SELECT id, seller_id, status, stock FROM products WHERE id = ?').get(req.params.product_id);
|
|
10
|
+
if (!p)
|
|
11
|
+
return void res.status(404).json({ error: '商品不存在' });
|
|
12
|
+
if (p.seller_id === user.id)
|
|
13
|
+
return void res.status(400).json({ error: '不可对自己商品排队' });
|
|
14
|
+
if (p.status !== 'active')
|
|
15
|
+
return void res.status(400).json({ error: '商品已下架,无法排队' });
|
|
16
|
+
if (p.stock > 0)
|
|
17
|
+
return void res.status(400).json({ error: '商品有货,无需排队 — 直接下单即可' });
|
|
18
|
+
const qty = Math.max(1, Math.min(99, Number(req.body?.desired_qty) || 1));
|
|
19
|
+
const note = req.body?.note ? String(req.body.note).slice(0, 200) : null;
|
|
20
|
+
db.prepare(`INSERT OR REPLACE INTO product_waitlist (user_id, product_id, desired_qty, note) VALUES (?,?,?,?)`)
|
|
21
|
+
.run(user.id, req.params.product_id, qty, note);
|
|
22
|
+
res.json({ success: true });
|
|
23
|
+
});
|
|
24
|
+
app.delete('/api/products/:product_id/waitlist', (req, res) => {
|
|
25
|
+
const user = auth(req, res);
|
|
26
|
+
if (!user)
|
|
27
|
+
return;
|
|
28
|
+
db.prepare('DELETE FROM product_waitlist WHERE user_id = ? AND product_id = ?').run(user.id, req.params.product_id);
|
|
29
|
+
res.json({ success: true });
|
|
30
|
+
});
|
|
31
|
+
app.get('/api/waitlist', (req, res) => {
|
|
32
|
+
const user = auth(req, res);
|
|
33
|
+
if (!user)
|
|
34
|
+
return;
|
|
35
|
+
const rows = db.prepare(`
|
|
36
|
+
SELECT w.product_id, w.desired_qty, w.note, w.notified_at, w.created_at,
|
|
37
|
+
p.title, p.price, p.stock, p.status as product_status, p.category,
|
|
38
|
+
u.name as seller_name, u.handle as seller_handle
|
|
39
|
+
FROM product_waitlist w
|
|
40
|
+
JOIN products p ON p.id = w.product_id AND p.status != 'deleted'
|
|
41
|
+
JOIN users u ON u.id = p.seller_id
|
|
42
|
+
WHERE w.user_id = ?
|
|
43
|
+
ORDER BY w.created_at DESC LIMIT 200
|
|
44
|
+
`).all(user.id);
|
|
45
|
+
res.json({ items: rows });
|
|
46
|
+
});
|
|
47
|
+
app.get('/api/products/:product_id/waitlist/check', (req, res) => {
|
|
48
|
+
const user = auth(req, res);
|
|
49
|
+
if (!user)
|
|
50
|
+
return;
|
|
51
|
+
const exists = db.prepare('SELECT 1 FROM product_waitlist WHERE user_id = ? AND product_id = ?').get(user.id, req.params.product_id);
|
|
52
|
+
res.json({ in_waitlist: !!exists });
|
|
53
|
+
});
|
|
54
|
+
// seller 查 waitlist count(决定备多少货)
|
|
55
|
+
app.get('/api/products/:product_id/waitlist/count', (req, res) => {
|
|
56
|
+
const user = auth(req, res);
|
|
57
|
+
if (!user)
|
|
58
|
+
return;
|
|
59
|
+
const p = db.prepare('SELECT seller_id FROM products WHERE id = ?').get(req.params.product_id);
|
|
60
|
+
if (!p || p.seller_id !== user.id)
|
|
61
|
+
return void res.status(403).json({ error: '仅卖家可看' });
|
|
62
|
+
const r = db.prepare(`SELECT COUNT(*) as cnt, COALESCE(SUM(desired_qty), 0) as total_qty FROM product_waitlist WHERE product_id = ? AND notified_at IS NULL`).get(req.params.product_id);
|
|
63
|
+
res.json({ pending_users: r.cnt, total_desired: r.total_qty });
|
|
64
|
+
});
|
|
65
|
+
}
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
export function registerWalletReadRoutes(app, deps) {
|
|
2
|
+
const { db, auth, isTrustedRole, generateId, verifyPassword, deriveDepositAddress, getProtocolParam, getPublicClient, getIsMainnet, getActiveChainId, getUsdcContract, getNetwork } = deps;
|
|
3
|
+
// 钱包状态
|
|
4
|
+
app.get('/api/wallet', (req, res) => {
|
|
5
|
+
const user = auth(req, res);
|
|
6
|
+
if (!user)
|
|
7
|
+
return;
|
|
8
|
+
if (isTrustedRole(user))
|
|
9
|
+
return void res.status(403).json({ error: '受信角色无钱包', error_code: 'TRUSTED_ROLE_NO_WALLET' });
|
|
10
|
+
const wallet = db.prepare('SELECT * FROM wallets WHERE user_id = ?').get(user.id);
|
|
11
|
+
if (!wallet)
|
|
12
|
+
return void res.status(500).json({ error: '钱包记录缺失', error_code: 'WALLET_MISSING' });
|
|
13
|
+
if (!wallet.deposit_address) {
|
|
14
|
+
const addr = deriveDepositAddress(user.id);
|
|
15
|
+
db.prepare('UPDATE wallets SET deposit_address = ? WHERE user_id = ?').run(addr, user.id);
|
|
16
|
+
wallet.deposit_address = addr;
|
|
17
|
+
}
|
|
18
|
+
res.json(wallet);
|
|
19
|
+
});
|
|
20
|
+
// 充值地址 QR — SVG(轻量 + 矢量,移动端扫码体验最佳)
|
|
21
|
+
app.get('/api/wallet/deposit-qr', async (req, res) => {
|
|
22
|
+
const user = auth(req, res);
|
|
23
|
+
if (!user)
|
|
24
|
+
return;
|
|
25
|
+
const wallet = db.prepare('SELECT deposit_address FROM wallets WHERE user_id = ?').get(user.id);
|
|
26
|
+
if (!wallet?.deposit_address)
|
|
27
|
+
return void res.status(404).json({ error: '充值地址未生成' });
|
|
28
|
+
try {
|
|
29
|
+
const { toString } = await import('qrcode');
|
|
30
|
+
// 裸地址 + 客户端按需加 hint;EIP-681 兼容性参差
|
|
31
|
+
const payload = wallet.deposit_address;
|
|
32
|
+
const svg = await toString(payload, { type: 'svg', errorCorrectionLevel: 'M', margin: 1, width: 240 });
|
|
33
|
+
res.setHeader('Content-Type', 'image/svg+xml');
|
|
34
|
+
res.setHeader('Cache-Control', 'private, max-age=86400');
|
|
35
|
+
res.send(svg);
|
|
36
|
+
}
|
|
37
|
+
catch (e) {
|
|
38
|
+
res.status(500).json({ error: 'QR 生成失败:' + e.message });
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
// 公开汇率
|
|
42
|
+
app.get('/api/wallet/rate', (_req, res) => {
|
|
43
|
+
res.json({
|
|
44
|
+
waz_usdc_rate: getProtocolParam('waz_usdc_rate', 1.0),
|
|
45
|
+
min_deposit_usdc: getProtocolParam('usdc_min_deposit', 0.01),
|
|
46
|
+
min_withdraw_waz: getProtocolParam('usdc_min_withdraw_waz', 10),
|
|
47
|
+
required_confirmations: getProtocolParam('usdc_required_confirmations', 12),
|
|
48
|
+
chain: getIsMainnet() ? 'base-mainnet' : 'base-sepolia',
|
|
49
|
+
chain_id: getActiveChainId(),
|
|
50
|
+
usdc_contract: getUsdcContract(),
|
|
51
|
+
network: getNetwork(),
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
// 白名单 GET / POST / DELETE
|
|
55
|
+
app.get('/api/wallet/whitelist', (req, res) => {
|
|
56
|
+
const user = auth(req, res);
|
|
57
|
+
if (!user)
|
|
58
|
+
return;
|
|
59
|
+
const rows = db.prepare(`
|
|
60
|
+
SELECT id, address, label, added_at, activates_at
|
|
61
|
+
FROM withdrawal_whitelist
|
|
62
|
+
WHERE user_id = ? AND revoked_at IS NULL
|
|
63
|
+
ORDER BY added_at DESC
|
|
64
|
+
`).all(user.id);
|
|
65
|
+
const now = Date.now();
|
|
66
|
+
res.json({
|
|
67
|
+
whitelist: rows.map(r => ({
|
|
68
|
+
...r,
|
|
69
|
+
activated: new Date(r.activates_at.replace(' ', 'T') + 'Z').getTime() <= now,
|
|
70
|
+
})),
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
app.post('/api/wallet/whitelist', (req, res) => {
|
|
74
|
+
const user = auth(req, res);
|
|
75
|
+
if (!user)
|
|
76
|
+
return;
|
|
77
|
+
const pwd = String(req.body?.password || '');
|
|
78
|
+
if (!user.password_hash)
|
|
79
|
+
return void res.json({ error: '请先设置登录密码(敏感操作必需)' });
|
|
80
|
+
if (!verifyPassword(pwd, user.password_hash))
|
|
81
|
+
return void res.json({ error: '密码错误' });
|
|
82
|
+
const addressRaw = String(req.body?.address || '').trim();
|
|
83
|
+
const label = String(req.body?.label || '').trim().slice(0, 50);
|
|
84
|
+
if (!/^0x[0-9a-fA-F]{40}$/.test(addressRaw))
|
|
85
|
+
return void res.json({ error: '请输入有效的以太坊地址' });
|
|
86
|
+
// P0-1: 统一小写存储
|
|
87
|
+
const address = addressRaw.toLowerCase();
|
|
88
|
+
const existing = db.prepare(`SELECT id, revoked_at FROM withdrawal_whitelist WHERE user_id = ? AND address = ?`).get(user.id, address);
|
|
89
|
+
if (existing && !existing.revoked_at)
|
|
90
|
+
return void res.json({ error: '该地址已在白名单中' });
|
|
91
|
+
const id = generateId('wl');
|
|
92
|
+
const activatesAt = new Date(Date.now() + 24 * 3600_000).toISOString().slice(0, 19).replace('T', ' ');
|
|
93
|
+
if (existing) {
|
|
94
|
+
db.prepare(`UPDATE withdrawal_whitelist
|
|
95
|
+
SET revoked_at = NULL, added_at = datetime('now'), activates_at = ?, label = ?, id = ?
|
|
96
|
+
WHERE user_id = ? AND address = ?`)
|
|
97
|
+
.run(activatesAt, label || null, id, user.id, address);
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
db.prepare(`INSERT INTO withdrawal_whitelist (id, user_id, address, label, activates_at)
|
|
101
|
+
VALUES (?,?,?,?,?)`).run(id, user.id, address, label || null, activatesAt);
|
|
102
|
+
}
|
|
103
|
+
res.json({ ok: true, id, activates_at: activatesAt, message: '地址已添加,24 小时冷却期后可用于提现' });
|
|
104
|
+
});
|
|
105
|
+
app.delete('/api/wallet/whitelist/:id', (req, res) => {
|
|
106
|
+
const user = auth(req, res);
|
|
107
|
+
if (!user)
|
|
108
|
+
return;
|
|
109
|
+
const row = db.prepare(`SELECT user_id FROM withdrawal_whitelist WHERE id = ?`).get(req.params.id);
|
|
110
|
+
if (!row || row.user_id !== user.id)
|
|
111
|
+
return void res.status(404).json({ error: '地址不存在' });
|
|
112
|
+
db.prepare(`UPDATE withdrawal_whitelist SET revoked_at = datetime('now') WHERE id = ?`).run(req.params.id);
|
|
113
|
+
res.json({ ok: true });
|
|
114
|
+
});
|
|
115
|
+
// 我的提现记录
|
|
116
|
+
app.get('/api/wallet/withdrawals', (req, res) => {
|
|
117
|
+
const user = auth(req, res);
|
|
118
|
+
if (!user)
|
|
119
|
+
return;
|
|
120
|
+
const list = db.prepare(`SELECT id, to_address, amount, status, created_at, tx_hash FROM withdrawal_requests WHERE user_id = ? ORDER BY created_at DESC LIMIT 10`).all(user.id);
|
|
121
|
+
res.json(list);
|
|
122
|
+
});
|
|
123
|
+
// 我的充值记录(含确认进度)
|
|
124
|
+
// P1-2: latestBlock 30s 缓存,多用户访问不重复打 RPC
|
|
125
|
+
let _latestBlockCache = null;
|
|
126
|
+
const getCachedLatestBlock = async () => {
|
|
127
|
+
const now = Date.now();
|
|
128
|
+
if (_latestBlockCache && _latestBlockCache.expiresAt > now)
|
|
129
|
+
return _latestBlockCache.value;
|
|
130
|
+
try {
|
|
131
|
+
const v = Number(await getPublicClient().getBlockNumber());
|
|
132
|
+
_latestBlockCache = { value: v, expiresAt: now + 30_000 };
|
|
133
|
+
return v;
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
return _latestBlockCache?.value ?? null;
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
app.get('/api/wallet/deposits', async (req, res) => {
|
|
140
|
+
const user = auth(req, res);
|
|
141
|
+
if (!user)
|
|
142
|
+
return;
|
|
143
|
+
const list = db.prepare(`SELECT tx_hash, amount, credited_waz, block_number, swept, confirmed_at, block_at_seen, created_at
|
|
144
|
+
FROM deposit_txns WHERE user_id = ? ORDER BY created_at DESC LIMIT 10`).all(user.id);
|
|
145
|
+
const requiredConf = getProtocolParam('usdc_required_confirmations', 12);
|
|
146
|
+
let latestBlock = null;
|
|
147
|
+
if (list.some(r => !r.confirmed_at)) {
|
|
148
|
+
latestBlock = await getCachedLatestBlock();
|
|
149
|
+
}
|
|
150
|
+
const enriched = list.map(r => {
|
|
151
|
+
const pending = !r.confirmed_at;
|
|
152
|
+
const confs = pending && latestBlock != null ? Math.max(0, latestBlock - r.block_number) : null;
|
|
153
|
+
return {
|
|
154
|
+
...r,
|
|
155
|
+
status: r.confirmed_at ? 'confirmed' : 'pending',
|
|
156
|
+
confirmations: confs,
|
|
157
|
+
required_confirmations: requiredConf,
|
|
158
|
+
};
|
|
159
|
+
});
|
|
160
|
+
res.json(enriched);
|
|
161
|
+
});
|
|
162
|
+
// 收入构成:分享 / 双轨对碰 / 销售
|
|
163
|
+
app.get('/api/wallet/income', (req, res) => {
|
|
164
|
+
const user = auth(req, res);
|
|
165
|
+
if (!user)
|
|
166
|
+
return;
|
|
167
|
+
const commByLevel = db.prepare(`
|
|
168
|
+
SELECT level, COUNT(*) as cnt, COALESCE(SUM(amount),0) as total
|
|
169
|
+
FROM commission_records WHERE beneficiary_id = ? GROUP BY level
|
|
170
|
+
`).all(user.id);
|
|
171
|
+
const commMap = { l1: { count: 0, total: 0 }, l2: { count: 0, total: 0 }, l3: { count: 0, total: 0 } };
|
|
172
|
+
for (const r of commByLevel) {
|
|
173
|
+
const key = `l${r.level}`;
|
|
174
|
+
if (commMap[key])
|
|
175
|
+
commMap[key] = { count: r.cnt, total: Number(r.total.toFixed(2)) };
|
|
176
|
+
}
|
|
177
|
+
const binary = db.prepare(`
|
|
178
|
+
SELECT
|
|
179
|
+
COUNT(CASE WHEN settled_at IS NOT NULL THEN 1 END) as settled_cnt,
|
|
180
|
+
COALESCE(SUM(CASE WHEN settled_at IS NOT NULL THEN waz_amount END), 0) as settled_waz,
|
|
181
|
+
COALESCE(SUM(CASE WHEN settled_at IS NULL THEN score END), 0) as pending_score
|
|
182
|
+
FROM binary_score_records WHERE user_id = ?
|
|
183
|
+
`).get(user.id);
|
|
184
|
+
const sales = db.prepare(`
|
|
185
|
+
SELECT COUNT(*) as cnt, COALESCE(SUM(total_amount),0) as total
|
|
186
|
+
FROM orders WHERE seller_id = ? AND status = 'completed'
|
|
187
|
+
`).get(user.id);
|
|
188
|
+
const totalIncome = commMap.l1.total + commMap.l2.total + commMap.l3.total +
|
|
189
|
+
Number(binary.settled_waz) + Number(sales.total);
|
|
190
|
+
res.json({
|
|
191
|
+
commissions: commMap,
|
|
192
|
+
binary: {
|
|
193
|
+
settled_count: binary.settled_cnt,
|
|
194
|
+
settled_waz: Number(Number(binary.settled_waz).toFixed(2)),
|
|
195
|
+
pending_score: Number(Number(binary.pending_score).toFixed(2)),
|
|
196
|
+
},
|
|
197
|
+
sales: { count: sales.cnt, total: Number(Number(sales.total).toFixed(2)) },
|
|
198
|
+
total_income: Number(totalIncome.toFixed(2)),
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
// P0 测试充值(Phase 0 专用)
|
|
202
|
+
app.post('/api/wallet/topup', (req, res) => {
|
|
203
|
+
const user = auth(req, res);
|
|
204
|
+
if (!user)
|
|
205
|
+
return;
|
|
206
|
+
if (isTrustedRole(user))
|
|
207
|
+
return void res.status(403).json({ error: '受信角色无钱包', error_code: 'TRUSTED_ROLE_NO_WALLET' });
|
|
208
|
+
const amount = Math.min(1000, Math.max(1, Number(req.body?.amount) || 500));
|
|
209
|
+
const wallet = db.prepare('SELECT balance FROM wallets WHERE user_id = ?').get(user.id);
|
|
210
|
+
if (!wallet)
|
|
211
|
+
return void res.status(500).json({ error: '钱包记录缺失', error_code: 'WALLET_MISSING' });
|
|
212
|
+
if (wallet.balance >= 5000)
|
|
213
|
+
return void res.json({ error: '余额已达上限 5000 WAZ,无需充值' });
|
|
214
|
+
const actual = Math.min(amount, 5000 - wallet.balance);
|
|
215
|
+
db.prepare('UPDATE wallets SET balance = balance + ? WHERE user_id = ?').run(actual, user.id);
|
|
216
|
+
res.json({ success: true, added: actual, new_balance: wallet.balance + actual });
|
|
217
|
+
});
|
|
218
|
+
}
|