@lombard.finance/sdk-solana 1.2.2 → 2.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.
Files changed (61) hide show
  1. package/README.md +48 -15
  2. package/dist/index.cjs +1 -1
  3. package/dist/index.js +37 -31
  4. package/dist/index2.cjs +57 -54
  5. package/dist/index2.js +7608 -7206
  6. package/package.json +2 -2
  7. package/src/const/errors.ts +0 -4
  8. package/src/const/getConfig.ts +43 -20
  9. package/src/const/rpcUrls.ts +2 -2
  10. package/src/idl/asset_router.json +548 -179
  11. package/src/idl/consortium.json +24 -43
  12. package/src/idl/mailbox.json +118 -107
  13. package/src/index.ts +1 -3
  14. package/src/services/SolanaServiceImpl.test.ts +123 -0
  15. package/src/services/SolanaServiceImpl.ts +53 -17
  16. package/src/stories/components/OutputSelector/OutputSelector.tsx +1 -0
  17. package/src/types/errors.ts +2 -0
  18. package/src/utils/createDebugLogger.ts +6 -13
  19. package/src/utils/errors.ts +2 -0
  20. package/src/utils/tokenAccount.ts +3 -1
  21. package/src/utils/transactions.ts +1 -1
  22. package/src/web3Sdk/claimToken/claimBtcb.ts +37 -28
  23. package/src/web3Sdk/claimToken/claimLbtcGmp.ts +66 -8
  24. package/src/web3Sdk/claimToken/claimToken.stories.tsx +2 -2
  25. package/src/web3Sdk/claimToken/claimToken.ts +20 -16
  26. package/src/web3Sdk/claimToken/constants.ts +5 -0
  27. package/src/web3Sdk/claimToken/index.ts +1 -0
  28. package/src/web3Sdk/claimToken/shared.ts +88 -80
  29. package/src/web3Sdk/deposit/deposit.stories.tsx +240 -0
  30. package/src/web3Sdk/deposit/deposit.test.ts +327 -0
  31. package/src/web3Sdk/deposit/deposit.ts +339 -0
  32. package/src/web3Sdk/deposit/index.ts +1 -0
  33. package/src/web3Sdk/getTokenFeeConfig/getTokenFeeConfig.stories.tsx +166 -0
  34. package/src/web3Sdk/getTokenFeeConfig/getTokenFeeConfig.test.ts +224 -0
  35. package/src/web3Sdk/getTokenFeeConfig/getTokenFeeConfig.ts +154 -0
  36. package/src/web3Sdk/getTokenFeeConfig/index.ts +11 -0
  37. package/src/web3Sdk/index.ts +3 -4
  38. package/src/web3Sdk/redeem/index.ts +1 -0
  39. package/src/web3Sdk/redeem/redeem.stories.tsx +226 -0
  40. package/src/web3Sdk/redeem/redeem.test.ts +327 -0
  41. package/src/web3Sdk/redeem/redeem.ts +352 -0
  42. package/src/web3Sdk/redeemToken/redeemBtcb.ts +174 -0
  43. package/src/web3Sdk/redeemToken/redeemForBtc.stories.tsx +35 -21
  44. package/src/web3Sdk/redeemToken/redeemForBtc.test.ts +306 -0
  45. package/src/web3Sdk/redeemToken/redeemForBtc.ts +54 -215
  46. package/src/web3Sdk/redeemToken/redeemLbtc.ts +174 -0
  47. package/src/web3Sdk/redeemToken/shared.test.ts +45 -0
  48. package/src/web3Sdk/redeemToken/shared.ts +97 -0
  49. package/src/web3Sdk/claimLBTC/claimLBTC.stories.tsx +0 -189
  50. package/src/web3Sdk/claimLBTC/claimLBTC.ts +0 -225
  51. package/src/web3Sdk/claimLBTC/index.ts +0 -1
  52. package/src/web3Sdk/claimLBTC/utils/generateDepositId.ts +0 -75
  53. package/src/web3Sdk/claimLBTC/utils/index.ts +0 -2
  54. package/src/web3Sdk/claimLBTC/utils/parseTransactionLogs.ts +0 -44
  55. package/src/web3Sdk/claimLBTC/utils/payloadUtils.ts +0 -58
  56. package/src/web3Sdk/claimLBTC/utils/postMintSignatures.ts +0 -50
  57. package/src/web3Sdk/unstakeLBTC/index.ts +0 -1
  58. package/src/web3Sdk/unstakeLBTC/unstakeLBTC.stories.tsx +0 -141
  59. package/src/web3Sdk/unstakeLBTC/unstakeLBTC.ts +0 -140
  60. /package/src/web3Sdk/{claimLBTC → claimToken}/utils/__tests__/signatureUtils.test.ts +0 -0
  61. /package/src/web3Sdk/{claimLBTC → claimToken}/utils/signatureUtils.ts +0 -0
