-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathsnapshot-storage.js
More file actions
207 lines (183 loc) · 7.57 KB
/
snapshot-storage.js
File metadata and controls
207 lines (183 loc) · 7.57 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
/**
* snapshot-storage.js — Universal Web3 off-chain storage via snapshot-hub
*
* Reusable across all PolyFactory ecosystem products (PolyFactory, DAOwidget, etc.)
* Configure via window.PolyFactory_ before loading this script:
*
* window.PolyFactory_ = {
* SNAPSHOT_HUB_URL: 'http://localhost:3700', // default
* SNAPSHOT_SPACE: 'polyfactory.wpmix.net', // default
* };
*
* Depends on: ethers (v6, UMD), CONFIG (from config.js), userAddress + signer (from app.js)
* All functions are non-fatal — errors are logged but never block the main UI.
*
* See: /root/snapshot-hub/INTEGRATION.md for full integration guide
*/
// ─── Config ───────────────────────────────────────────────────────────────────
function _hubUrl() {
return (
(window.PolyFactory_ && window.PolyFactory_.SNAPSHOT_HUB_URL) ||
(typeof CONFIG !== 'undefined' && CONFIG.SNAPSHOT_HUB_URL) ||
'http://localhost:3700'
).replace(/\/$/, '');
}
function _space() {
return (
(window.PolyFactory_ && window.PolyFactory_.SNAPSHOT_SPACE) ||
(typeof CONFIG !== 'undefined' && CONFIG.SNAPSHOT_SPACE) ||
'polyfactory.eth'
);
}
function _chainId() {
return String((typeof CONFIG !== 'undefined' && CONFIG.CHAIN_ID) || 97);
}
// ─── EIP-712 types (must match snapshot-hub src/writer/order.ts) ──────────────
const SNAPSHOT_ORDER_DOMAIN = { name: 'snapshot', version: '0.1.4' };
const SNAPSHOT_ORDER_TYPES = {
Order: [
{ name: 'from', type: 'address' },
{ name: 'space', type: 'string' },
{ name: 'timestamp', type: 'uint64' },
{ name: 'marketId', type: 'string' },
{ name: 'side', type: 'string' },
{ name: 'price', type: 'string' },
{ name: 'amount', type: 'string' },
{ name: 'txHash', type: 'string' },
{ name: 'network', type: 'string' },
]
};
// ─── Sign & submit ────────────────────────────────────────────────────────────
/**
* Sign an order with EIP-712 and submit to snapshot-hub.
* Call AFTER tx.wait() so txHash is available.
* Non-fatal: on any error logs to console.warn only.
*
* @param {object} signer — ethers v6 Signer (connected wallet)
* @param {string} address — signer address (from userAddress global)
* @param {object} orderData — { marketId, side, price, amount, txHash }
*/
async function snapshotSignAndSubmit(signer, address, orderData) {
try {
const hubUrl = _hubUrl();
const space = _space();
const timestamp = Math.floor(Date.now() / 1000);
const message = {
from: address,
space,
timestamp, // ethers v6 signTypedData converts to BigInt automatically for uint64
marketId: String(orderData.marketId),
side: String(orderData.side),
price: String(orderData.price),
amount: String(orderData.amount),
txHash: orderData.txHash,
network: _chainId(),
};
// ethers v6: signer.signTypedData(domain, types, value)
const sig = await signer.signTypedData(SNAPSHOT_ORDER_DOMAIN, SNAPSHOT_ORDER_TYPES, message);
const envelope = {
address,
sig,
data: { domain: SNAPSHOT_ORDER_DOMAIN, types: SNAPSHOT_ORDER_TYPES, message },
};
const resp = await fetch(`${hubUrl}/api/msg`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(envelope),
});
if (!resp.ok) {
console.warn('[snapshot-hub] submit failed:', await resp.text());
} else {
const result = await resp.json();
console.log('[snapshot-hub] order recorded:', result.id);
}
} catch (e) {
// Non-fatal — order is already confirmed on-chain
console.warn('[snapshot-hub] snapshotSignAndSubmit error:', e);
}
}
// ─── Read orders via GraphQL ──────────────────────────────────────────────────
/**
* Load signed order history from snapshot-hub for a user + market.
* Returns [] on any error.
*
* @param {string} voter — wallet address
* @param {string|number} marketId
* @param {number} [first] — max results (default 50)
*/
async function snapshotLoadOrders(voter, marketId, first = 50) {
try {
const hubUrl = _hubUrl();
const space = _space();
const query = `{
orders(
where: { voter: "${voter.toLowerCase()}", market_id: "${marketId}", space: "${space}" }
orderBy: "created"
orderDirection: desc
first: ${first}
) {
id voter created space marketId side price amount txHash network
}
}`;
const resp = await fetch(`${hubUrl}/graphql`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ query }),
});
const data = await resp.json();
return (data && data.data && data.data.orders) ? data.data.orders : [];
} catch (e) {
console.warn('[snapshot-hub] snapshotLoadOrders error:', e);
return [];
}
}
// ─── Render helpers ───────────────────────────────────────────────────────────
/**
* Render signed order history into a DOM container.
*
* @param {HTMLElement} container
* @param {Array} orders — from snapshotLoadOrders()
*/
function snapshotRenderOrders(container, orders) {
if (!orders || orders.length === 0) {
container.innerHTML = '<p class="text-muted small">No signed orders recorded yet.</p>';
return;
}
const explorerBase = (typeof CONFIG !== 'undefined' && CONFIG.BLOCK_EXPLORER) || '';
const rows = orders.map(function(o) {
const sideNum = parseInt(String(o.side));
const sideLabel = sideNum === 0
? '<span class="badge badge-yes-sm">YES</span>'
: '<span class="badge badge-no-sm">NO</span>';
const priceDisplay = (parseInt(String(o.price)) / 100).toFixed(2) + '%';
let amountDisplay = '—';
try { amountDisplay = parseFloat(ethers.formatEther(String(o.amount))).toFixed(2); } catch(e) {}
const date = new Date(o.created * 1000).toLocaleString();
const txShort = String(o.txHash).slice(0, 10) + '…';
const txLink = explorerBase
? `<a href="${explorerBase}/tx/${o.txHash}" target="_blank" rel="noopener" class="text-muted">${txShort}</a>`
: `<span class="text-muted">${txShort}</span>`;
return `<tr><td>${sideLabel}</td><td>${priceDisplay}</td><td>${amountDisplay}</td><td>${txLink}</td><td class="text-muted small">${date}</td></tr>`;
}).join('');
container.innerHTML = `
<h6 class="snapshot-orders-title">My Signed Orders</h6>
<div class="table-responsive">
<table class="table table-sm snapshot-orders-table mb-0">
<thead><tr><th>Side</th><th>Price</th><th>Amount</th><th>Tx</th><th>Time</th></tr></thead>
<tbody>${rows}</tbody>
</table>
</div>`;
}
/**
* Convenience: load + render signed orders for a market into #snapshotOrders element.
* Used by loadMarketDetail() in app.js.
*
* @param {string} address — current user address
* @param {string|number} marketId
*/
async function snapshotLoadAndRender(address, marketId) {
const container = document.getElementById('snapshotOrders');
if (!container || !address) return;
const orders = await snapshotLoadOrders(address, marketId);
snapshotRenderOrders(container, orders);
}