@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.
@@ -0,0 +1,206 @@
1
+ import { PublicKey } from './publickey.js';
2
+ const PROGRAM_SYMBOL_CANDIDATES = ['LEND', 'lend', 'THALLLEND', 'thalllend'];
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 u64LE(value, fieldName) {
19
+ const out = new Uint8Array(8);
20
+ new DataView(out.buffer).setBigUint64(0, normalizeUnsignedU64(value, fieldName), true);
21
+ return out;
22
+ }
23
+ function buildLayoutArgs(layout, chunks) {
24
+ const header = Uint8Array.from([0xAB, ...layout]);
25
+ const total = chunks.reduce((sum, chunk) => sum + chunk.length, header.length);
26
+ const out = new Uint8Array(total);
27
+ out.set(header, 0);
28
+ let offset = header.length;
29
+ for (const chunk of chunks) {
30
+ out.set(chunk, offset);
31
+ offset += chunk.length;
32
+ }
33
+ return out;
34
+ }
35
+ function encodeUserAmountArgs(user, amount) {
36
+ return buildLayoutArgs([0x20, 0x08], [
37
+ user.toBytes(),
38
+ u64LE(amount, 'amount'),
39
+ ]);
40
+ }
41
+ function encodeUserLookupArgs(user) {
42
+ return buildLayoutArgs([0x20], [normalizeAddress(user).toBytes()]);
43
+ }
44
+ function encodeLiquidateArgs(liquidator, params) {
45
+ return buildLayoutArgs([0x20, 0x20, 0x08], [
46
+ liquidator.toBytes(),
47
+ normalizeAddress(params.borrower).toBytes(),
48
+ u64LE(params.repayAmount, 'repayAmount'),
49
+ ]);
50
+ }
51
+ function decodeReturnData(returnData) {
52
+ return Uint8Array.from(Buffer.from(returnData, 'base64'));
53
+ }
54
+ function readU64(bytes, offset) {
55
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
56
+ return view.getBigUint64(offset, true);
57
+ }
58
+ function ensureReadonlySuccess(result, functionName, allowedReturnCodes = [0]) {
59
+ const code = result.returnCode ?? 0;
60
+ if (!allowedReturnCodes.includes(code)) {
61
+ throw new Error(result.error ?? `ThallLend ${functionName} returned code ${code}`);
62
+ }
63
+ if (result.success === false && result.error) {
64
+ throw new Error(result.error);
65
+ }
66
+ }
67
+ function decodeU64Return(result, functionName) {
68
+ ensureReadonlySuccess(result, functionName);
69
+ if (!result.returnData) {
70
+ throw new Error(`ThallLend ${functionName} did not return payload data`);
71
+ }
72
+ const bytes = decodeReturnData(result.returnData);
73
+ if (bytes.length < 8) {
74
+ throw new Error(`ThallLend ${functionName} payload was shorter than expected`);
75
+ }
76
+ return readU64(bytes, 0);
77
+ }
78
+ function decodeAccountInfo(result) {
79
+ ensureReadonlySuccess(result, 'get_account_info');
80
+ if (!result.returnData) {
81
+ throw new Error('ThallLend get_account_info did not return account data');
82
+ }
83
+ const bytes = decodeReturnData(result.returnData);
84
+ if (bytes.length < 24) {
85
+ throw new Error('ThallLend get_account_info payload was shorter than expected');
86
+ }
87
+ return {
88
+ deposit: readU64(bytes, 0),
89
+ borrow: readU64(bytes, 8),
90
+ healthFactorBps: readU64(bytes, 16),
91
+ };
92
+ }
93
+ function decodeProtocolStats(result) {
94
+ ensureReadonlySuccess(result, 'get_protocol_stats');
95
+ if (!result.returnData) {
96
+ throw new Error('ThallLend get_protocol_stats did not return stats data');
97
+ }
98
+ const bytes = decodeReturnData(result.returnData);
99
+ if (bytes.length < 32) {
100
+ throw new Error('ThallLend get_protocol_stats payload was shorter than expected');
101
+ }
102
+ return {
103
+ totalDeposits: readU64(bytes, 0),
104
+ totalBorrows: readU64(bytes, 8),
105
+ utilizationPct: readU64(bytes, 16),
106
+ reserves: readU64(bytes, 24),
107
+ };
108
+ }
109
+ function decodeInterestRate(result) {
110
+ ensureReadonlySuccess(result, 'get_interest_rate');
111
+ if (!result.returnData) {
112
+ throw new Error('ThallLend get_interest_rate did not return rate data');
113
+ }
114
+ const bytes = decodeReturnData(result.returnData);
115
+ if (bytes.length < 24) {
116
+ throw new Error('ThallLend get_interest_rate payload was shorter than expected');
117
+ }
118
+ return {
119
+ ratePerSlot: readU64(bytes, 0),
120
+ utilizationPct: readU64(bytes, 8),
121
+ totalAvailable: readU64(bytes, 16),
122
+ };
123
+ }
124
+ export class ThallLendClient {
125
+ constructor(connection, programId) {
126
+ this.connection = connection;
127
+ this.resolvedProgram = programId;
128
+ }
129
+ async callReadonly(functionName, args = new Uint8Array()) {
130
+ const programId = await this.getProgramId();
131
+ return this.connection.callReadonlyContract(programId, functionName, args);
132
+ }
133
+ async getProgramId() {
134
+ if (this.resolvedProgram) {
135
+ return this.resolvedProgram;
136
+ }
137
+ for (const symbol of PROGRAM_SYMBOL_CANDIDATES) {
138
+ try {
139
+ const entry = await this.connection.getSymbolRegistry(symbol);
140
+ if (entry?.program) {
141
+ this.resolvedProgram = new PublicKey(entry.program);
142
+ return this.resolvedProgram;
143
+ }
144
+ }
145
+ catch {
146
+ // Try the next known registry alias.
147
+ }
148
+ }
149
+ throw new Error('Unable to resolve the ThallLend program via getSymbolRegistry("LEND")');
150
+ }
151
+ async getAccountInfo(user) {
152
+ return decodeAccountInfo(await this.callReadonly('get_account_info', encodeUserLookupArgs(user)));
153
+ }
154
+ async getProtocolStats() {
155
+ return decodeProtocolStats(await this.callReadonly('get_protocol_stats'));
156
+ }
157
+ async getInterestRate() {
158
+ return decodeInterestRate(await this.callReadonly('get_interest_rate'));
159
+ }
160
+ async getDepositCount() {
161
+ return decodeU64Return(await this.callReadonly('get_deposit_count'), 'get_deposit_count');
162
+ }
163
+ async getBorrowCount() {
164
+ return decodeU64Return(await this.callReadonly('get_borrow_count'), 'get_borrow_count');
165
+ }
166
+ async getLiquidationCount() {
167
+ return decodeU64Return(await this.callReadonly('get_liquidation_count'), 'get_liquidation_count');
168
+ }
169
+ async getStats() {
170
+ const stats = await this.connection.getThallLendStats();
171
+ return {
172
+ totalDeposits: stats.total_deposits ?? 0,
173
+ totalBorrows: stats.total_borrows ?? 0,
174
+ reserves: stats.reserves ?? 0,
175
+ depositCount: stats.deposit_count ?? 0,
176
+ borrowCount: stats.borrow_count ?? 0,
177
+ liquidationCount: stats.liquidation_count ?? 0,
178
+ paused: Boolean(stats.paused),
179
+ };
180
+ }
181
+ async deposit(depositor, amount) {
182
+ const programId = await this.getProgramId();
183
+ const args = encodeUserAmountArgs(depositor.pubkey(), amount);
184
+ return this.connection.callContract(depositor, programId, 'deposit', args, normalizeUnsignedU64(amount, 'amount'));
185
+ }
186
+ async withdraw(depositor, amount) {
187
+ const programId = await this.getProgramId();
188
+ const args = encodeUserAmountArgs(depositor.pubkey(), amount);
189
+ return this.connection.callContract(depositor, programId, 'withdraw', args);
190
+ }
191
+ async borrow(borrower, amount) {
192
+ const programId = await this.getProgramId();
193
+ const args = encodeUserAmountArgs(borrower.pubkey(), amount);
194
+ return this.connection.callContract(borrower, programId, 'borrow', args);
195
+ }
196
+ async repay(borrower, amount) {
197
+ const programId = await this.getProgramId();
198
+ const args = encodeUserAmountArgs(borrower.pubkey(), amount);
199
+ return this.connection.callContract(borrower, programId, 'repay', args, normalizeUnsignedU64(amount, 'amount'));
200
+ }
201
+ async liquidate(liquidator, params) {
202
+ const programId = await this.getProgramId();
203
+ const args = encodeLiquidateArgs(liquidator.pubkey(), params);
204
+ return this.connection.callContract(liquidator, programId, 'liquidate', args, normalizeUnsignedU64(params.repayAmount, 'repayAmount'));
205
+ }
206
+ }
@@ -0,0 +1,100 @@
1
+ import { PublicKey } from './publickey.js';
2
+ import { Keypair } from './keypair.js';
3
+ import { PqSignature } from './pq.js';
4
+ /**
5
+ * Transaction instruction
6
+ */
7
+ export interface Instruction {
8
+ programId: PublicKey;
9
+ accounts: PublicKey[];
10
+ data: Uint8Array;
11
+ }
12
+ /**
13
+ * Transaction message (before signing)
14
+ */
15
+ export interface Message {
16
+ instructions: Instruction[];
17
+ recentBlockhash: string;
18
+ computeBudget?: number;
19
+ computeUnitPrice?: number;
20
+ }
21
+ /**
22
+ * Signed transaction
23
+ */
24
+ export interface Transaction {
25
+ signatures: PqSignature[];
26
+ message: Message;
27
+ }
28
+ /**
29
+ * Transaction builder
30
+ */
31
+ export declare class TransactionBuilder {
32
+ private instructions;
33
+ private recentBlockhash?;
34
+ /**
35
+ * Add an instruction
36
+ */
37
+ add(instruction: Instruction): this;
38
+ /**
39
+ * Set recent blockhash
40
+ */
41
+ setRecentBlockhash(blockhash: string): this;
42
+ /**
43
+ * Build the message (ready for signing)
44
+ */
45
+ build(): Message;
46
+ /**
47
+ * Build and sign the transaction
48
+ */
49
+ buildAndSign(keypair: Keypair): Transaction;
50
+ /**
51
+ * Create a transfer instruction
52
+ *
53
+ * P9-SDK-01: `amount` accepts `number | bigint` to avoid silent truncation
54
+ * for values exceeding `Number.MAX_SAFE_INTEGER` (2^53 - 1).
55
+ * Using `bigint` is recommended for large LICN amounts.
56
+ */
57
+ static transfer(from: PublicKey, to: PublicKey, amount: number | bigint): Instruction;
58
+ /**
59
+ * Create a stake instruction
60
+ *
61
+ * P9-SDK-01: `amount` accepts `number | bigint`.
62
+ */
63
+ static stake(from: PublicKey, validator: PublicKey, amount: number | bigint): Instruction;
64
+ /**
65
+ * Create an unstake request instruction
66
+ *
67
+ * P9-SDK-01: `amount` accepts `number | bigint`.
68
+ */
69
+ static unstake(from: PublicKey, validator: PublicKey, amount: number | bigint): Instruction;
70
+ /**
71
+ * Contract program ID: [0xFF; 32]
72
+ */
73
+ private static readonly CONTRACT_PROGRAM_ID;
74
+ /**
75
+ * Create a deploy contract instruction.
76
+ *
77
+ * @param deployer - The deployer's public key (signer, pays deploy fee)
78
+ * @param code - WASM bytecode
79
+ * @param initData - Optional initialization data (default: empty)
80
+ */
81
+ static deployContract(deployer: PublicKey, code: Uint8Array, initData?: Uint8Array): Instruction;
82
+ /**
83
+ * Create a call contract instruction.
84
+ *
85
+ * @param caller - The caller's public key (signer)
86
+ * @param contract - The contract's public key
87
+ * @param functionName - Contract function to call
88
+ * @param args - Serialized function arguments (default: empty)
89
+ * @param value - Native LICN to send with the call in spores (default: 0)
90
+ */
91
+ static callContract(caller: PublicKey, contract: PublicKey, functionName: string, args?: Uint8Array, value?: number | bigint): Instruction;
92
+ /**
93
+ * Create an upgrade contract instruction (owner only).
94
+ *
95
+ * @param owner - The contract owner's public key (signer)
96
+ * @param contract - The contract's public key
97
+ * @param code - New WASM bytecode
98
+ */
99
+ static upgradeContract(owner: PublicKey, contract: PublicKey, code: Uint8Array): Instruction;
100
+ }
@@ -0,0 +1,202 @@
1
+ // Lichen SDK - Transaction Types and Builder
2
+ import { PublicKey } from './publickey.js';
3
+ import { encodeMessage } from './bincode.js';
4
+ /**
5
+ * Transaction builder
6
+ */
7
+ export class TransactionBuilder {
8
+ constructor() {
9
+ this.instructions = [];
10
+ }
11
+ /**
12
+ * Add an instruction
13
+ */
14
+ add(instruction) {
15
+ this.instructions.push(instruction);
16
+ return this;
17
+ }
18
+ /**
19
+ * Set recent blockhash
20
+ */
21
+ setRecentBlockhash(blockhash) {
22
+ this.recentBlockhash = blockhash;
23
+ return this;
24
+ }
25
+ /**
26
+ * Build the message (ready for signing)
27
+ */
28
+ build() {
29
+ if (!this.recentBlockhash) {
30
+ throw new Error('Recent blockhash not set');
31
+ }
32
+ if (this.instructions.length === 0) {
33
+ throw new Error('No instructions added');
34
+ }
35
+ return {
36
+ instructions: this.instructions,
37
+ recentBlockhash: this.recentBlockhash,
38
+ };
39
+ }
40
+ /**
41
+ * Build and sign the transaction
42
+ */
43
+ buildAndSign(keypair) {
44
+ const message = this.build();
45
+ const messageBytes = encodeMessage(message);
46
+ const signature = keypair.sign(messageBytes);
47
+ return {
48
+ signatures: [signature],
49
+ message,
50
+ };
51
+ }
52
+ /**
53
+ * Create a transfer instruction
54
+ *
55
+ * P9-SDK-01: `amount` accepts `number | bigint` to avoid silent truncation
56
+ * for values exceeding `Number.MAX_SAFE_INTEGER` (2^53 - 1).
57
+ * Using `bigint` is recommended for large LICN amounts.
58
+ */
59
+ static transfer(from, to, amount) {
60
+ const amt = BigInt(amount);
61
+ if (amt < 0n)
62
+ throw new Error('Transfer amount must be non-negative');
63
+ if (amt > 0xffffffffffffffffn)
64
+ throw new Error('Transfer amount exceeds u64 max');
65
+ // Encode transfer data (program-specific format)
66
+ const data = new Uint8Array(9);
67
+ data[0] = 0; // Transfer instruction type
68
+ const view = new DataView(data.buffer);
69
+ view.setBigUint64(1, amt, true);
70
+ return {
71
+ programId: new PublicKey('11111111111111111111111111111111'), // System program (all-zero pubkey)
72
+ accounts: [from, to],
73
+ data,
74
+ };
75
+ }
76
+ /**
77
+ * Create a stake instruction
78
+ *
79
+ * P9-SDK-01: `amount` accepts `number | bigint`.
80
+ */
81
+ static stake(from, validator, amount) {
82
+ const amt = BigInt(amount);
83
+ if (amt < 0n)
84
+ throw new Error('Stake amount must be non-negative');
85
+ if (amt > 0xffffffffffffffffn)
86
+ throw new Error('Stake amount exceeds u64 max');
87
+ const data = new Uint8Array(9);
88
+ data[0] = 9; // Stake instruction type
89
+ const view = new DataView(data.buffer);
90
+ view.setBigUint64(1, amt, true);
91
+ return {
92
+ programId: new PublicKey('11111111111111111111111111111111'), // System program (all-zero pubkey)
93
+ accounts: [from, validator],
94
+ data,
95
+ };
96
+ }
97
+ /**
98
+ * Create an unstake request instruction
99
+ *
100
+ * P9-SDK-01: `amount` accepts `number | bigint`.
101
+ */
102
+ static unstake(from, validator, amount) {
103
+ const amt = BigInt(amount);
104
+ if (amt < 0n)
105
+ throw new Error('Unstake amount must be non-negative');
106
+ if (amt > 0xffffffffffffffffn)
107
+ throw new Error('Unstake amount exceeds u64 max');
108
+ const data = new Uint8Array(9);
109
+ data[0] = 10; // Unstake request instruction type
110
+ const view = new DataView(data.buffer);
111
+ view.setBigUint64(1, amt, true);
112
+ return {
113
+ programId: new PublicKey('11111111111111111111111111111111'), // System program (all-zero pubkey)
114
+ accounts: [from, validator],
115
+ data,
116
+ };
117
+ }
118
+ /**
119
+ * Create a deploy contract instruction.
120
+ *
121
+ * @param deployer - The deployer's public key (signer, pays deploy fee)
122
+ * @param code - WASM bytecode
123
+ * @param initData - Optional initialization data (default: empty)
124
+ */
125
+ static deployContract(deployer, code, initData = new Uint8Array(0)) {
126
+ if (code.length < 4 || code[0] !== 0x00 || code[1] !== 0x61 || code[2] !== 0x73 || code[3] !== 0x6d) {
127
+ throw new Error('Invalid WASM bytecode: missing magic header (\\0asm)');
128
+ }
129
+ if (code.length > 512 * 1024) {
130
+ throw new Error('Contract code exceeds 512 KB limit');
131
+ }
132
+ // ContractInstruction::Deploy serialized as JSON (matches core serde_json format)
133
+ const payload = JSON.stringify({
134
+ Deploy: {
135
+ code: Array.from(code),
136
+ init_data: Array.from(initData),
137
+ },
138
+ });
139
+ const data = new TextEncoder().encode(payload);
140
+ return {
141
+ programId: TransactionBuilder.CONTRACT_PROGRAM_ID,
142
+ accounts: [deployer],
143
+ data,
144
+ };
145
+ }
146
+ /**
147
+ * Create a call contract instruction.
148
+ *
149
+ * @param caller - The caller's public key (signer)
150
+ * @param contract - The contract's public key
151
+ * @param functionName - Contract function to call
152
+ * @param args - Serialized function arguments (default: empty)
153
+ * @param value - Native LICN to send with the call in spores (default: 0)
154
+ */
155
+ static callContract(caller, contract, functionName, args = new Uint8Array(0), value = 0) {
156
+ const val = BigInt(value);
157
+ if (val < 0n)
158
+ throw new Error('Call value must be non-negative');
159
+ if (val > 0xffffffffffffffffn)
160
+ throw new Error('Call value exceeds u64 max');
161
+ // ContractInstruction::Call serialized as JSON (matches core serde_json format)
162
+ const payload = JSON.stringify({
163
+ Call: {
164
+ function: functionName,
165
+ args: Array.from(args),
166
+ value: Number(val),
167
+ },
168
+ });
169
+ const data = new TextEncoder().encode(payload);
170
+ return {
171
+ programId: TransactionBuilder.CONTRACT_PROGRAM_ID,
172
+ accounts: [caller, contract],
173
+ data,
174
+ };
175
+ }
176
+ /**
177
+ * Create an upgrade contract instruction (owner only).
178
+ *
179
+ * @param owner - The contract owner's public key (signer)
180
+ * @param contract - The contract's public key
181
+ * @param code - New WASM bytecode
182
+ */
183
+ static upgradeContract(owner, contract, code) {
184
+ if (code.length < 4 || code[0] !== 0x00 || code[1] !== 0x61 || code[2] !== 0x73 || code[3] !== 0x6d) {
185
+ throw new Error('Invalid WASM bytecode: missing magic header (\\0asm)');
186
+ }
187
+ if (code.length > 512 * 1024) {
188
+ throw new Error('Contract code exceeds 512 KB limit');
189
+ }
190
+ const payload = JSON.stringify({ Upgrade: { code: Array.from(code) } });
191
+ const data = new TextEncoder().encode(payload);
192
+ return {
193
+ programId: TransactionBuilder.CONTRACT_PROGRAM_ID,
194
+ accounts: [owner, contract],
195
+ data,
196
+ };
197
+ }
198
+ }
199
+ /**
200
+ * Contract program ID: [0xFF; 32]
201
+ */
202
+ TransactionBuilder.CONTRACT_PROGRAM_ID = new PublicKey(new Uint8Array(32).fill(0xFF));
package/package.json ADDED
@@ -0,0 +1,56 @@
1
+ {
2
+ "name": "@lobstercove/lichen-sdk",
3
+ "version": "1.0.0",
4
+ "description": "Official JavaScript/TypeScript SDK for Lichen blockchain",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js",
12
+ "default": "./dist/index.js"
13
+ }
14
+ },
15
+ "files": [
16
+ "dist",
17
+ "README.md"
18
+ ],
19
+ "scripts": {
20
+ "build": "tsc",
21
+ "dev": "tsc --watch",
22
+ "test": "tsc --noEmit",
23
+ "prepare": "npm run build"
24
+ },
25
+ "keywords": [
26
+ "lichen",
27
+ "blockchain",
28
+ "crypto",
29
+ "licn",
30
+ "web3"
31
+ ],
32
+ "author": "Lichen Labs <hello@lichen.network>",
33
+ "license": "MIT",
34
+ "repository": {
35
+ "type": "git",
36
+ "url": "git+https://github.com/lobstercove/lichen.git",
37
+ "directory": "sdk/js"
38
+ },
39
+ "publishConfig": {
40
+ "access": "public"
41
+ },
42
+ "engines": {
43
+ "node": ">=20.19.0"
44
+ },
45
+ "devDependencies": {
46
+ "@types/node": "^20.19.32",
47
+ "@types/ws": "^8.5.10",
48
+ "ts-node": "^10.9.2",
49
+ "typescript": "^5.3.3"
50
+ },
51
+ "dependencies": {
52
+ "@noble/post-quantum": "^0.6.0",
53
+ "bs58": "^5.0.0",
54
+ "ws": "^8.16.0"
55
+ }
56
+ }