@@ -11,7 +11,7 @@ import { sha256 } from 'js-sha256';
11
11
  import { IConfig } from '../../const/getConfig';
12
12
  import { ISolanaWalletProvider, SolanaNetwork } from '../../types';
13
13
  import { sendAndConfirmTransaction } from '../../utils';
14
- import { parseSignaturesFromProof } from '../claimLBTC/utils/signatureUtils';
14
+ import { parseSignaturesFromProof } from './utils/signatureUtils';
15
15
 
16
16
  // ── PDA seeds ──
17
17
 
@@ -137,83 +137,62 @@ export function getConsortiumSessionPDA(
137
137
  }
138
138
 
139
139
  /**
140
- * Fetch the current epoch from the on-chain consortium config account.
141
- *
142
- * Borsh layout of Consortium Config:
143
- * discriminator: 8 bytes (offset 0)
144
- * admin: 32 bytes (offset 8)
145
- * pending_admin: 32 bytes (offset 40)
146
- * current_epoch: 8 bytes (offset 72, u64 LE)
140
+ * Fetch the current epoch from the on-chain consortium config account
141
+ * using Anchor IDL-based deserialization.
147
142
  */
148
143
  export async function fetchCurrentEpoch(
149
- connection: Connection,
144
+ consortiumProgram: Program,
150
145
  consortiumConfigPDA: PublicKey,
151
146
  ): Promise<BN> {
152
- const accountInfo = await connection.getAccountInfo(consortiumConfigPDA);
153
- if (!accountInfo) {
154
- throw new Error(
155
- `Consortium config account not found at ${consortiumConfigPDA.toBase58()}`,
156
- );
157
- }
158
-
159
- const EPOCH_OFFSET = 72; // 8 (discriminator) + 32 (admin) + 32 (pending_admin)
160
- const MIN_SIZE = EPOCH_OFFSET + 8;
161
- if (accountInfo.data.length < MIN_SIZE) {
162
- throw new Error(
163
- `Consortium config data too short: expected >= ${MIN_SIZE} bytes, got ${accountInfo.data.length}`,
164
- );
165
- }
166
-
167
- const epochLe = accountInfo.data.readBigUInt64LE(EPOCH_OFFSET);
168
- return new BN(epochLe.toString());
147
+ // Anchor converts all IDL names to camelCase before building the namespace
148
+ // and decoding accounts, so field access uses camelCase keys.
149
+ const accountNs = consortiumProgram.account as unknown as Record<
150
+ string,
151
+ { fetch: (address: PublicKey) => Promise<unknown> }
152
+ >;
153
+ const raw = (await accountNs.config.fetch(consortiumConfigPDA)) as {
154
+ currentEpoch: BN;
155
+ };
156
+ return raw.currentEpoch;
169
157
  }
170
158
 
171
- // ── parseAssetRouterConfig ──
159
+ // ── fetchAssetRouterConfig ──
172
160
 
