@lobstercove/lichen-sdk 1.0.0
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/README.md +50 -0
- package/dist/bincode.d.ts +5 -0
- package/dist/bincode.js +91 -0
- package/dist/bountyboard.d.ts +57 -0
- package/dist/bountyboard.js +205 -0
- package/dist/connection.d.ts +482 -0
- package/dist/connection.js +813 -0
- package/dist/index.d.ts +32 -0
- package/dist/index.js +27 -0
- package/dist/keypair.d.ts +40 -0
- package/dist/keypair.js +69 -0
- package/dist/lichenid.d.ts +271 -0
- package/dist/lichenid.js +628 -0
- package/dist/lichenswap.d.ts +79 -0
- package/dist/lichenswap.js +311 -0
- package/dist/pq.d.ts +57 -0
- package/dist/pq.js +178 -0
- package/dist/publickey.d.ts +35 -0
- package/dist/publickey.js +71 -0
- package/dist/sporepay.d.ts +57 -0
- package/dist/sporepay.js +208 -0
- package/dist/sporevault.d.ts +43 -0
- package/dist/sporevault.js +176 -0
- package/dist/thalllend.d.ts +51 -0
- package/dist/thalllend.js +206 -0
- package/dist/transaction.d.ts +100 -0
- package/dist/transaction.js +202 -0
- package/package.json +56 -0
|
@@ -0,0 +1,813 @@
|
|
|
1
|
+
// Lichen SDK - Connection Class
|
|
2
|
+
import WebSocket from 'ws';
|
|
3
|
+
import { createHash } from 'crypto';
|
|
4
|
+
import { TransactionBuilder } from './transaction.js';
|
|
5
|
+
import { encodeTransaction, hexToBytes, bytesToHex } from './bincode.js';
|
|
6
|
+
/** SHA-256 hash as Uint8Array */
|
|
7
|
+
function sha256(data) {
|
|
8
|
+
return new Uint8Array(createHash('sha256').update(data).digest());
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* RPC/WebSocket connection to Lichen
|
|
12
|
+
*/
|
|
13
|
+
export class Connection {
|
|
14
|
+
constructor(rpcUrl, wsUrl, options) {
|
|
15
|
+
this.subscriptions = new Map();
|
|
16
|
+
this.nextId = 1;
|
|
17
|
+
this.rpcUrl = rpcUrl;
|
|
18
|
+
this.wsUrl = wsUrl;
|
|
19
|
+
this.timeoutMs = options?.timeoutMs ?? 30000;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Make an RPC call
|
|
23
|
+
*/
|
|
24
|
+
async rpc(method, params = []) {
|
|
25
|
+
const controller = new AbortController();
|
|
26
|
+
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
27
|
+
let response;
|
|
28
|
+
try {
|
|
29
|
+
response = await fetch(this.rpcUrl, {
|
|
30
|
+
method: 'POST',
|
|
31
|
+
headers: { 'Content-Type': 'application/json' },
|
|
32
|
+
body: JSON.stringify({
|
|
33
|
+
jsonrpc: '2.0',
|
|
34
|
+
id: this.nextId++,
|
|
35
|
+
method,
|
|
36
|
+
params,
|
|
37
|
+
}),
|
|
38
|
+
signal: controller.signal,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
catch (err) {
|
|
42
|
+
clearTimeout(timer);
|
|
43
|
+
if (err.name === 'AbortError') {
|
|
44
|
+
throw new Error(`RPC request timed out after ${this.timeoutMs}ms: ${method}`);
|
|
45
|
+
}
|
|
46
|
+
throw err;
|
|
47
|
+
}
|
|
48
|
+
finally {
|
|
49
|
+
clearTimeout(timer);
|
|
50
|
+
}
|
|
51
|
+
if (!response.ok) {
|
|
52
|
+
const text = await response.text().catch(() => '');
|
|
53
|
+
throw new Error(`RPC HTTP ${response.status}: ${text}`);
|
|
54
|
+
}
|
|
55
|
+
const data = await response.json();
|
|
56
|
+
if (data.error) {
|
|
57
|
+
throw new Error(`RPC Error: ${data.error.message}`);
|
|
58
|
+
}
|
|
59
|
+
return data.result;
|
|
60
|
+
}
|
|
61
|
+
// ============================================================================
|
|
62
|
+
// BASIC QUERIES
|
|
63
|
+
// ============================================================================
|
|
64
|
+
/**
|
|
65
|
+
* Get account balance
|
|
66
|
+
*/
|
|
67
|
+
async getBalance(pubkey) {
|
|
68
|
+
return this.rpc('getBalance', [pubkey.toBase58()]);
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Get account information
|
|
72
|
+
*/
|
|
73
|
+
async getAccount(pubkey) {
|
|
74
|
+
return this.rpc('getAccount', [pubkey.toBase58()]);
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Get block by slot number
|
|
78
|
+
*/
|
|
79
|
+
async getBlock(slot) {
|
|
80
|
+
return this.rpc('getBlock', [slot]);
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Get latest block
|
|
84
|
+
*/
|
|
85
|
+
async getLatestBlock() {
|
|
86
|
+
return this.rpc('getLatestBlock');
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Get current slot
|
|
90
|
+
*/
|
|
91
|
+
async getSlot() {
|
|
92
|
+
const result = await this.rpc('getSlot');
|
|
93
|
+
return typeof result === 'number' ? result : result.slot;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Get recent blockhash for transactions
|
|
97
|
+
*/
|
|
98
|
+
async getRecentBlockhash() {
|
|
99
|
+
const result = await this.rpc('getRecentBlockhash');
|
|
100
|
+
return typeof result === 'string' ? result : result.blockhash;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Get transaction by signature.
|
|
104
|
+
* For contract calls, response includes: return_code (u32), return_data (base64), contract_logs (string[]).
|
|
105
|
+
*/
|
|
106
|
+
async getTransaction(signature) {
|
|
107
|
+
return this.rpc('getTransaction', [signature]);
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Get a Merkle inclusion proof for a transaction by its signature.
|
|
111
|
+
*/
|
|
112
|
+
async getTransactionProof(signature) {
|
|
113
|
+
return this.rpc('getTransactionProof', [signature]);
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Verify a Merkle inclusion proof for a transaction against a root.
|
|
117
|
+
* Uses SHA-256 with domain-separated leaf (0x00) and internal (0x01) nodes.
|
|
118
|
+
*
|
|
119
|
+
* This is a pure static function — no RPC call needed.
|
|
120
|
+
*/
|
|
121
|
+
static verifyTransactionProof(root, txHash, proof) {
|
|
122
|
+
// Domain-separated leaf: SHA256(0x00 || tx_hash_bytes)
|
|
123
|
+
const leafData = new Uint8Array(33);
|
|
124
|
+
leafData[0] = 0x00;
|
|
125
|
+
leafData.set(hexToBytes(txHash), 1);
|
|
126
|
+
let current = sha256(leafData);
|
|
127
|
+
for (const step of proof) {
|
|
128
|
+
const sibling = hexToBytes(step.hash);
|
|
129
|
+
const nodeData = new Uint8Array(65);
|
|
130
|
+
nodeData[0] = 0x01; // internal node domain tag
|
|
131
|
+
if (step.direction === 'left') {
|
|
132
|
+
nodeData.set(sibling, 1);
|
|
133
|
+
nodeData.set(current, 33);
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
nodeData.set(current, 1);
|
|
137
|
+
nodeData.set(sibling, 33);
|
|
138
|
+
}
|
|
139
|
+
current = sha256(nodeData);
|
|
140
|
+
}
|
|
141
|
+
return bytesToHex(current) === root;
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Send transaction
|
|
145
|
+
*/
|
|
146
|
+
async sendTransaction(transaction) {
|
|
147
|
+
const txBytes = encodeTransaction(transaction);
|
|
148
|
+
const txBase64 = Buffer.from(txBytes).toString('base64');
|
|
149
|
+
const result = await this.rpc('sendTransaction', [txBase64]);
|
|
150
|
+
return typeof result === 'string' ? result : result.signature;
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Get total burned LICN
|
|
154
|
+
*/
|
|
155
|
+
async getTotalBurned() {
|
|
156
|
+
return this.rpc('getTotalBurned');
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Get all validators
|
|
160
|
+
*/
|
|
161
|
+
async getValidators() {
|
|
162
|
+
const result = await this.rpc('getValidators');
|
|
163
|
+
return result.validators;
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Get performance metrics
|
|
167
|
+
*/
|
|
168
|
+
async getMetrics() {
|
|
169
|
+
return this.rpc('getMetrics');
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Health check
|
|
173
|
+
*/
|
|
174
|
+
async health() {
|
|
175
|
+
return this.rpc('health');
|
|
176
|
+
}
|
|
177
|
+
// ============================================================================
|
|
178
|
+
// NETWORK ENDPOINTS
|
|
179
|
+
// ============================================================================
|
|
180
|
+
/**
|
|
181
|
+
* Get connected peers
|
|
182
|
+
*/
|
|
183
|
+
async getPeers() {
|
|
184
|
+
const result = await this.rpc('getPeers');
|
|
185
|
+
return result.peers;
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Get network information
|
|
189
|
+
*/
|
|
190
|
+
async getNetworkInfo() {
|
|
191
|
+
return this.rpc('getNetworkInfo');
|
|
192
|
+
}
|
|
193
|
+
// ============================================================================
|
|
194
|
+
// VALIDATOR ENDPOINTS
|
|
195
|
+
// ============================================================================
|
|
196
|
+
/**
|
|
197
|
+
* Get detailed validator information
|
|
198
|
+
*/
|
|
199
|
+
async getValidatorInfo(pubkey) {
|
|
200
|
+
return this.rpc('getValidatorInfo', [pubkey.toBase58()]);
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Get validator performance metrics
|
|
204
|
+
*/
|
|
205
|
+
async getValidatorPerformance(pubkey) {
|
|
206
|
+
return this.rpc('getValidatorPerformance', [pubkey.toBase58()]);
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Get comprehensive chain status
|
|
210
|
+
*/
|
|
211
|
+
async getChainStatus() {
|
|
212
|
+
return this.rpc('getChainStatus');
|
|
213
|
+
}
|
|
214
|
+
// ============================================================================
|
|
215
|
+
// STAKING ENDPOINTS
|
|
216
|
+
// ============================================================================
|
|
217
|
+
/**
|
|
218
|
+
* Create stake transaction
|
|
219
|
+
*/
|
|
220
|
+
async stake(from, validator, amount) {
|
|
221
|
+
const blockhash = await this.getRecentBlockhash();
|
|
222
|
+
const instruction = TransactionBuilder.stake(from.pubkey(), validator, amount);
|
|
223
|
+
const transaction = new TransactionBuilder()
|
|
224
|
+
.add(instruction)
|
|
225
|
+
.setRecentBlockhash(blockhash)
|
|
226
|
+
.buildAndSign(from);
|
|
227
|
+
return this.sendTransaction(transaction);
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Create unstake transaction
|
|
231
|
+
*/
|
|
232
|
+
async unstake(from, validator, amount) {
|
|
233
|
+
const blockhash = await this.getRecentBlockhash();
|
|
234
|
+
const instruction = TransactionBuilder.unstake(from.pubkey(), validator, amount);
|
|
235
|
+
const transaction = new TransactionBuilder()
|
|
236
|
+
.add(instruction)
|
|
237
|
+
.setRecentBlockhash(blockhash)
|
|
238
|
+
.buildAndSign(from);
|
|
239
|
+
return this.sendTransaction(transaction);
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Get staking status
|
|
243
|
+
*/
|
|
244
|
+
async getStakingStatus(pubkey) {
|
|
245
|
+
return this.rpc('getStakingStatus', [pubkey.toBase58()]);
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* Get staking rewards
|
|
249
|
+
*/
|
|
250
|
+
async getStakingRewards(pubkey) {
|
|
251
|
+
return this.rpc('getStakingRewards', [pubkey.toBase58()]);
|
|
252
|
+
}
|
|
253
|
+
// ============================================================================
|
|
254
|
+
// TRANSFER & CONTRACT ENDPOINTS
|
|
255
|
+
// ============================================================================
|
|
256
|
+
/**
|
|
257
|
+
* Transfer native LICN (spores) from one account to another.
|
|
258
|
+
*
|
|
259
|
+
* @param from - Sender keypair (signer)
|
|
260
|
+
* @param to - Recipient public key
|
|
261
|
+
* @param amount - Amount in spores (1 LICN = 1,000,000,000 spores)
|
|
262
|
+
* @returns Transaction signature
|
|
263
|
+
*/
|
|
264
|
+
async transfer(from, to, amount) {
|
|
265
|
+
const blockhash = await this.getRecentBlockhash();
|
|
266
|
+
const instruction = TransactionBuilder.transfer(from.pubkey(), to, amount);
|
|
267
|
+
const transaction = new TransactionBuilder()
|
|
268
|
+
.add(instruction)
|
|
269
|
+
.setRecentBlockhash(blockhash)
|
|
270
|
+
.buildAndSign(from);
|
|
271
|
+
return this.sendTransaction(transaction);
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* Deploy a WASM smart contract.
|
|
275
|
+
*
|
|
276
|
+
* @param deployer - Deployer keypair (signer, pays deploy fee)
|
|
277
|
+
* @param code - WASM bytecode (must start with \\0asm magic, max 512 KB)
|
|
278
|
+
* @param initData - Optional initialization data passed to contract init
|
|
279
|
+
* @returns Transaction signature
|
|
280
|
+
*/
|
|
281
|
+
async deployContract(deployer, code, initData = new Uint8Array(0)) {
|
|
282
|
+
const blockhash = await this.getRecentBlockhash();
|
|
283
|
+
const instruction = TransactionBuilder.deployContract(deployer.pubkey(), code, initData);
|
|
284
|
+
const transaction = new TransactionBuilder()
|
|
285
|
+
.add(instruction)
|
|
286
|
+
.setRecentBlockhash(blockhash)
|
|
287
|
+
.buildAndSign(deployer);
|
|
288
|
+
return this.sendTransaction(transaction);
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* Call a function on a deployed WASM smart contract.
|
|
292
|
+
*
|
|
293
|
+
* @param caller - Caller keypair (signer)
|
|
294
|
+
* @param contract - Contract account public key
|
|
295
|
+
* @param functionName - Name of the contract function to invoke
|
|
296
|
+
* @param args - Serialized function arguments (default: empty)
|
|
297
|
+
* @param value - Native LICN to send with the call in spores (default: 0)
|
|
298
|
+
* @returns Transaction signature
|
|
299
|
+
*/
|
|
300
|
+
async callContract(caller, contract, functionName, args = new Uint8Array(0), value = 0) {
|
|
301
|
+
const blockhash = await this.getRecentBlockhash();
|
|
302
|
+
const instruction = TransactionBuilder.callContract(caller.pubkey(), contract, functionName, args, value);
|
|
303
|
+
const transaction = new TransactionBuilder()
|
|
304
|
+
.add(instruction)
|
|
305
|
+
.setRecentBlockhash(blockhash)
|
|
306
|
+
.buildAndSign(caller);
|
|
307
|
+
return this.sendTransaction(transaction);
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* Upgrade a deployed WASM smart contract (owner only).
|
|
311
|
+
*
|
|
312
|
+
* @param owner - Contract owner keypair (signer)
|
|
313
|
+
* @param contract - Contract account public key
|
|
314
|
+
* @param code - New WASM bytecode
|
|
315
|
+
* @returns Transaction signature
|
|
316
|
+
*/
|
|
317
|
+
async upgradeContract(owner, contract, code) {
|
|
318
|
+
const blockhash = await this.getRecentBlockhash();
|
|
319
|
+
const instruction = TransactionBuilder.upgradeContract(owner.pubkey(), contract, code);
|
|
320
|
+
const transaction = new TransactionBuilder()
|
|
321
|
+
.add(instruction)
|
|
322
|
+
.setRecentBlockhash(blockhash)
|
|
323
|
+
.buildAndSign(owner);
|
|
324
|
+
return this.sendTransaction(transaction);
|
|
325
|
+
}
|
|
326
|
+
// ============================================================================
|
|
327
|
+
// ACCOUNT ENDPOINTS
|
|
328
|
+
// ============================================================================
|
|
329
|
+
/**
|
|
330
|
+
* Get enhanced account information
|
|
331
|
+
*/
|
|
332
|
+
async getAccountInfo(pubkey) {
|
|
333
|
+
return this.rpc('getAccountInfo', [pubkey.toBase58()]);
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* Get transaction history
|
|
337
|
+
*/
|
|
338
|
+
async getTransactionHistory(pubkey, limit = 10) {
|
|
339
|
+
return this.rpc('getTransactionHistory', [pubkey.toBase58(), limit]);
|
|
340
|
+
}
|
|
341
|
+
/**
|
|
342
|
+
* Get program accounts
|
|
343
|
+
*/
|
|
344
|
+
async getProgramAccounts(programId) {
|
|
345
|
+
const result = await this.rpc('getProgramAccounts', [programId.toBase58()]);
|
|
346
|
+
return result.accounts || [];
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* Simulate transaction (dry run)
|
|
350
|
+
*/
|
|
351
|
+
async simulateTransaction(transaction) {
|
|
352
|
+
const txBytes = encodeTransaction(transaction);
|
|
353
|
+
const txBase64 = Buffer.from(txBytes).toString('base64');
|
|
354
|
+
return this.rpc('simulateTransaction', [txBase64]);
|
|
355
|
+
}
|
|
356
|
+
/**
|
|
357
|
+
* Execute a read-only contract call without submitting a transaction.
|
|
358
|
+
*/
|
|
359
|
+
async callReadonlyContract(contractId, functionName, args = new Uint8Array(), from) {
|
|
360
|
+
const params = [contractId.toBase58(), functionName, Buffer.from(args).toString('base64')];
|
|
361
|
+
if (from) {
|
|
362
|
+
params.push(from.toBase58());
|
|
363
|
+
}
|
|
364
|
+
return this.rpc('callContract', params);
|
|
365
|
+
}
|
|
366
|
+
// ============================================================================
|
|
367
|
+
// CONTRACT ENDPOINTS
|
|
368
|
+
// ============================================================================
|
|
369
|
+
/**
|
|
370
|
+
* Get contract information
|
|
371
|
+
*/
|
|
372
|
+
async getContractInfo(contractId) {
|
|
373
|
+
return this.rpc('getContractInfo', [contractId.toBase58()]);
|
|
374
|
+
}
|
|
375
|
+
/**
|
|
376
|
+
* Get contract logs
|
|
377
|
+
*/
|
|
378
|
+
async getContractLogs(contractId) {
|
|
379
|
+
return this.rpc('getContractLogs', [contractId.toBase58()]);
|
|
380
|
+
}
|
|
381
|
+
/**
|
|
382
|
+
* Get contract ABI/IDL (machine-readable function and event interface)
|
|
383
|
+
*/
|
|
384
|
+
async getContractAbi(contractId) {
|
|
385
|
+
return this.rpc('getContractAbi', [contractId.toBase58()]);
|
|
386
|
+
}
|
|
387
|
+
/**
|
|
388
|
+
* Set/update contract ABI (owner only)
|
|
389
|
+
*/
|
|
390
|
+
async setContractAbi(contractId, abi) {
|
|
391
|
+
return this.rpc('setContractAbi', [contractId.toBase58(), abi]);
|
|
392
|
+
}
|
|
393
|
+
/**
|
|
394
|
+
* Get all deployed contracts
|
|
395
|
+
*/
|
|
396
|
+
async getAllContracts() {
|
|
397
|
+
return this.rpc('getAllContracts');
|
|
398
|
+
}
|
|
399
|
+
/**
|
|
400
|
+
* Get a symbol-registry entry.
|
|
401
|
+
*/
|
|
402
|
+
async getSymbolRegistry(symbol) {
|
|
403
|
+
return this.rpc('getSymbolRegistry', [symbol]);
|
|
404
|
+
}
|
|
405
|
+
/**
|
|
406
|
+
* Get the complete LichenID profile for an address.
|
|
407
|
+
*/
|
|
408
|
+
async getLichenIdProfile(pubkey) {
|
|
409
|
+
return this.rpc('getLichenIdProfile', [pubkey.toBase58()]);
|
|
410
|
+
}
|
|
411
|
+
/**
|
|
412
|
+
* Get the LichenID reputation summary for an address.
|
|
413
|
+
*/
|
|
414
|
+
async getLichenIdReputation(pubkey) {
|
|
415
|
+
return this.rpc('getLichenIdReputation', [pubkey.toBase58()]);
|
|
416
|
+
}
|
|
417
|
+
/**
|
|
418
|
+
* Get LichenID skills for an address.
|
|
419
|
+
*/
|
|
420
|
+
async getLichenIdSkills(pubkey) {
|
|
421
|
+
return this.rpc('getLichenIdSkills', [pubkey.toBase58()]);
|
|
422
|
+
}
|
|
423
|
+
/**
|
|
424
|
+
* Get LichenID vouches for an address.
|
|
425
|
+
*/
|
|
426
|
+
async getLichenIdVouches(pubkey) {
|
|
427
|
+
return this.rpc('getLichenIdVouches', [pubkey.toBase58()]);
|
|
428
|
+
}
|
|
429
|
+
/**
|
|
430
|
+
* Resolve a .lichen name to its owner.
|
|
431
|
+
*/
|
|
432
|
+
async resolveLichenName(name) {
|
|
433
|
+
return this.rpc('resolveLichenName', [name]);
|
|
434
|
+
}
|
|
435
|
+
/**
|
|
436
|
+
* Get premium-name auction state for a .lichen label.
|
|
437
|
+
*/
|
|
438
|
+
async getNameAuction(name) {
|
|
439
|
+
return this.rpc('getNameAuction', [name]);
|
|
440
|
+
}
|
|
441
|
+
/**
|
|
442
|
+
* Get the LichenID agent directory.
|
|
443
|
+
*/
|
|
444
|
+
async getLichenIdAgentDirectory(options = {}) {
|
|
445
|
+
return this.rpc('getLichenIdAgentDirectory', [options]);
|
|
446
|
+
}
|
|
447
|
+
/**
|
|
448
|
+
* Get aggregated LichenID statistics.
|
|
449
|
+
*/
|
|
450
|
+
async getLichenIdStats() {
|
|
451
|
+
return this.rpc('getLichenIdStats');
|
|
452
|
+
}
|
|
453
|
+
/**
|
|
454
|
+
* Get aggregated SporePay streaming statistics.
|
|
455
|
+
*/
|
|
456
|
+
async getSporePayStats() {
|
|
457
|
+
return this.rpc('getSporePayStats');
|
|
458
|
+
}
|
|
459
|
+
/**
|
|
460
|
+
* Get aggregated LichenSwap AMM statistics.
|
|
461
|
+
*/
|
|
462
|
+
async getLichenSwapStats() {
|
|
463
|
+
return this.rpc('getLichenSwapStats');
|
|
464
|
+
}
|
|
465
|
+
/**
|
|
466
|
+
* Get aggregated ThallLend lending statistics.
|
|
467
|
+
*/
|
|
468
|
+
async getThallLendStats() {
|
|
469
|
+
return this.rpc('getThallLendStats');
|
|
470
|
+
}
|
|
471
|
+
/**
|
|
472
|
+
* Get aggregated SporeVault yield-vault statistics.
|
|
473
|
+
*/
|
|
474
|
+
async getSporeVaultStats() {
|
|
475
|
+
return this.rpc('getSporeVaultStats');
|
|
476
|
+
}
|
|
477
|
+
/**
|
|
478
|
+
* Get aggregated BountyBoard marketplace statistics.
|
|
479
|
+
*/
|
|
480
|
+
async getBountyBoardStats() {
|
|
481
|
+
return this.rpc('getBountyBoardStats');
|
|
482
|
+
}
|
|
483
|
+
// ==========================================================================
|
|
484
|
+
// PROGRAM ENDPOINTS (DRAFT)
|
|
485
|
+
// ==========================================================================
|
|
486
|
+
async getProgram(programId) {
|
|
487
|
+
return this.rpc('getProgram', [programId.toBase58()]);
|
|
488
|
+
}
|
|
489
|
+
async getProgramStats(programId) {
|
|
490
|
+
return this.rpc('getProgramStats', [programId.toBase58()]);
|
|
491
|
+
}
|
|
492
|
+
async getPrograms() {
|
|
493
|
+
return this.rpc('getPrograms');
|
|
494
|
+
}
|
|
495
|
+
async getProgramCalls(programId) {
|
|
496
|
+
return this.rpc('getProgramCalls', [programId.toBase58()]);
|
|
497
|
+
}
|
|
498
|
+
async getProgramStorage(programId) {
|
|
499
|
+
return this.rpc('getProgramStorage', [programId.toBase58()]);
|
|
500
|
+
}
|
|
501
|
+
// ==========================================================================
|
|
502
|
+
// NFT ENDPOINTS (DRAFT)
|
|
503
|
+
// ==========================================================================
|
|
504
|
+
async getCollection(collectionId) {
|
|
505
|
+
return this.rpc('getCollection', [collectionId.toBase58()]);
|
|
506
|
+
}
|
|
507
|
+
async getNFT(collectionId, tokenId) {
|
|
508
|
+
return this.rpc('getNFT', [collectionId.toBase58(), tokenId]);
|
|
509
|
+
}
|
|
510
|
+
async getNFTsByOwner(owner) {
|
|
511
|
+
return this.rpc('getNFTsByOwner', [owner.toBase58()]);
|
|
512
|
+
}
|
|
513
|
+
async getNFTsByCollection(collectionId) {
|
|
514
|
+
return this.rpc('getNFTsByCollection', [collectionId.toBase58()]);
|
|
515
|
+
}
|
|
516
|
+
async getNFTActivity(collectionId, tokenId) {
|
|
517
|
+
return this.rpc('getNFTActivity', [collectionId.toBase58(), tokenId]);
|
|
518
|
+
}
|
|
519
|
+
// ============================================================================
|
|
520
|
+
// WEBSOCKET SUBSCRIPTIONS
|
|
521
|
+
// ============================================================================
|
|
522
|
+
/**
|
|
523
|
+
* Connect WebSocket
|
|
524
|
+
*/
|
|
525
|
+
async connectWs() {
|
|
526
|
+
if (!this.wsUrl) {
|
|
527
|
+
throw new Error('WebSocket URL not provided');
|
|
528
|
+
}
|
|
529
|
+
if (this.ws?.readyState === WebSocket.OPEN) {
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
return new Promise((resolve, reject) => {
|
|
533
|
+
this.ws = new WebSocket(this.wsUrl);
|
|
534
|
+
this.ws.on('open', () => {
|
|
535
|
+
resolve();
|
|
536
|
+
});
|
|
537
|
+
this.ws.on('message', (data) => {
|
|
538
|
+
// AUDIT-FIX J-1: Guard against malformed WebSocket messages
|
|
539
|
+
let msg;
|
|
540
|
+
try {
|
|
541
|
+
msg = JSON.parse(data.toString());
|
|
542
|
+
}
|
|
543
|
+
catch {
|
|
544
|
+
console.warn('Lichen WS: ignoring non-JSON message');
|
|
545
|
+
return;
|
|
546
|
+
}
|
|
547
|
+
if (msg.method === 'subscription') {
|
|
548
|
+
const { subscription, result } = msg.params;
|
|
549
|
+
const handler = this.subscriptions.get(subscription);
|
|
550
|
+
if (handler) {
|
|
551
|
+
handler(result);
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
});
|
|
555
|
+
this.ws.on('error', (error) => {
|
|
556
|
+
console.error('WebSocket error:', error);
|
|
557
|
+
reject(error);
|
|
558
|
+
});
|
|
559
|
+
});
|
|
560
|
+
}
|
|
561
|
+
/**
|
|
562
|
+
* Subscribe to method
|
|
563
|
+
*/
|
|
564
|
+
async subscribe(method, params = null) {
|
|
565
|
+
await this.connectWs();
|
|
566
|
+
return new Promise((resolve, reject) => {
|
|
567
|
+
const id = this.nextId++;
|
|
568
|
+
const messageHandler = (data) => {
|
|
569
|
+
// AUDIT-FIX J-1: Guard against malformed WebSocket messages
|
|
570
|
+
let msg;
|
|
571
|
+
try {
|
|
572
|
+
msg = JSON.parse(data.toString());
|
|
573
|
+
}
|
|
574
|
+
catch {
|
|
575
|
+
return; // skip non-JSON frames
|
|
576
|
+
}
|
|
577
|
+
if (msg.id === id) {
|
|
578
|
+
clearTimeout(timeout);
|
|
579
|
+
this.ws.off('message', messageHandler);
|
|
580
|
+
if (msg.error) {
|
|
581
|
+
reject(new Error(msg.error.message));
|
|
582
|
+
}
|
|
583
|
+
else {
|
|
584
|
+
resolve(msg.result);
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
};
|
|
588
|
+
const timeout = setTimeout(() => {
|
|
589
|
+
this.ws?.off('message', messageHandler);
|
|
590
|
+
reject(new Error('Subscription timeout'));
|
|
591
|
+
}, 5000);
|
|
592
|
+
this.ws.on('message', messageHandler);
|
|
593
|
+
this.ws.send(JSON.stringify({
|
|
594
|
+
jsonrpc: '2.0',
|
|
595
|
+
id,
|
|
596
|
+
method,
|
|
597
|
+
params,
|
|
598
|
+
}));
|
|
599
|
+
});
|
|
600
|
+
}
|
|
601
|
+
/**
|
|
602
|
+
* Unsubscribe from subscription
|
|
603
|
+
*/
|
|
604
|
+
async unsubscribe(method, subscriptionId) {
|
|
605
|
+
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
606
|
+
this.subscriptions.delete(subscriptionId);
|
|
607
|
+
return false;
|
|
608
|
+
}
|
|
609
|
+
const id = this.nextId++;
|
|
610
|
+
return new Promise((resolve, reject) => {
|
|
611
|
+
const messageHandler = (data) => {
|
|
612
|
+
// AUDIT-FIX J-1: Guard against malformed WebSocket messages
|
|
613
|
+
let msg;
|
|
614
|
+
try {
|
|
615
|
+
msg = JSON.parse(data.toString());
|
|
616
|
+
}
|
|
617
|
+
catch {
|
|
618
|
+
return; // skip non-JSON frames
|
|
619
|
+
}
|
|
620
|
+
if (msg.id === id) {
|
|
621
|
+
clearTimeout(timeout);
|
|
622
|
+
this.ws.off('message', messageHandler);
|
|
623
|
+
this.subscriptions.delete(subscriptionId);
|
|
624
|
+
if (msg.error) {
|
|
625
|
+
reject(new Error(msg.error.message));
|
|
626
|
+
}
|
|
627
|
+
else {
|
|
628
|
+
resolve(msg.result);
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
};
|
|
632
|
+
const timeout = setTimeout(() => {
|
|
633
|
+
this.ws?.off('message', messageHandler);
|
|
634
|
+
reject(new Error('Unsubscribe timeout'));
|
|
635
|
+
}, 5000);
|
|
636
|
+
this.ws.on('message', messageHandler);
|
|
637
|
+
this.ws.send(JSON.stringify({
|
|
638
|
+
jsonrpc: '2.0',
|
|
639
|
+
id,
|
|
640
|
+
method,
|
|
641
|
+
params: subscriptionId,
|
|
642
|
+
}));
|
|
643
|
+
});
|
|
644
|
+
}
|
|
645
|
+
/**
|
|
646
|
+
* Subscribe to slot updates
|
|
647
|
+
*/
|
|
648
|
+
async onSlot(callback) {
|
|
649
|
+
const subId = await this.subscribe('subscribeSlots');
|
|
650
|
+
this.subscriptions.set(subId, (data) => callback(data.slot));
|
|
651
|
+
return subId;
|
|
652
|
+
}
|
|
653
|
+
/**
|
|
654
|
+
* Unsubscribe from slots
|
|
655
|
+
*/
|
|
656
|
+
async offSlot(subscriptionId) {
|
|
657
|
+
return this.unsubscribe('unsubscribeSlots', subscriptionId);
|
|
658
|
+
}
|
|
659
|
+
/**
|
|
660
|
+
* Subscribe to block updates
|
|
661
|
+
*/
|
|
662
|
+
async onBlock(callback) {
|
|
663
|
+
const subId = await this.subscribe('subscribeBlocks');
|
|
664
|
+
this.subscriptions.set(subId, callback);
|
|
665
|
+
return subId;
|
|
666
|
+
}
|
|
667
|
+
/**
|
|
668
|
+
* Unsubscribe from blocks
|
|
669
|
+
*/
|
|
670
|
+
async offBlock(subscriptionId) {
|
|
671
|
+
return this.unsubscribe('unsubscribeBlocks', subscriptionId);
|
|
672
|
+
}
|
|
673
|
+
/**
|
|
674
|
+
* Subscribe to transaction updates
|
|
675
|
+
*/
|
|
676
|
+
async onTransaction(callback) {
|
|
677
|
+
const subId = await this.subscribe('subscribeTransactions');
|
|
678
|
+
this.subscriptions.set(subId, callback);
|
|
679
|
+
return subId;
|
|
680
|
+
}
|
|
681
|
+
/**
|
|
682
|
+
* Unsubscribe from transactions
|
|
683
|
+
*/
|
|
684
|
+
async offTransaction(subscriptionId) {
|
|
685
|
+
return this.unsubscribe('unsubscribeTransactions', subscriptionId);
|
|
686
|
+
}
|
|
687
|
+
/**
|
|
688
|
+
* Subscribe to account changes
|
|
689
|
+
*/
|
|
690
|
+
async onAccountChange(pubkey, callback) {
|
|
691
|
+
const subId = await this.subscribe('subscribeAccount', pubkey.toBase58());
|
|
692
|
+
this.subscriptions.set(subId, callback);
|
|
693
|
+
return subId;
|
|
694
|
+
}
|
|
695
|
+
/**
|
|
696
|
+
* Unsubscribe from account changes
|
|
697
|
+
*/
|
|
698
|
+
async offAccountChange(subscriptionId) {
|
|
699
|
+
return this.unsubscribe('unsubscribeAccount', subscriptionId);
|
|
700
|
+
}
|
|
701
|
+
/**
|
|
702
|
+
* Subscribe to contract logs
|
|
703
|
+
*/
|
|
704
|
+
async onLogs(callback, contractId) {
|
|
705
|
+
const params = contractId ? contractId.toBase58() : null;
|
|
706
|
+
const subId = await this.subscribe('subscribeLogs', params);
|
|
707
|
+
this.subscriptions.set(subId, callback);
|
|
708
|
+
return subId;
|
|
709
|
+
}
|
|
710
|
+
/**
|
|
711
|
+
* Unsubscribe from logs
|
|
712
|
+
*/
|
|
713
|
+
async offLogs(subscriptionId) {
|
|
714
|
+
return this.unsubscribe('unsubscribeLogs', subscriptionId);
|
|
715
|
+
}
|
|
716
|
+
/**
|
|
717
|
+
* Subscribe to program updates
|
|
718
|
+
*/
|
|
719
|
+
async onProgramUpdates(callback) {
|
|
720
|
+
const subId = await this.subscribe('subscribeProgramUpdates');
|
|
721
|
+
this.subscriptions.set(subId, callback);
|
|
722
|
+
return subId;
|
|
723
|
+
}
|
|
724
|
+
/**
|
|
725
|
+
* Unsubscribe from program updates
|
|
726
|
+
*/
|
|
727
|
+
async offProgramUpdates(subscriptionId) {
|
|
728
|
+
return this.unsubscribe('unsubscribeProgramUpdates', subscriptionId);
|
|
729
|
+
}
|
|
730
|
+
/**
|
|
731
|
+
* Subscribe to program calls
|
|
732
|
+
*/
|
|
733
|
+
async onProgramCalls(callback, programId) {
|
|
734
|
+
const params = programId ? programId.toBase58() : null;
|
|
735
|
+
const subId = await this.subscribe('subscribeProgramCalls', params);
|
|
736
|
+
this.subscriptions.set(subId, callback);
|
|
737
|
+
return subId;
|
|
738
|
+
}
|
|
739
|
+
/**
|
|
740
|
+
* Unsubscribe from program calls
|
|
741
|
+
*/
|
|
742
|
+
async offProgramCalls(subscriptionId) {
|
|
743
|
+
return this.unsubscribe('unsubscribeProgramCalls', subscriptionId);
|
|
744
|
+
}
|
|
745
|
+
/**
|
|
746
|
+
* Subscribe to NFT mints
|
|
747
|
+
*/
|
|
748
|
+
async onNftMints(callback, collectionId) {
|
|
749
|
+
const params = collectionId ? collectionId.toBase58() : null;
|
|
750
|
+
const subId = await this.subscribe('subscribeNftMints', params);
|
|
751
|
+
this.subscriptions.set(subId, callback);
|
|
752
|
+
return subId;
|
|
753
|
+
}
|
|
754
|
+
/**
|
|
755
|
+
* Unsubscribe from NFT mints
|
|
756
|
+
*/
|
|
757
|
+
async offNftMints(subscriptionId) {
|
|
758
|
+
return this.unsubscribe('unsubscribeNftMints', subscriptionId);
|
|
759
|
+
}
|
|
760
|
+
/**
|
|
761
|
+
* Subscribe to NFT transfers
|
|
762
|
+
*/
|
|
763
|
+
async onNftTransfers(callback, collectionId) {
|
|
764
|
+
const params = collectionId ? collectionId.toBase58() : null;
|
|
765
|
+
const subId = await this.subscribe('subscribeNftTransfers', params);
|
|
766
|
+
this.subscriptions.set(subId, callback);
|
|
767
|
+
return subId;
|
|
768
|
+
}
|
|
769
|
+
/**
|
|
770
|
+
* Unsubscribe from NFT transfers
|
|
771
|
+
*/
|
|
772
|
+
async offNftTransfers(subscriptionId) {
|
|
773
|
+
return this.unsubscribe('unsubscribeNftTransfers', subscriptionId);
|
|
774
|
+
}
|
|
775
|
+
/**
|
|
776
|
+
* Subscribe to marketplace listings
|
|
777
|
+
*/
|
|
778
|
+
async onMarketListings(callback) {
|
|
779
|
+
const subId = await this.subscribe('subscribeMarketListings');
|
|
780
|
+
this.subscriptions.set(subId, callback);
|
|
781
|
+
return subId;
|
|
782
|
+
}
|
|
783
|
+
/**
|
|
784
|
+
* Unsubscribe from marketplace listings
|
|
785
|
+
*/
|
|
786
|
+
async offMarketListings(subscriptionId) {
|
|
787
|
+
return this.unsubscribe('unsubscribeMarketListings', subscriptionId);
|
|
788
|
+
}
|
|
789
|
+
/**
|
|
790
|
+
* Subscribe to marketplace sales
|
|
791
|
+
*/
|
|
792
|
+
async onMarketSales(callback) {
|
|
793
|
+
const subId = await this.subscribe('subscribeMarketSales');
|
|
794
|
+
this.subscriptions.set(subId, callback);
|
|
795
|
+
return subId;
|
|
796
|
+
}
|
|
797
|
+
/**
|
|
798
|
+
* Unsubscribe from marketplace sales
|
|
799
|
+
*/
|
|
800
|
+
async offMarketSales(subscriptionId) {
|
|
801
|
+
return this.unsubscribe('unsubscribeMarketSales', subscriptionId);
|
|
802
|
+
}
|
|
803
|
+
/**
|
|
804
|
+
* Close connection
|
|
805
|
+
*/
|
|
806
|
+
close() {
|
|
807
|
+
if (this.ws) {
|
|
808
|
+
this.ws.close();
|
|
809
|
+
this.ws = undefined;
|
|
810
|
+
}
|
|
811
|
+
this.subscriptions.clear();
|
|
812
|
+
}
|
|
813
|
+
}
|