@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,311 @@
|
|
|
1
|
+
import { PublicKey } from './publickey.js';
|
|
2
|
+
const PROGRAM_SYMBOL_CANDIDATES = ['LICHENSWAP', 'lichenswap'];
|
|
3
|
+
const MAX_U64 = (1n << 64n) - 1n;
|
|
4
|
+
function normalizeAddress(value) {
|
|
5
|
+
return value instanceof PublicKey ? value : new PublicKey(value);
|
|
6
|
+
}
|
|
7
|
+
function normalizeUnsignedU64(value, fieldName) {
|
|
8
|
+
const normalized = typeof value === 'bigint'
|
|
9
|
+
? value
|
|
10
|
+
: Number.isSafeInteger(value) && value >= 0
|
|
11
|
+
? BigInt(value)
|
|
12
|
+
: null;
|
|
13
|
+
if (normalized === null || normalized < 0n || normalized > MAX_U64) {
|
|
14
|
+
throw new Error(`${fieldName} must be a u64-safe integer value`);
|
|
15
|
+
}
|
|
16
|
+
return normalized;
|
|
17
|
+
}
|
|
18
|
+
function addUnsignedU64(left, right, fieldName) {
|
|
19
|
+
const sum = normalizeUnsignedU64(left, fieldName) + normalizeUnsignedU64(right, fieldName);
|
|
20
|
+
if (sum > MAX_U64) {
|
|
21
|
+
throw new Error(`${fieldName} must be a u64-safe integer value`);
|
|
22
|
+
}
|
|
23
|
+
return sum;
|
|
24
|
+
}
|
|
25
|
+
function u32LE(value) {
|
|
26
|
+
if (!Number.isInteger(value) || value < 0 || value > 4294967295) {
|
|
27
|
+
throw new Error('u32 values must fit within 0 to 4,294,967,295');
|
|
28
|
+
}
|
|
29
|
+
const out = new Uint8Array(4);
|
|
30
|
+
new DataView(out.buffer).setUint32(0, value, true);
|
|
31
|
+
return out;
|
|
32
|
+
}
|
|
33
|
+
function u64LE(value, fieldName) {
|
|
34
|
+
const out = new Uint8Array(8);
|
|
35
|
+
new DataView(out.buffer).setBigUint64(0, normalizeUnsignedU64(value, fieldName), true);
|
|
36
|
+
return out;
|
|
37
|
+
}
|
|
38
|
+
function buildLayoutArgs(layout, chunks) {
|
|
39
|
+
const header = Uint8Array.from([0xAB, ...layout]);
|
|
40
|
+
const total = chunks.reduce((sum, chunk) => sum + chunk.length, header.length);
|
|
41
|
+
const out = new Uint8Array(total);
|
|
42
|
+
out.set(header, 0);
|
|
43
|
+
let offset = header.length;
|
|
44
|
+
for (const chunk of chunks) {
|
|
45
|
+
out.set(chunk, offset);
|
|
46
|
+
offset += chunk.length;
|
|
47
|
+
}
|
|
48
|
+
return out;
|
|
49
|
+
}
|
|
50
|
+
function encodeCreatePoolArgs(params) {
|
|
51
|
+
return buildLayoutArgs([0x20, 0x20], [
|
|
52
|
+
normalizeAddress(params.tokenA).toBytes(),
|
|
53
|
+
normalizeAddress(params.tokenB).toBytes(),
|
|
54
|
+
]);
|
|
55
|
+
}
|
|
56
|
+
function encodeAddLiquidityArgs(provider, params) {
|
|
57
|
+
return buildLayoutArgs([0x20, 0x08, 0x08, 0x08], [
|
|
58
|
+
provider.toBytes(),
|
|
59
|
+
u64LE(params.amountA, 'amountA'),
|
|
60
|
+
u64LE(params.amountB, 'amountB'),
|
|
61
|
+
u64LE(params.minLiquidity ?? 0, 'minLiquidity'),
|
|
62
|
+
]);
|
|
63
|
+
}
|
|
64
|
+
function encodeSwapArgs(params, aToB) {
|
|
65
|
+
return buildLayoutArgs([0x08, 0x08, 0x04], [
|
|
66
|
+
u64LE(params.amountIn, 'amountIn'),
|
|
67
|
+
u64LE(params.minAmountOut ?? 0, 'minAmountOut'),
|
|
68
|
+
u32LE(aToB ? 1 : 0),
|
|
69
|
+
]);
|
|
70
|
+
}
|
|
71
|
+
function encodeDirectionalSwapArgs(params) {
|
|
72
|
+
return buildLayoutArgs([0x08, 0x08], [
|
|
73
|
+
u64LE(params.amountIn, 'amountIn'),
|
|
74
|
+
u64LE(params.minAmountOut ?? 0, 'minAmountOut'),
|
|
75
|
+
]);
|
|
76
|
+
}
|
|
77
|
+
function encodeDirectionalSwapWithDeadlineArgs(params) {
|
|
78
|
+
return buildLayoutArgs([0x08, 0x08, 0x08], [
|
|
79
|
+
u64LE(params.amountIn, 'amountIn'),
|
|
80
|
+
u64LE(params.minAmountOut ?? 0, 'minAmountOut'),
|
|
81
|
+
u64LE(params.deadline, 'deadline'),
|
|
82
|
+
]);
|
|
83
|
+
}
|
|
84
|
+
function encodeQuoteArgs(amountIn, aToB) {
|
|
85
|
+
return buildLayoutArgs([0x08, 0x04], [
|
|
86
|
+
u64LE(amountIn, 'amountIn'),
|
|
87
|
+
u32LE(aToB ? 1 : 0),
|
|
88
|
+
]);
|
|
89
|
+
}
|
|
90
|
+
function encodeLiquidityBalanceArgs(provider) {
|
|
91
|
+
return buildLayoutArgs([0x20], [provider.toBytes()]);
|
|
92
|
+
}
|
|
93
|
+
function encodeAmountArgs(amount, fieldName) {
|
|
94
|
+
return buildLayoutArgs([0x08], [u64LE(amount, fieldName)]);
|
|
95
|
+
}
|
|
96
|
+
function decodeReturnData(returnData) {
|
|
97
|
+
return Uint8Array.from(Buffer.from(returnData, 'base64'));
|
|
98
|
+
}
|
|
99
|
+
function readU64(bytes, offset) {
|
|
100
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
101
|
+
return view.getBigUint64(offset, true);
|
|
102
|
+
}
|
|
103
|
+
function ensureReadonlySuccess(result, functionName, allowedReturnCodes = [0]) {
|
|
104
|
+
const code = result.returnCode ?? 0;
|
|
105
|
+
if (!allowedReturnCodes.includes(code)) {
|
|
106
|
+
throw new Error(result.error ?? `LichenSwap ${functionName} returned code ${code}`);
|
|
107
|
+
}
|
|
108
|
+
if (result.success === false && result.error) {
|
|
109
|
+
throw new Error(result.error);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
function decodeU64Return(result, functionName) {
|
|
113
|
+
ensureReadonlySuccess(result, functionName);
|
|
114
|
+
if (!result.returnData) {
|
|
115
|
+
throw new Error(`LichenSwap ${functionName} did not return payload data`);
|
|
116
|
+
}
|
|
117
|
+
const bytes = decodeReturnData(result.returnData);
|
|
118
|
+
if (bytes.length < 8) {
|
|
119
|
+
throw new Error(`LichenSwap ${functionName} payload was shorter than expected`);
|
|
120
|
+
}
|
|
121
|
+
return readU64(bytes, 0);
|
|
122
|
+
}
|
|
123
|
+
function decodePoolInfo(result) {
|
|
124
|
+
ensureReadonlySuccess(result, 'get_pool_info', [0, 1]);
|
|
125
|
+
if (!result.returnData) {
|
|
126
|
+
throw new Error('LichenSwap get_pool_info did not return pool data');
|
|
127
|
+
}
|
|
128
|
+
const bytes = decodeReturnData(result.returnData);
|
|
129
|
+
if (bytes.length < 24) {
|
|
130
|
+
throw new Error('LichenSwap get_pool_info payload was shorter than expected');
|
|
131
|
+
}
|
|
132
|
+
return {
|
|
133
|
+
reserveA: readU64(bytes, 0),
|
|
134
|
+
reserveB: readU64(bytes, 8),
|
|
135
|
+
totalLiquidity: readU64(bytes, 16),
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
function decodeVolumeTotals(result, functionName) {
|
|
139
|
+
ensureReadonlySuccess(result, functionName);
|
|
140
|
+
if (!result.returnData) {
|
|
141
|
+
throw new Error(`LichenSwap ${functionName} did not return volume data`);
|
|
142
|
+
}
|
|
143
|
+
const bytes = decodeReturnData(result.returnData);
|
|
144
|
+
if (bytes.length < 16) {
|
|
145
|
+
throw new Error(`LichenSwap ${functionName} payload was shorter than expected`);
|
|
146
|
+
}
|
|
147
|
+
return {
|
|
148
|
+
volumeA: readU64(bytes, 0),
|
|
149
|
+
volumeB: readU64(bytes, 8),
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
function decodeProtocolFees(result) {
|
|
153
|
+
ensureReadonlySuccess(result, 'get_protocol_fees');
|
|
154
|
+
if (!result.returnData) {
|
|
155
|
+
throw new Error('LichenSwap get_protocol_fees did not return fee data');
|
|
156
|
+
}
|
|
157
|
+
const bytes = decodeReturnData(result.returnData);
|
|
158
|
+
if (bytes.length < 16) {
|
|
159
|
+
throw new Error('LichenSwap get_protocol_fees payload was shorter than expected');
|
|
160
|
+
}
|
|
161
|
+
return {
|
|
162
|
+
feesA: readU64(bytes, 0),
|
|
163
|
+
feesB: readU64(bytes, 8),
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
function decodeTwapCumulatives(result) {
|
|
167
|
+
ensureReadonlySuccess(result, 'get_twap_cumulatives');
|
|
168
|
+
if (!result.returnData) {
|
|
169
|
+
throw new Error('LichenSwap get_twap_cumulatives did not return TWAP data');
|
|
170
|
+
}
|
|
171
|
+
const bytes = decodeReturnData(result.returnData);
|
|
172
|
+
if (bytes.length < 24) {
|
|
173
|
+
throw new Error('LichenSwap get_twap_cumulatives payload was shorter than expected');
|
|
174
|
+
}
|
|
175
|
+
return {
|
|
176
|
+
cumulativePriceA: readU64(bytes, 0),
|
|
177
|
+
cumulativePriceB: readU64(bytes, 8),
|
|
178
|
+
lastUpdatedAt: readU64(bytes, 16),
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
function decodeSwapStats(result) {
|
|
182
|
+
ensureReadonlySuccess(result, 'get_swap_stats');
|
|
183
|
+
if (!result.returnData) {
|
|
184
|
+
throw new Error('LichenSwap get_swap_stats did not return stats data');
|
|
185
|
+
}
|
|
186
|
+
const bytes = decodeReturnData(result.returnData);
|
|
187
|
+
if (bytes.length < 40) {
|
|
188
|
+
throw new Error('LichenSwap get_swap_stats payload was shorter than expected');
|
|
189
|
+
}
|
|
190
|
+
return {
|
|
191
|
+
swapCount: readU64(bytes, 0),
|
|
192
|
+
volumeA: readU64(bytes, 8),
|
|
193
|
+
volumeB: readU64(bytes, 16),
|
|
194
|
+
poolCount: readU64(bytes, 24),
|
|
195
|
+
totalLiquidity: readU64(bytes, 32),
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
export class LichenSwapClient {
|
|
199
|
+
constructor(connection, programId) {
|
|
200
|
+
this.connection = connection;
|
|
201
|
+
this.resolvedProgram = programId;
|
|
202
|
+
}
|
|
203
|
+
async callReadonly(functionName, args = new Uint8Array()) {
|
|
204
|
+
const programId = await this.getProgramId();
|
|
205
|
+
return this.connection.callReadonlyContract(programId, functionName, args);
|
|
206
|
+
}
|
|
207
|
+
async getProgramId() {
|
|
208
|
+
if (this.resolvedProgram) {
|
|
209
|
+
return this.resolvedProgram;
|
|
210
|
+
}
|
|
211
|
+
for (const symbol of PROGRAM_SYMBOL_CANDIDATES) {
|
|
212
|
+
try {
|
|
213
|
+
const entry = await this.connection.getSymbolRegistry(symbol);
|
|
214
|
+
if (entry?.program) {
|
|
215
|
+
this.resolvedProgram = new PublicKey(entry.program);
|
|
216
|
+
return this.resolvedProgram;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
catch {
|
|
220
|
+
// Try the next known registry alias.
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
throw new Error('Unable to resolve the LichenSwap program via getSymbolRegistry("LICHENSWAP")');
|
|
224
|
+
}
|
|
225
|
+
async getPoolInfo() {
|
|
226
|
+
return decodePoolInfo(await this.callReadonly('get_pool_info'));
|
|
227
|
+
}
|
|
228
|
+
async getQuote(amountIn, aToB = true) {
|
|
229
|
+
return decodeU64Return(await this.callReadonly('get_quote', encodeQuoteArgs(amountIn, aToB)), 'get_quote');
|
|
230
|
+
}
|
|
231
|
+
async getLiquidityBalance(provider) {
|
|
232
|
+
return decodeU64Return(await this.callReadonly('get_liquidity_balance', encodeLiquidityBalanceArgs(normalizeAddress(provider))), 'get_liquidity_balance');
|
|
233
|
+
}
|
|
234
|
+
async getTotalLiquidity() {
|
|
235
|
+
return decodeU64Return(await this.callReadonly('get_total_liquidity'), 'get_total_liquidity');
|
|
236
|
+
}
|
|
237
|
+
async getFlashLoanFee(amount) {
|
|
238
|
+
return decodeU64Return(await this.callReadonly('get_flash_loan_fee', encodeAmountArgs(amount, 'amount')), 'get_flash_loan_fee');
|
|
239
|
+
}
|
|
240
|
+
async getTwapCumulatives() {
|
|
241
|
+
return decodeTwapCumulatives(await this.callReadonly('get_twap_cumulatives'));
|
|
242
|
+
}
|
|
243
|
+
async getTwapSnapshotCount() {
|
|
244
|
+
return decodeU64Return(await this.callReadonly('get_twap_snapshot_count'), 'get_twap_snapshot_count');
|
|
245
|
+
}
|
|
246
|
+
async getProtocolFees() {
|
|
247
|
+
return decodeProtocolFees(await this.callReadonly('get_protocol_fees'));
|
|
248
|
+
}
|
|
249
|
+
async getPoolCount() {
|
|
250
|
+
return decodeU64Return(await this.callReadonly('get_pool_count'), 'get_pool_count');
|
|
251
|
+
}
|
|
252
|
+
async getSwapCount() {
|
|
253
|
+
return decodeU64Return(await this.callReadonly('get_swap_count'), 'get_swap_count');
|
|
254
|
+
}
|
|
255
|
+
async getTotalVolume() {
|
|
256
|
+
return decodeVolumeTotals(await this.callReadonly('get_total_volume'), 'get_total_volume');
|
|
257
|
+
}
|
|
258
|
+
async getSwapStats() {
|
|
259
|
+
return decodeSwapStats(await this.callReadonly('get_swap_stats'));
|
|
260
|
+
}
|
|
261
|
+
async getStats() {
|
|
262
|
+
const stats = await this.connection.getLichenSwapStats();
|
|
263
|
+
return {
|
|
264
|
+
swapCount: stats.swap_count ?? 0,
|
|
265
|
+
volumeA: stats.volume_a ?? 0,
|
|
266
|
+
volumeB: stats.volume_b ?? 0,
|
|
267
|
+
paused: Boolean(stats.paused),
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
async createPool(owner, params) {
|
|
271
|
+
const programId = await this.getProgramId();
|
|
272
|
+
const args = encodeCreatePoolArgs(params);
|
|
273
|
+
return this.connection.callContract(owner, programId, 'create_pool', args);
|
|
274
|
+
}
|
|
275
|
+
async addLiquidity(provider, params) {
|
|
276
|
+
const programId = await this.getProgramId();
|
|
277
|
+
const args = encodeAddLiquidityArgs(provider.pubkey(), params);
|
|
278
|
+
const value = params.valueSpores ?? addUnsignedU64(params.amountA, params.amountB, 'valueSpores');
|
|
279
|
+
return this.connection.callContract(provider, programId, 'add_liquidity', args, value);
|
|
280
|
+
}
|
|
281
|
+
async swap(provider, params, aToB = true) {
|
|
282
|
+
const programId = await this.getProgramId();
|
|
283
|
+
const args = encodeSwapArgs(params, aToB);
|
|
284
|
+
const value = params.valueSpores ?? normalizeUnsignedU64(params.amountIn, 'valueSpores');
|
|
285
|
+
return this.connection.callContract(provider, programId, 'swap', args, value);
|
|
286
|
+
}
|
|
287
|
+
async swapAToB(provider, params) {
|
|
288
|
+
const programId = await this.getProgramId();
|
|
289
|
+
const args = encodeDirectionalSwapArgs(params);
|
|
290
|
+
const value = params.valueSpores ?? normalizeUnsignedU64(params.amountIn, 'valueSpores');
|
|
291
|
+
return this.connection.callContract(provider, programId, 'swap_a_for_b', args, value);
|
|
292
|
+
}
|
|
293
|
+
async swapBToA(provider, params) {
|
|
294
|
+
const programId = await this.getProgramId();
|
|
295
|
+
const args = encodeDirectionalSwapArgs(params);
|
|
296
|
+
const value = params.valueSpores ?? normalizeUnsignedU64(params.amountIn, 'valueSpores');
|
|
297
|
+
return this.connection.callContract(provider, programId, 'swap_b_for_a', args, value);
|
|
298
|
+
}
|
|
299
|
+
async swapAToBWithDeadline(provider, params) {
|
|
300
|
+
const programId = await this.getProgramId();
|
|
301
|
+
const args = encodeDirectionalSwapWithDeadlineArgs(params);
|
|
302
|
+
const value = params.valueSpores ?? normalizeUnsignedU64(params.amountIn, 'valueSpores');
|
|
303
|
+
return this.connection.callContract(provider, programId, 'swap_a_for_b_with_deadline', args, value);
|
|
304
|
+
}
|
|
305
|
+
async swapBToAWithDeadline(provider, params) {
|
|
306
|
+
const programId = await this.getProgramId();
|
|
307
|
+
const args = encodeDirectionalSwapWithDeadlineArgs(params);
|
|
308
|
+
const value = params.valueSpores ?? normalizeUnsignedU64(params.amountIn, 'valueSpores');
|
|
309
|
+
return this.connection.callContract(provider, programId, 'swap_b_for_a_with_deadline', args, value);
|
|
310
|
+
}
|
|
311
|
+
}
|
package/dist/pq.d.ts
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { PublicKey } from './publickey.js';
|
|
2
|
+
export declare const PQ_SCHEME_ML_DSA_65 = 1;
|
|
3
|
+
export declare const ML_DSA_65_PUBLIC_KEY_BYTES = 1952;
|
|
4
|
+
export declare const ML_DSA_65_SIGNATURE_BYTES = 3309;
|
|
5
|
+
export interface JsonPqPublicKey {
|
|
6
|
+
scheme_version: number;
|
|
7
|
+
bytes: string;
|
|
8
|
+
}
|
|
9
|
+
export interface JsonPqSignature {
|
|
10
|
+
scheme_version: number;
|
|
11
|
+
public_key: JsonPqPublicKey;
|
|
12
|
+
sig: string;
|
|
13
|
+
}
|
|
14
|
+
type BytesLike = Uint8Array | number[];
|
|
15
|
+
type PqPublicKeyInput = {
|
|
16
|
+
schemeVersion?: number;
|
|
17
|
+
scheme_version?: number;
|
|
18
|
+
bytes: string | BytesLike;
|
|
19
|
+
};
|
|
20
|
+
type PqSignatureInput = {
|
|
21
|
+
schemeVersion?: number;
|
|
22
|
+
scheme_version?: number;
|
|
23
|
+
publicKey?: PqPublicKeyInput | PqPublicKey;
|
|
24
|
+
public_key?: PqPublicKeyInput | PqPublicKey;
|
|
25
|
+
sig: string | BytesLike;
|
|
26
|
+
};
|
|
27
|
+
export declare function generateMlDsa65Seed(): Uint8Array;
|
|
28
|
+
export declare function hexToBytes(hex: string): Uint8Array;
|
|
29
|
+
export declare function bytesToHex(bytes: Uint8Array): string;
|
|
30
|
+
export declare class PqPublicKey {
|
|
31
|
+
readonly schemeVersion: number;
|
|
32
|
+
private readonly _bytes;
|
|
33
|
+
constructor(schemeVersion: number, bytes: string | BytesLike);
|
|
34
|
+
static mlDsa65(bytes: string | BytesLike): PqPublicKey;
|
|
35
|
+
toBytes(): Uint8Array;
|
|
36
|
+
address(): PublicKey;
|
|
37
|
+
equals(other: PqPublicKey): boolean;
|
|
38
|
+
toJSON(): JsonPqPublicKey;
|
|
39
|
+
toString(): string;
|
|
40
|
+
static fromJSON(value: PqPublicKeyInput | PqPublicKey): PqPublicKey;
|
|
41
|
+
}
|
|
42
|
+
export declare class PqSignature {
|
|
43
|
+
readonly schemeVersion: number;
|
|
44
|
+
readonly publicKey: PqPublicKey;
|
|
45
|
+
private readonly _sig;
|
|
46
|
+
constructor(schemeVersion: number, publicKey: PqPublicKey, sig: string | BytesLike);
|
|
47
|
+
static mlDsa65(publicKey: PqPublicKey, sig: string | BytesLike): PqSignature;
|
|
48
|
+
signerAddress(): PublicKey;
|
|
49
|
+
toBytes(): Uint8Array;
|
|
50
|
+
verify(message: Uint8Array): boolean;
|
|
51
|
+
toJSON(): JsonPqSignature;
|
|
52
|
+
toString(): string;
|
|
53
|
+
static fromJSON(value: PqSignatureInput | PqSignature): PqSignature;
|
|
54
|
+
}
|
|
55
|
+
export declare function toPqPublicKey(value: PqPublicKeyInput | PqPublicKey): PqPublicKey;
|
|
56
|
+
export declare function toPqSignature(value: string | PqSignatureInput | PqSignature): PqSignature;
|
|
57
|
+
export {};
|
package/dist/pq.js
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { createHash, randomBytes as nodeRandomBytes } from 'crypto';
|
|
2
|
+
import { ml_dsa65 } from '@noble/post-quantum/ml-dsa.js';
|
|
3
|
+
import { PublicKey } from './publickey.js';
|
|
4
|
+
export const PQ_SCHEME_ML_DSA_65 = 0x01;
|
|
5
|
+
export const ML_DSA_65_PUBLIC_KEY_BYTES = 1952;
|
|
6
|
+
export const ML_DSA_65_SIGNATURE_BYTES = 3309;
|
|
7
|
+
function copyBytes(bytes) {
|
|
8
|
+
return new Uint8Array(bytes);
|
|
9
|
+
}
|
|
10
|
+
function schemePublicKeyLength(schemeVersion) {
|
|
11
|
+
switch (schemeVersion) {
|
|
12
|
+
case PQ_SCHEME_ML_DSA_65:
|
|
13
|
+
return ML_DSA_65_PUBLIC_KEY_BYTES;
|
|
14
|
+
default:
|
|
15
|
+
throw new Error(`Unsupported PQ public key scheme: 0x${schemeVersion.toString(16).padStart(2, '0')}`);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
function schemeSignatureLength(schemeVersion) {
|
|
19
|
+
switch (schemeVersion) {
|
|
20
|
+
case PQ_SCHEME_ML_DSA_65:
|
|
21
|
+
return ML_DSA_65_SIGNATURE_BYTES;
|
|
22
|
+
default:
|
|
23
|
+
throw new Error(`Unsupported PQ signature scheme: 0x${schemeVersion.toString(16).padStart(2, '0')}`);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
function normalizeBytes(value, label) {
|
|
27
|
+
if (typeof value === 'string') {
|
|
28
|
+
return hexToBytes(value);
|
|
29
|
+
}
|
|
30
|
+
return new Uint8Array(value);
|
|
31
|
+
}
|
|
32
|
+
function sha256(bytes) {
|
|
33
|
+
return new Uint8Array(createHash('sha256').update(bytes).digest());
|
|
34
|
+
}
|
|
35
|
+
export function generateMlDsa65Seed() {
|
|
36
|
+
return new Uint8Array(nodeRandomBytes(32));
|
|
37
|
+
}
|
|
38
|
+
export function hexToBytes(hex) {
|
|
39
|
+
const clean = hex.startsWith('0x') ? hex.slice(2) : hex;
|
|
40
|
+
if (clean.length % 2 !== 0) {
|
|
41
|
+
throw new Error('Invalid hex string');
|
|
42
|
+
}
|
|
43
|
+
const out = new Uint8Array(clean.length / 2);
|
|
44
|
+
for (let i = 0; i < out.length; i++) {
|
|
45
|
+
out[i] = parseInt(clean.slice(i * 2, i * 2 + 2), 16);
|
|
46
|
+
}
|
|
47
|
+
return out;
|
|
48
|
+
}
|
|
49
|
+
export function bytesToHex(bytes) {
|
|
50
|
+
return Array.from(bytes)
|
|
51
|
+
.map((b) => b.toString(16).padStart(2, '0'))
|
|
52
|
+
.join('');
|
|
53
|
+
}
|
|
54
|
+
export class PqPublicKey {
|
|
55
|
+
constructor(schemeVersion, bytes) {
|
|
56
|
+
this.schemeVersion = schemeVersion;
|
|
57
|
+
this._bytes = normalizeBytes(bytes, 'public key');
|
|
58
|
+
const expectedLength = schemePublicKeyLength(this.schemeVersion);
|
|
59
|
+
if (this._bytes.length !== expectedLength) {
|
|
60
|
+
throw new Error(`Invalid PQ public key length for scheme 0x${this.schemeVersion.toString(16).padStart(2, '0')}: `
|
|
61
|
+
+ `${this._bytes.length}, expected ${expectedLength}`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
static mlDsa65(bytes) {
|
|
65
|
+
return new PqPublicKey(PQ_SCHEME_ML_DSA_65, bytes);
|
|
66
|
+
}
|
|
67
|
+
toBytes() {
|
|
68
|
+
return copyBytes(this._bytes);
|
|
69
|
+
}
|
|
70
|
+
address() {
|
|
71
|
+
const digest = sha256(this._bytes);
|
|
72
|
+
const address = new Uint8Array(32);
|
|
73
|
+
address[0] = this.schemeVersion;
|
|
74
|
+
address.set(digest.subarray(0, 31), 1);
|
|
75
|
+
return new PublicKey(address);
|
|
76
|
+
}
|
|
77
|
+
equals(other) {
|
|
78
|
+
if (this.schemeVersion !== other.schemeVersion) {
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
const left = this._bytes;
|
|
82
|
+
const right = other._bytes;
|
|
83
|
+
if (left.length !== right.length) {
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
for (let i = 0; i < left.length; i++) {
|
|
87
|
+
if (left[i] !== right[i]) {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
toJSON() {
|
|
94
|
+
return {
|
|
95
|
+
scheme_version: this.schemeVersion,
|
|
96
|
+
bytes: bytesToHex(this._bytes),
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
toString() {
|
|
100
|
+
return JSON.stringify(this.toJSON());
|
|
101
|
+
}
|
|
102
|
+
static fromJSON(value) {
|
|
103
|
+
if (value instanceof PqPublicKey) {
|
|
104
|
+
return value;
|
|
105
|
+
}
|
|
106
|
+
const schemeVersion = value.scheme_version ?? value.schemeVersion;
|
|
107
|
+
if (schemeVersion === undefined) {
|
|
108
|
+
throw new Error('PQ public key is missing scheme version');
|
|
109
|
+
}
|
|
110
|
+
return new PqPublicKey(schemeVersion, value.bytes);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
export class PqSignature {
|
|
114
|
+
constructor(schemeVersion, publicKey, sig) {
|
|
115
|
+
this.schemeVersion = schemeVersion;
|
|
116
|
+
this.publicKey = publicKey;
|
|
117
|
+
this._sig = normalizeBytes(sig, 'signature');
|
|
118
|
+
if (this.publicKey.schemeVersion !== this.schemeVersion) {
|
|
119
|
+
throw new Error(`PQ signature/public-key scheme mismatch: 0x${this.schemeVersion.toString(16).padStart(2, '0')} `
|
|
120
|
+
+ `vs 0x${this.publicKey.schemeVersion.toString(16).padStart(2, '0')}`);
|
|
121
|
+
}
|
|
122
|
+
const expectedLength = schemeSignatureLength(this.schemeVersion);
|
|
123
|
+
if (this._sig.length !== expectedLength) {
|
|
124
|
+
throw new Error(`Invalid PQ signature length for scheme 0x${this.schemeVersion.toString(16).padStart(2, '0')}: `
|
|
125
|
+
+ `${this._sig.length}, expected ${expectedLength}`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
static mlDsa65(publicKey, sig) {
|
|
129
|
+
return new PqSignature(PQ_SCHEME_ML_DSA_65, publicKey, sig);
|
|
130
|
+
}
|
|
131
|
+
signerAddress() {
|
|
132
|
+
return this.publicKey.address();
|
|
133
|
+
}
|
|
134
|
+
toBytes() {
|
|
135
|
+
return copyBytes(this._sig);
|
|
136
|
+
}
|
|
137
|
+
verify(message) {
|
|
138
|
+
switch (this.schemeVersion) {
|
|
139
|
+
case PQ_SCHEME_ML_DSA_65:
|
|
140
|
+
return ml_dsa65.verify(this._sig, message, this.publicKey.toBytes());
|
|
141
|
+
default:
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
toJSON() {
|
|
146
|
+
return {
|
|
147
|
+
scheme_version: this.schemeVersion,
|
|
148
|
+
public_key: this.publicKey.toJSON(),
|
|
149
|
+
sig: bytesToHex(this._sig),
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
toString() {
|
|
153
|
+
return JSON.stringify(this.toJSON());
|
|
154
|
+
}
|
|
155
|
+
static fromJSON(value) {
|
|
156
|
+
if (value instanceof PqSignature) {
|
|
157
|
+
return value;
|
|
158
|
+
}
|
|
159
|
+
const schemeVersion = value.scheme_version ?? value.schemeVersion;
|
|
160
|
+
if (schemeVersion === undefined) {
|
|
161
|
+
throw new Error('PQ signature is missing scheme version');
|
|
162
|
+
}
|
|
163
|
+
const publicKey = value.public_key ?? value.publicKey;
|
|
164
|
+
if (!publicKey) {
|
|
165
|
+
throw new Error('PQ signature is missing public key');
|
|
166
|
+
}
|
|
167
|
+
return new PqSignature(schemeVersion, PqPublicKey.fromJSON(publicKey), value.sig);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
export function toPqPublicKey(value) {
|
|
171
|
+
return PqPublicKey.fromJSON(value);
|
|
172
|
+
}
|
|
173
|
+
export function toPqSignature(value) {
|
|
174
|
+
if (typeof value === 'string') {
|
|
175
|
+
return PqSignature.fromJSON(JSON.parse(value));
|
|
176
|
+
}
|
|
177
|
+
return PqSignature.fromJSON(value);
|
|
178
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A 32-byte address digest.
|
|
3
|
+
*
|
|
4
|
+
* The historical `PublicKey` name is preserved for SDK compatibility, but
|
|
5
|
+
* native PQ verifying keys live in `PqPublicKey`.
|
|
6
|
+
*/
|
|
7
|
+
export declare class PublicKey {
|
|
8
|
+
private readonly bytes;
|
|
9
|
+
constructor(value: string | Uint8Array | number[]);
|
|
10
|
+
/**
|
|
11
|
+
* Convert to base58 string
|
|
12
|
+
*/
|
|
13
|
+
toBase58(): string;
|
|
14
|
+
/**
|
|
15
|
+
* Convert to bytes
|
|
16
|
+
* AUDIT-FIX J-6: Returns a copy to prevent external mutation of internal state
|
|
17
|
+
*/
|
|
18
|
+
toBytes(): Uint8Array;
|
|
19
|
+
/**
|
|
20
|
+
* Convert to string (base58)
|
|
21
|
+
*/
|
|
22
|
+
toString(): string;
|
|
23
|
+
/**
|
|
24
|
+
* Check equality
|
|
25
|
+
*/
|
|
26
|
+
equals(other: PublicKey): boolean;
|
|
27
|
+
/**
|
|
28
|
+
* Create from base58 string
|
|
29
|
+
*/
|
|
30
|
+
static fromBase58(str: string): PublicKey;
|
|
31
|
+
/**
|
|
32
|
+
* Create from bytes
|
|
33
|
+
*/
|
|
34
|
+
static fromBytes(bytes: Uint8Array): PublicKey;
|
|
35
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// Lichen SDK - PublicKey Utilities
|
|
2
|
+
import bs58 from 'bs58';
|
|
3
|
+
/**
|
|
4
|
+
* A 32-byte address digest.
|
|
5
|
+
*
|
|
6
|
+
* The historical `PublicKey` name is preserved for SDK compatibility, but
|
|
7
|
+
* native PQ verifying keys live in `PqPublicKey`.
|
|
8
|
+
*/
|
|
9
|
+
export class PublicKey {
|
|
10
|
+
constructor(value) {
|
|
11
|
+
if (typeof value === 'string') {
|
|
12
|
+
// Decode from base58
|
|
13
|
+
this.bytes = bs58.decode(value);
|
|
14
|
+
}
|
|
15
|
+
else if (Array.isArray(value)) {
|
|
16
|
+
this.bytes = new Uint8Array(value);
|
|
17
|
+
}
|
|
18
|
+
else {
|
|
19
|
+
// AUDIT-FIX J-6: Defensive copy to prevent external mutation
|
|
20
|
+
this.bytes = new Uint8Array(value);
|
|
21
|
+
}
|
|
22
|
+
if (this.bytes.length !== 32) {
|
|
23
|
+
throw new Error(`Invalid public key length: ${this.bytes.length}, expected 32`);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Convert to base58 string
|
|
28
|
+
*/
|
|
29
|
+
toBase58() {
|
|
30
|
+
return bs58.encode(this.bytes);
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Convert to bytes
|
|
34
|
+
* AUDIT-FIX J-6: Returns a copy to prevent external mutation of internal state
|
|
35
|
+
*/
|
|
36
|
+
toBytes() {
|
|
37
|
+
return new Uint8Array(this.bytes);
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Convert to string (base58)
|
|
41
|
+
*/
|
|
42
|
+
toString() {
|
|
43
|
+
return this.toBase58();
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Check equality
|
|
47
|
+
*/
|
|
48
|
+
equals(other) {
|
|
49
|
+
if (this.bytes.length !== other.bytes.length) {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
for (let i = 0; i < this.bytes.length; i++) {
|
|
53
|
+
if (this.bytes[i] !== other.bytes[i]) {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Create from base58 string
|
|
61
|
+
*/
|
|
62
|
+
static fromBase58(str) {
|
|
63
|
+
return new PublicKey(str);
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Create from bytes
|
|
67
|
+
*/
|
|
68
|
+
static fromBytes(bytes) {
|
|
69
|
+
return new PublicKey(bytes);
|
|
70
|
+
}
|
|
71
|
+
}
|