173
161
  /**
174
- * Parse Asset Router config from raw account bytes.
175
- * Equivalent to Go's `getPausedAndBasculeFromConfig(data)`.
162
+ * Fetch and deserialize Asset Router `Config` account via Anchor IDL.
176
163
  *
177
- * On-chain layout (discriminator 8 bytes, then fields):
178
- * admin: Pubkey (offset 8, 32 bytes)
179
- * pending_admin: Pubkey (offset 40, 32 bytes)
180
- * treasury: Pubkey (offset 72, 32 bytes)
181
- * paused: bool (offset 104, 1 byte)
182
- * native_mint: Pubkey (offset 105, 32 bytes)
183
- * mailbox: Pubkey (offset 137, 32 bytes)
184
- * bascule_enabled: bool (offset 169, 1 byte)
185
- * bascule_program: Pubkey (offset 170, 32 bytes) <- not in IDL
186
- * bascule_gmp_program:Pubkey (offset 202, 32 bytes) <- not in IDL
187
- * ledger_lchain_id: [u8;32] (offset 234, 32 bytes)
188
- * bitcoin_lchain_id: [u8;32] (offset 266, 32 bytes)
164
+ * IDL layout (fields used by SDK):
165
+ * paused: bool
166
+ * native_mint: pubkey
167
+ * bascule: Option<pubkey> (classic bascule for BTC.B)
168
+ * bascule_gmp: Option<pubkey> (bascule for LBTC GMP)
169
+ * ledger_lchain_id: [u8; 32]
189
170
  */
190
- export function parseAssetRouterConfig(data: Buffer): AssetRouterConfig {
191
- // ledger_lchain_id ends at offset 266 — the last field we read
192
- const MIN_SIZE = 266;
193
- if (data.length < MIN_SIZE) {
194
- throw new Error(
195
- `Asset Router config account data too short: expected >= ${MIN_SIZE} bytes, got ${data.length}`,
196
- );
197
- }
198
-
199
- const ZERO_PUBKEY = new PublicKey(new Uint8Array(32));
200
-
201
- const paused = data[104] !== 0;
202
- const nativeMint = new PublicKey(data.subarray(105, 137));
203
-
204
- const basculeProgramKey = new PublicKey(data.subarray(170, 202));
205
- const basculeProgramId = basculeProgramKey.equals(ZERO_PUBKEY)
206
- ? null
207
- : basculeProgramKey;
208
-
209
- const basculeGmpProgramKey = new PublicKey(data.subarray(202, 234));
210
- const basculeGmpProgramId = basculeGmpProgramKey.equals(ZERO_PUBKEY)
211
- ? null
212
- : basculeGmpProgramKey;
213
-
214
- const ledgerChainId = new Uint8Array(data.subarray(234, 266));
215
-
216
- return { paused, nativeMint, basculeProgramId, basculeGmpProgramId, ledgerChainId };
171
+ export async function fetchAssetRouterConfig(
172
+ assetRouterProgram: Program,
173
+ configPDA: PublicKey,
174
+ ): Promise<AssetRouterConfig> {
175
+ // Anchor camelCases IDL names, so `native_mint` → `nativeMint`,
176
+ // `bascule_gmp` `basculeGmp`, `ledger_lchain_id` `ledgerLchainId`.
177
+ const accountNs = assetRouterProgram.account as unknown as Record<
178
+ string,
179
+ { fetch: (address: PublicKey) => Promise<unknown> }
180
+ >;
181
+ const raw = (await accountNs.config.fetch(configPDA)) as {
182
+ paused: boolean;
183
+ nativeMint: PublicKey;
184
+ bascule: PublicKey | null;
185
+ basculeGmp: PublicKey | null;
186
+ ledgerLchainId: number[];
187
+ };
188
+
189
+ return {
190
+ paused: raw.paused,
191
+ nativeMint: raw.nativeMint,
192
+ basculeProgramId: raw.bascule ?? null,
193
+ basculeGmpProgramId: raw.basculeGmp ?? null,
194
+ ledgerChainId: new Uint8Array(raw.ledgerLchainId),
195
+ };
217
196
  }
218
197
 
219
198
  // ── Consortium session ──
@@ -255,7 +234,6 @@ export async function executeConsortiumSession(ctx: ClaimContext): Promise<void>
255
234
  payer: provider.publicKey,
256
235
  config: consortiumConfigPDA,
257
236
  session: sessionPDA,
258
- validatedPayload: validatedPayloadPDA,
259
237
  systemProgram: SystemProgram.programId,
260
238
  })
261
239
  .transaction();
@@ -265,22 +243,52 @@ export async function executeConsortiumSession(ctx: ClaimContext): Promise<void>
265
243
  connection,
266
244
  provider,
267
245
  debugLabel: 'Consortium create_session',
268
- skipPreflight: params.skipPreflight ?? false,
246
+ skipPreflight: params.skipPreflight ?? true,
269
247
  });
270
248
  debugLog('create_session completed');
271
249
  } else {
272
250
  debugLog('Session already exists, skipping create_session');
273
251
  }
274
252
 
275
- // Step 2: post_session_signatures
276
- const freshSession =
277
- sessionAccount ?? (await connection.getAccountInfo(sessionPDA));
253
+ // Step 2: post_session_signatures.
254
+ // Session raw layout: discriminator(8) | signed: Vec<bool> (4 LE len + N bytes) | weight: u64 LE | trailing bytes.
255
+ // Manual parse: Anchor coder rejects extra trailing bytes that exist in the on-chain account.
278
256
  let sessionSigned = false;
279
- if (freshSession && freshSession.data.length >= 8 + 8 + 4 + 8) {
280
- const vecLen = freshSession.data.readUInt32LE(8 + 8);
281
- const weightOffset = 8 + 8 + 4 + vecLen;
282
- if (freshSession.data.length >= weightOffset + 8) {
283
- sessionSigned = freshSession.data.readBigUInt64LE(weightOffset) > 0n;
257
+ const freshSessionAccount = sessionAccount
258
+ ? sessionAccount
259
+ : await connection.getAccountInfo(sessionPDA);
260
+ if (freshSessionAccount) {
261
+ try {
262
+ const data = freshSessionAccount.data;
263
+ if (data.length < 20) {
264
+ throw new Error(`session account too short: ${data.length}`);
265
+ }
266
+ const signedLen = data.readUInt32LE(8);
267
+ const weightOffset = 12 + signedLen;
268
+ if (data.length < weightOffset + 8) {
269
+ throw new Error(
270
+ `session data truncated: need ${weightOffset + 8}, got ${data.length}`,
271
+ );
272
+ }
273
+ const weight = data.readBigUInt64LE(weightOffset);
274
+ let signedCount = 0;
275
+ for (let i = 0; i < signedLen; i++) {
276
+ if (data[12 + i] !== 0) signedCount += 1;
277
+ }
278
+ sessionSigned = weight > 0n;
279
+ debugLog(
280
+ 'Session weight:',
281
+ weight.toString(),
282
+ 'signed count:',
283
+ signedCount,
284
+ 'of',
285
+ signedLen,
286
+ );
287
+ } catch (err) {
288
+ debugLog(
289
+ 'Failed to parse session for signature check:',
290
+ err instanceof Error ? err.message : String(err),
291
+ );
284
292
  }
285
293
  }
286
294
 
@@ -309,7 +317,7 @@ export async function executeConsortiumSession(ctx: ClaimContext): Promise<void>
309
317
  connection,
310
318
  provider,
311
319
  debugLabel: 'Consortium post_session_signatures',
312
- skipPreflight: params.skipPreflight ?? false,
320
+ skipPreflight: params.skipPreflight ?? true,
313
321
  });
314
322
  debugLog('post_session_signatures completed');
315
323
  } else {
@@ -334,7 +342,7 @@ export async function executeConsortiumSession(ctx: ClaimContext): Promise<void>
334
342
  connection,
335
343
  provider,
336
344
  debugLabel: 'Consortium finalize_session',
337
- skipPreflight: params.skipPreflight ?? false,
345
+ skipPreflight: params.skipPreflight ?? true,
338
346
  });
339
347
  debugLog('finalize_session completed — ValidatedPayload created');
340
348
  }
@@ -0,0 +1,240 @@
1
+ import { Env } from '@lombard.finance/sdk-common';
2
+ import type { Meta, StoryObj } from '@storybook/react';
3
+ import { useState } from 'react';
4
+
5
+ import { envToNetwork, getConfig } from '../../const/getConfig';
6
+ import {
7
+ Button,
8
+ CodeBlock,
9
+ ConnectButton,
10
+ ErrorDisplay,
11
+ ResultDisplay,
12
+ SectionCard,
13
+ } from '../../stories/components';
14
+ import { functionType } from '../../stories/decorators/function-type';
15
+ import { useConnect } from '../../stories/hooks/useConnect';
16
+ import useQuery from '../../stories/hooks/useQuery';
17
+ import { deposit } from './deposit';
18
+
19
+ interface DepositStoryArgs {
20
+ environment: Env;
21
+ amount: string;
22
+ recipient: string;
23
+ sourceTokenMint: string;
24
+ toLchainId: string;
25
+ toTokenAddress: string;
26
+ }
27
+
28
+ export const StoryView = ({
29
+ environment,
30
+ amount,
31
+ recipient,
32
+ sourceTokenMint,
33
+ toLchainId,
34
+ toTokenAddress,
35
+ }: DepositStoryArgs) => {
36
+ const network = envToNetwork[environment];
37
+ const config = getConfig(environment);
38
+ const [transactionLogs, setTransactionLogs] = useState<string[] | null>(null);
39
+
40
+ const {
41
+ data: connectionData,
42
+ error: connectError,
43
+ isLoading: isConnecting,
44
+ connect,
45
+ disconnect,
46
+ } = useConnect();
47
+ const isConnected = !!connectionData;
48
+ const address = connectionData?.address;
49
+ const provider = connectionData?.provider;
50
+
51
+ const effectiveSource = sourceTokenMint || config.btcbTokenMint;
52
+ const effectiveToToken = toTokenAddress || config.lbtcTokenMint;
53
+
54
+ const request = async () => {
55
+ if (!provider || !address) throw new Error('Wallet not connected.');
56
+ if (!recipient)
57
+ throw new Error('Recipient address is required (set in args).');
58
+ const parsedAmount = parseFloat(amount);
59
+ if (!amount || isNaN(parsedAmount) || parsedAmount <= 0)
60
+ throw new Error('Amount must be a positive number in BTC (set in args).');
61
+
62
+ const amountSats = Math.round(parsedAmount * 1e8).toString();
63
+
64
+ setTransactionLogs(null);
65
+ try {
66
+ const result = await deposit(provider, {
67
+ amount: amountSats,
68
+ recipient,
69
+ sourceTokenMint: sourceTokenMint || undefined,
70
+ toLchainId: toLchainId || undefined,
71
+ toTokenAddress: toTokenAddress || undefined,
72
+ network,
73
+ env: environment,
74
+ debug: true,
75
+ });
76
+ return result;
77
+ } catch (err: unknown) {
78
+ if (err instanceof Error && err.message.includes('Debug logs:')) {
79
+ const parts = err.message.split('Debug logs:\n');
80
+ setTransactionLogs(parts[1]?.split('\n') || []);
81
+ }
82
+ throw err;
83
+ }
84
+ };
85
+
86
+ const {
87
+ data: txHash,
88
+ error,
89
+ isLoading,
90
+ refetch: handleDeposit,
91
+ } = useQuery(
92
+ request,
93
+ [
94
+ provider,
95
+ address,
96
+ amount,
97
+ recipient,
98
+ sourceTokenMint,
99
+ toLchainId,
100
+ toTokenAddress,
101
+ environment,
102
+ ],
103
+ false,
104
+ );
105
+
106
+ return (
107
+ <>
108
+ <ConnectButton
109
+ connect={connect}
110
+ disconnect={disconnect}
111
+ isConnected={isConnected}
112
+ isLoading={isConnecting}
113
+ error={connectError}
114
+ walletName={connectionData?.walletName}
115
+ address={connectionData?.address}
116
+ network={network}
117
+ />
118
+
119
+ {isConnected && (
120
+ <>
121
+ <SectionCard title="Configuration">
122
+ <p>
123
+ <strong>Environment:</strong> {environment}
124
+ </p>
125
+ <p>
126
+ <strong>Network:</strong> {network}
127
+ </p>
128
+ <p>
129
+ <strong>Amount:</strong> {amount} BTC
130
+ </p>
131
+ <p>
132
+ <strong>Recipient:</strong> {recipient || <em>Not set</em>}
133
+ </p>
134
+ <p>
135
+ <strong>Source token (e.g. BTC.b):</strong>{' '}
136
+ {effectiveSource || <em>Not configured</em>}
137
+ </p>
138
+ <p>
139
+ <strong>Destination token (e.g. LBTC):</strong>{' '}
140
+ {effectiveToToken || <em>Not configured</em>}
141
+ </p>
142
+ </SectionCard>
143
+
144
+ <div className="d-grid gap-2 my-4">
145
+ <Button
146
+ primary
147
+ size="large"
148
+ onClick={handleDeposit}
149
+ isLoading={isLoading}
150
+ actionName={deposit.name}
151
+ />
152
+ </div>
153
+
154
+ {txHash && (
155
+ <ResultDisplay
156
+ result={txHash}
157
+ title="Deposit Transaction Hash"
158
+ successMessage="Success! Deposit (BTC.b → LBTC) transaction submitted."
159
+ />
160
+ )}
161
+ {(error || connectError) && (
162
+ <ErrorDisplay
163
+ error={error || connectError}
164
+ title="Deposit Error"
165
+ />
166
+ )}
167
+
168
+ {transactionLogs && transactionLogs.length > 0 && (
169
+ <SectionCard title="Transaction Logs (Debug)">
170
+ <CodeBlock text={transactionLogs.join('\n')} />
171
+ </SectionCard>
172
+ )}
173
+ </>
174
+ )}
175
+ </>
176
+ );
177
+ };
178
+
179
+ const meta: Meta<typeof StoryView> = {
180
+ title: 'write/deposit (Asset Router)',
181
+ component: StoryView,
182
+ tags: ['autodocs'],
183
+ decorators: [functionType('write')],
184
+ parameters: {
185
+ docs: {
186
+ description: {
187
+ component: `Demonstrates depositing source token (e.g. BTC.b) for destination token (e.g. LBTC) via the Asset Router's \`deposit\` instruction.
188
+
189
+ **Flow:**
190
+ 1. Connect a Solana wallet holding the source token (default: BTC.b)
191
+ 2. Enter the recipient address and amount (in BTC)
192
+ 3. Optionally override source mint, destination chain ID, and destination token
193
+ 4. Call \`deposit\` — burns the source token and sends a GMP message through the Mailbox
194
+ 5. The destination token (e.g. LBTC) is minted to the recipient's ATA for that mint on the target chain (payload carries the token account address)
195
+
196
+ **Example (devnet):** PROGRAM_ID=LomVyJDZ91jeVbNnTupJXKJTQFakJVMc87CmwDHYt95, MAILBOX=LomJw912MoUd7iiAesTQAgz1paLcTqi6ndG3w3pnKH9`,
197
+ },
198
+ },
199
+ },
200
+ args: {
201
+ environment: Env.stage,
202
+ amount: '0.0002',
203
+ recipient: '',
204
+ sourceTokenMint: '',
205
+ toLchainId: '',
206
+ toTokenAddress: '',
207
+ },
208
+ argTypes: {
209
+ environment: {
210
+ control: { type: 'select' },
211
+ options: Object.values(Env),
212
+ },
213
+ amount: {
214
+ control: { type: 'text' },
215
+ description: 'Amount to deposit in BTC (e.g. 0.0002)',
216
+ },
217
+ recipient: {
218
+ control: { type: 'text' },
219
+ description:
220
+ 'Recipient wallet (owner) for the destination token; SDK uses the associated token account in the deposit payload (Solana base58)',
221
+ },
222
+ sourceTokenMint: {
223
+ control: { type: 'text' },
224
+ description: 'Source token mint override (defaults to BTC.b from config)',
225
+ },
226
+ toLchainId: {
227
+ control: { type: 'text' },
228
+ description:
229
+ 'Destination Lombard routing chain ID (hex). Defaults to Solana',
230
+ },
231
+ toTokenAddress: {
232
+ control: { type: 'text' },
233
+ description:
234
+ 'Destination token mint override (defaults to LBTC from config)',
235
+ },
236
+ },
237
+ };
238
+
239
+ export default meta;
240
+ type Story = StoryObj<typeof meta>;