@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
@@ -0,0 +1,327 @@
1
+ import { PublicKey } from '@solana/web3.js';
2
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
3
+
4
+ import type { IConfig } from '../../const/getConfig';
5
+ import { type ISolanaWalletProvider, SolanaNetwork } from '../../types';
6
+ import { ErrorCode, SolanaSdkError } from '../../utils';
7
+
8
+ // ── Constants ──
9
+
10
+ const MOCK_LBTC_MINT = 'LBTCojyVJ63rsEED2DLEGWMzSxWJyQynXE91LMLgV1J';
11
+ const MOCK_BTCB_MINT = 'BTCB3ripBAut19jM8kDPVbJHb2ZdR2GcZvGZkCmFPtV8';
12
+ const MOCK_ASSET_ROUTER = 'LomVyJDZ91jeVbNnTupJXKJTQFakJVMc87CmwDHYt95';
13
+ const MOCK_MAILBOX = 'LomJw912MoUd7iiAesTQAgz1paLcTqi6ndG3w3pnKH9';
14
+ const MOCK_SOLANA_CHAIN_ID = '0259db5080fc2c6d3bcf7ca90712d3c2e5e6c28f27f0dfbb9953bdb0894c03ab';
15
+ const MOCK_LEDGER_CHAIN_ID = '031f51c4e4cc1dae1c752d2f8fe2ae045da668a13f2e47a465964d630f5ed22e';
16
+ const MOCK_PAYER = '8yarEiDaJVikHZbk3PQSoWiDn2T3oM1FHZN1Jv4VZFdr';
17
+ const MOCK_RECIPIENT = 'DVMiNi7uxHEPABTBt1nLMoxnPniPKbLAFj4MPJq1RDjg';
18
+ /** Distinct pubkey returned for ATA(mint=LBTC, owner=recipient) */
19
+ const MOCK_RECIPIENT_ATA = 'GRq2yasTvWWPPqSwxCZvqfCTfDhP3MswDH4nW2v6F5To';
20
+ /** Distinct pubkey returned for ATA(mint=BTC.b, owner=payer) */
21
+ const MOCK_PAYER_ATA = 'GfYV1f1bR9vy41mSyQ8quxYbds121kijSBj5A3nG8oDQ';
22
+
23
+ const fullConfig: IConfig = {
24
+ lbtcTokenMint: MOCK_LBTC_MINT,
25
+ btcbTokenMint: MOCK_BTCB_MINT,
26
+ assetRouter: MOCK_ASSET_ROUTER,
27
+ mailbox: MOCK_MAILBOX,
28
+ solanaRoutingChainId: MOCK_SOLANA_CHAIN_ID,
29
+ bitcoinRoutingChainId: 'ff000008819873e925422c1ff0f99f7cc9bbb232af63a077a480a3633bee1ef6',
30
+ ledgerChainId: MOCK_LEDGER_CHAIN_ID,
31
+ lbtcProgramId: 'HEY7PCJe3GB27UWdopuYb1xDbB5SNtTcYPxRjntvfBSA',
32
+ treasuryAddress: 'ByHNGi4zPJw5StyWZoLQJ9n2wT12oupJF2pTSNKMnnAZ',
33
+ bascule: null,
34
+ basculeData: null,
35
+ admin: '6MKjyWZnkSMitJYAixvJzqhJiVsjTA3hYHX8aP9qNioj',
36
+ lzOftAdapter: 'AEFwQgaSNhQcZhAcGZGM9iTyGML3fsJC2aBvYmzV81FE',
37
+ lzOftStore: '3SG3oyrG3KSvJ9bbxPDu7ZXEe5o1TW1QkgudkKvK6FK4',
38
+ lzMultisig: 'GfYV1f1bR9vy41mSyQ8quxYbds121kijSBj5A3nG8oDQ',
39
+ lzEscrow: 'GRq2yasTvWWPPqSwxCZvqfCTfDhP3MswDH4nW2v6F5To',
40
+ consortium: null,
41
+ ratioOracle: null,
42
+ bridge: null,
43
+ lombardTokenPool: null,
44
+ };
45
+
46
+ // ── Mocks ──
47
+
48
+ vi.mock('../../const/getConfig', () => ({
49
+ DEFAULT_ENV: 'dev' as const,
50
+ networkToEnv: { devnet: 'dev', testnet: 'testnet', 'mainnet-beta': 'prod' },
51
+ getConfig: vi.fn(() => fullConfig),
52
+ }));
53
+
54
+ function buildArConfigData(paused = false) {
55
+ const data = Buffer.alloc(145, 0);
56
+ const treasury = new PublicKey(MOCK_PAYER);
57
+ treasury.toBuffer().copy(data, 72);
58
+ data[104] = paused ? 1 : 0;
59
+ data.writeBigUInt64LE(42n, 137);
60
+ return data;
61
+ }
62
+
63
+ function buildMailboxConfigData() {
64
+ const data = Buffer.alloc(145, 0);
65
+ const treasury = new PublicKey(MOCK_PAYER);
66
+ treasury.toBuffer().copy(data, 72);
67
+ data.writeBigUInt64LE(42n, 137);
68
+ return data;
69
+ }
70
+
71
+ const arConfigPDA = PublicKey.findProgramAddressSync(
72
+ [Buffer.from('asset_router_config')],
73
+ new PublicKey(MOCK_ASSET_ROUTER),
74
+ )[0];
75
+
76
+ function defaultGetAccountInfo(pubkey: PublicKey) {
77
+ if (pubkey.toBase58() === arConfigPDA.toBase58()) {
78
+ return Promise.resolve({ data: buildArConfigData() });
79
+ }
80
+ return Promise.resolve({ data: buildMailboxConfigData() });
81
+ }
82
+
83
+ const mockConnection = {
84
+ getAccountInfo: vi.fn().mockImplementation(defaultGetAccountInfo),
85
+ getTokenAccountBalance: vi.fn().mockResolvedValue({
86
+ value: { amount: '999999999', uiAmountString: '9.99' },
87
+ }),
88
+ };
89
+
90
+ vi.mock('../../const/rpcUrls', () => ({
91
+ getConnection: vi.fn(() => mockConnection),
92
+ }));
93
+
94
+ vi.mock('../../utils/tokenAccount', () => ({
95
+ getTokenProgramForMint: vi.fn().mockResolvedValue(new PublicKey('TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA')),
96
+ }));
97
+
98
+ vi.mock('../../idl/getAssetRouterIdl', () => ({
99
+ getAssetRouterIdl: vi.fn(() => ({
100
+ address: MOCK_ASSET_ROUTER,
101
+ metadata: { name: 'asset_router', version: '0.1.0', spec: '0.1.0' },
102
+ instructions: [],
103
+ })),
104
+ }));
105
+
106
+ vi.mock('@solana/spl-token', () => ({
107
+ getAssociatedTokenAddress: vi
108
+ .fn()
109
+ .mockImplementation(async (mint: PublicKey) => {
110
+ if (mint.toBase58() === MOCK_LBTC_MINT) {
111
+ return new PublicKey(MOCK_RECIPIENT_ATA);
112
+ }
113
+ return new PublicKey(MOCK_PAYER_ATA);
114
+ }),
115
+ ASSOCIATED_TOKEN_PROGRAM_ID: new PublicKey('ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL'),
116
+ }));
117
+
118
+ const mockTx = { instructions: [{ keys: [] }] };
119
+ const mockMethods = {
120
+ deposit: vi.fn().mockReturnValue({
121
+ accounts: vi.fn().mockReturnValue({
122
+ transaction: vi.fn().mockResolvedValue(mockTx),
123
+ }),
124
+ }),
125
+ };
126
+
127
+ vi.mock('@coral-xyz/anchor', () => ({
128
+ Program: vi.fn().mockImplementation(() => ({ methods: mockMethods })),
129
+ BN: vi.fn().mockImplementation((v: string) => ({ toString: () => v })),
130
+ }));
131
+
132
+ vi.mock('../../utils', async () => {
133
+ const actual = await vi.importActual('../../utils');
134
+ return {
135
+ ...actual,
136
+ sendAndConfirmTransaction: vi.fn().mockResolvedValue({
137
+ signature: 'mock-deposit-sig',
138
+ }),
139
+ };
140
+ });
141
+
142
+ const baseParams = {
143
+ amount: '100000',
144
+ recipient: MOCK_RECIPIENT,
145
+ network: SolanaNetwork.devnet,
146
+ };
147
+
148
+ function testWallet(pubkeyBase58: string): ISolanaWalletProvider {
149
+ const pk = new PublicKey(pubkeyBase58);
150
+ return {
151
+ isConnected: true,
152
+ publicKey: pk,
153
+ connect: async () => {},
154
+ disconnect: async () => {},
155
+ signMessage: async () => ({ signature: new Uint8Array(), publicKey: pk }),
156
+ signTransaction: async <T>(tx: T) => tx,
157
+ signAllTransactions: async <T>(txs: T[]) => txs,
158
+ };
159
+ }
160
+
161
+ const disconnectedWallet: ISolanaWalletProvider = {
162
+ isConnected: false,
163
+ publicKey: null as unknown as PublicKey,
164
+ connect: async () => {},
165
+ disconnect: async () => {},
166
+ signMessage: async () => {
167
+ throw new Error('not used');
168
+ },
169
+ signTransaction: async <T>(tx: T) => tx,
170
+ signAllTransactions: async <T>(txs: T[]) => txs,
171
+ };
172
+
173
+ describe('deposit', () => {
174
+ let depositFn: typeof import('./deposit').deposit;
175
+
176
+ beforeEach(async () => {
177
+ vi.clearAllMocks();
178
+ mockConnection.getAccountInfo.mockImplementation(defaultGetAccountInfo);
179
+ mockConnection.getTokenAccountBalance.mockResolvedValue({
180
+ value: { amount: '999999999', uiAmountString: '9.99' },
181
+ });
182
+ const mod = await import('./deposit');
183
+ depositFn = mod.deposit;
184
+ });
185
+
186
+ afterEach(() => {
187
+ vi.clearAllMocks();
188
+ });
189
+
190
+ it('should throw when wallet is not connected', async () => {
191
+ await expect(depositFn(disconnectedWallet, baseParams)).rejects.toThrow(
192
+ 'Wallet not connected',
193
+ );
194
+ });
195
+
196
+ it('should throw when Asset Router is not configured', async () => {
197
+ const { getConfig } = await import('../../const/getConfig');
198
+ vi.mocked(getConfig).mockReturnValueOnce({ ...fullConfig, assetRouter: null });
199
+
200
+ await expect(
201
+ depositFn(testWallet(MOCK_PAYER), baseParams),
202
+ ).rejects.toThrow('Asset Router not configured');
203
+ });
204
+
205
+ it('should throw when Mailbox is not configured', async () => {
206
+ const { getConfig } = await import('../../const/getConfig');
207
+ vi.mocked(getConfig).mockReturnValueOnce({ ...fullConfig, mailbox: null });
208
+
209
+ await expect(
210
+ depositFn(testWallet(MOCK_PAYER), baseParams),
211
+ ).rejects.toThrow('Mailbox not configured');
212
+ });
213
+
214
+ it('should throw when Solana routing chain ID is not configured', async () => {
215
+ const { getConfig } = await import('../../const/getConfig');
216
+ vi.mocked(getConfig).mockReturnValueOnce({
217
+ ...fullConfig,
218
+ solanaRoutingChainId: null,
219
+ });
220
+
221
+ await expect(
222
+ depositFn(testWallet(MOCK_PAYER), baseParams),
223
+ ).rejects.toThrow('Solana routing chain ID not configured');
224
+ });
225
+
226
+ it('should throw when source token mint is not resolved', async () => {
227
+ const { getConfig } = await import('../../const/getConfig');
228
+ vi.mocked(getConfig).mockReturnValueOnce({ ...fullConfig, btcbTokenMint: '' });
229
+
230
+ await expect(
231
+ depositFn(testWallet(MOCK_PAYER), baseParams),
232
+ ).rejects.toThrow('Source token mint not configured');
233
+ });
234
+
235
+ it('should throw when destination token is not configured', async () => {
236
+ const { getConfig } = await import('../../const/getConfig');
237
+ vi.mocked(getConfig).mockReturnValueOnce({ ...fullConfig, lbtcTokenMint: '' });
238
+
239
+ await expect(
240
+ depositFn(testWallet(MOCK_PAYER), baseParams),
241
+ ).rejects.toThrow('Destination token not configured');
242
+ });
243
+
244
+ it('should throw when amount is zero', async () => {
245
+ await expect(
246
+ depositFn(testWallet(MOCK_PAYER), { ...baseParams, amount: '0' }),
247
+ ).rejects.toThrow('greater than zero');
248
+ });
249
+
250
+ it('should throw when Asset Router is paused', async () => {
251
+ mockConnection.getAccountInfo.mockImplementation((pubkey: PublicKey) => {
252
+ if (pubkey.toBase58() === arConfigPDA.toBase58()) {
253
+ return Promise.resolve({ data: buildArConfigData(true) });
254
+ }
255
+ return Promise.resolve({ data: buildMailboxConfigData() });
256
+ });
257
+
258
+ await expect(
259
+ depositFn(testWallet(MOCK_PAYER), baseParams),
260
+ ).rejects.toThrow('Asset Router is paused');
261
+ });
262
+
263
+ it('should throw on insufficient balance', async () => {
264
+ mockConnection.getTokenAccountBalance.mockResolvedValue({
265
+ value: { amount: '10', uiAmountString: '0.0000001' },
266
+ });
267
+
268
+ await expect(
269
+ depositFn(testWallet(MOCK_PAYER), baseParams),
270
+ ).rejects.toThrow('Insufficient balance');
271
+ });
272
+
273
+ it('should throw when Ledger chain ID is not configured', async () => {
274
+ const { getConfig } = await import('../../const/getConfig');
275
+ vi.mocked(getConfig).mockReturnValueOnce({ ...fullConfig, ledgerChainId: null });
276
+
277
+ await expect(
278
+ depositFn(testWallet(MOCK_PAYER), baseParams),
279
+ ).rejects.toThrow('Ledger chain ID not configured');
280
+ });
281
+
282
+ it('should return transaction signature on success', async () => {
283
+ mockConnection.getAccountInfo.mockResolvedValue({ data: buildMailboxConfigData() });
284
+
285
+ const sig = await depositFn(testWallet(MOCK_PAYER), baseParams);
286
+
287
+ expect(sig).toBe('mock-deposit-sig');
288
+ });
289
+
290
+ it('should pass destination associated token account bytes to deposit()', async () => {
291
+ mockConnection.getAccountInfo.mockResolvedValue({ data: buildMailboxConfigData() });
292
+
293
+ await depositFn(testWallet(MOCK_PAYER), baseParams);
294
+
295
+ expect(mockMethods.deposit).toHaveBeenCalled();
296
+ const recipientArg = mockMethods.deposit.mock.calls[0][2] as number[];
297
+ expect(recipientArg).toEqual(
298
+ Array.from(new PublicKey(MOCK_RECIPIENT_ATA).toBytes()),
299
+ );
300
+ });
301
+
302
+ it('should wrap errors with SolanaSdkError', async () => {
303
+ const { getConfig } = await import('../../const/getConfig');
304
+ vi.mocked(getConfig).mockReturnValueOnce({ ...fullConfig, assetRouter: null });
305
+
306
+ try {
307
+ await depositFn(testWallet(MOCK_PAYER), baseParams);
308
+ expect.unreachable('should have thrown');
309
+ } catch (err) {
310
+ expect(err).toBeInstanceOf(SolanaSdkError);
311
+ expect((err as SolanaSdkError).code).toBe(ErrorCode.DEPOSIT_REJECTED);
312
+ }
313
+ });
314
+
315
+ it('should use env override when provided', async () => {
316
+ mockConnection.getAccountInfo.mockResolvedValue({ data: buildMailboxConfigData() });
317
+
318
+ const { getConfig } = await import('../../const/getConfig');
319
+
320
+ await depositFn(testWallet(MOCK_PAYER), {
321
+ ...baseParams,
322
+ env: 'stage',
323
+ });
324
+
325
+ expect(getConfig).toHaveBeenCalledWith('stage');
326
+ });
327
+ });
@@ -0,0 +1,339 @@
1
+ import { BN, Program } from '@coral-xyz/anchor';
2
+ import { Env } from '@lombard.finance/sdk-common';
3
+ import {
4
+ ASSOCIATED_TOKEN_PROGRAM_ID,
5
+ getAssociatedTokenAddress,
6
+ } from '@solana/spl-token';
7
+ import { PublicKey, SystemProgram } from '@solana/web3.js';
8
+
9
+ import { DEFAULT_ENV, getConfig, networkToEnv } from '../../const/getConfig';
10
+ import { getConnection } from '../../const/rpcUrls';
11
+ import { getAssetRouterIdl } from '../../idl/getAssetRouterIdl';
12
+ import { ISolanaWalletProvider, SolanaNetwork } from '../../types';
13
+ import {
14
+ ErrorCode,
15
+ sendAndConfirmTransaction,
16
+ SolanaSdkError,
17
+ } from '../../utils';
18
+ import { createDebugLogger } from '../../utils/createDebugLogger';
19
+ import { getTokenProgramForMint } from '../../utils/tokenAccount';
20
+ import { validateAmount } from '../redeemToken/shared';
21
+
22
+ export interface DepositParams {
23
+ amount: string;
24
+ /**
25
+ * Owner wallet that will receive the destination token (e.g. LBTC).
26
+ * Solana pubkey (base58). The GMP payload uses the associated token account
27
+ * for `toTokenAddress` / default LBTC mint — matching api backend and claimer expectations.
28
+ */
29
+ recipient: string;
30
+ /**
31
+ * Source token mint to burn (e.g. BTC.b). Defaults to `config.btcbTokenMint`.
32
+ */
33
+ sourceTokenMint?: string;
34
+ /**
35
+ * Destination Lombard routing chain ID (hex, 32 bytes).
36
+ * Defaults to `config.solanaRoutingChainId` (same-chain deposit).
37
+ */
38
+ toLchainId?: string;
39
+ /**
40
+ * Destination token address / mint (e.g. LBTC). Defaults to `config.lbtcTokenMint`.
41
+ */
42
+ toTokenAddress?: string;
43
+ network: SolanaNetwork;
44
+ env?: Env;
45
+ rpcUrl?: string;
46
+ debug?: boolean;
47
+ skipPreflight?: boolean;
48
+ }
49
+
50
+ /**
51
+ * Deposit (swap) source token to destination token via Asset Router's `deposit` instruction.
52
+ *
53
+ * Burns the source token (e.g. BTC.b) and sends a GMP message through the Mailbox
54
+ * to mint the destination token (e.g. LBTC) to the recipient's ATA for that mint.
55
+ *
56
+ * Default flow (all optional params omitted): BTC.b → LBTC on Solana.
57
+ */
58
+ export async function deposit(
59
+ provider: ISolanaWalletProvider,
60
+ params: DepositParams,
61
+ ): Promise<string> {
62
+ const {
63
+ amount,
64
+ recipient,
65
+ network,
66
+ env: envOverride,
67
+ rpcUrl,
68
+ debug = false,
69
+ skipPreflight = true,
70
+ } = params;
71
+ const { debugLog, printLogs } = createDebugLogger({ debug });
72
+
73
+ try {
74
+ if (!provider.publicKey) {
75
+ throw new Error('Wallet not connected');
76
+ }
77
+
78
+ const env = envOverride ?? networkToEnv[network] ?? DEFAULT_ENV;
79
+ const config = getConfig(env);
80
+
81
+ if (!config.assetRouter) {
82
+ throw new Error(`Asset Router not configured for network: ${network}`);
83
+ }
84
+ if (!config.mailbox) {
85
+ throw new Error(`Mailbox not configured for network: ${network}`);
86
+ }
87
+ if (!config.solanaRoutingChainId) {
88
+ throw new Error(
89
+ `Solana routing chain ID not configured for network: ${network}`,
90
+ );
91
+ }
92
+
93
+ const sourceMintAddress = params.sourceTokenMint ?? config.btcbTokenMint;
94
+ if (!sourceMintAddress) {
95
+ throw new Error(
96
+ `Source token mint not configured for network: ${network}`,
97
+ );
98
+ }
99
+
100
+ const toLchainIdHex = params.toLchainId ?? config.solanaRoutingChainId;
101
+ const toTokenAddressStr = params.toTokenAddress ?? config.lbtcTokenMint;
102
+ if (!toTokenAddressStr) {
103
+ throw new Error(
104
+ `Destination token not configured for network: ${network}`,
105
+ );
106
+ }
107
+
108
+ validateAmount(amount);
109
+
110
+ const connection = getConnection(network, rpcUrl);
111
+ const payer = new PublicKey(provider.publicKey);
112
+ const mint = new PublicKey(sourceMintAddress);
113
+ const recipientPubkey = new PublicKey(recipient);
114
+ const assetRouterProgramId = new PublicKey(config.assetRouter);
115
+ const mailboxProgramId = new PublicKey(config.mailbox);
116
+ const solanaRoutingChainId = Buffer.from(
117
+ config.solanaRoutingChainId,
118
+ 'hex',
119
+ );
120
+ const toLchainId = Buffer.from(toLchainIdHex, 'hex');
121
+ const destinationMint = new PublicKey(toTokenAddressStr);
122
+ const toTokenAddress = destinationMint.toBytes();
123
+
124
+ debugLog('Payer:', payer.toBase58());
125
+ debugLog('Mint (source, e.g. BTC.b):', mint.toBase58());
126
+ debugLog('Destination token (e.g. LBTC):', toTokenAddressStr);
127
+ debugLog('Recipient (owner):', recipientPubkey.toBase58());
128
+ debugLog('Amount:', amount);
129
+
130
+ const [tokenProgramId, destinationTokenProgramId] = await Promise.all([
131
+ getTokenProgramForMint(connection, mint),
132
+ getTokenProgramForMint(connection, destinationMint),
133
+ ]);
134
+ debugLog('Source token program:', tokenProgramId.toBase58());
135
+ debugLog('Destination token program:', destinationTokenProgramId.toBase58());
136
+
137
+ const [assetRouterConfigPDA] = PublicKey.findProgramAddressSync(
138
+ [Buffer.from('asset_router_config')],
139
+ assetRouterProgramId,
140
+ );
141
+ const [tokenRoutePDA] = PublicKey.findProgramAddressSync(
142
+ [
143
+ Buffer.from('token_route'),
144
+ solanaRoutingChainId,
145
+ mint.toBuffer(),
146
+ toLchainId,
147
+ Buffer.from(toTokenAddress),
148
+ ],
149
+ assetRouterProgramId,
150
+ );
151
+ const [messagingAuthorityPDA] = PublicKey.findProgramAddressSync(
152
+ [Buffer.from('messaging_authority')],
153
+ assetRouterProgramId,
154
+ );
155
+
156
+ debugLog('Asset Router config PDA:', assetRouterConfigPDA.toBase58());
157
+ debugLog('Token route PDA:', tokenRoutePDA.toBase58());
158
+ debugLog('Messaging authority PDA:', messagingAuthorityPDA.toBase58());
159
+
160
+ const [mailboxConfigPDA] = PublicKey.findProgramAddressSync(
161
+ [Buffer.from('mailbox_config')],
162
+ mailboxProgramId,
163
+ );
164
+ if (!config.ledgerChainId) {
165
+ throw new Error(
166
+ `Ledger chain ID not configured for network: ${network}`,
167
+ );
168
+ }
169
+ const ledgerChainId = Buffer.from(config.ledgerChainId, 'hex');
170
+ const [outboundMessagePathPDA] = PublicKey.findProgramAddressSync(
171
+ [Buffer.from('outbound_message_path'), ledgerChainId],
172
+ mailboxProgramId,
173
+ );
174
+ const [senderConfigPDA] = PublicKey.findProgramAddressSync(
175
+ [Buffer.from('sender_config'), assetRouterProgramId.toBuffer()],
176
+ mailboxProgramId,
177
+ );
178
+
179
+ debugLog('Mailbox config PDA:', mailboxConfigPDA.toBase58());
180
+ debugLog(
181
+ 'Outbound message path PDA:',
182
+ outboundMessagePathPDA.toBase58(),
183
+ );
184
+ debugLog('Sender config PDA:', senderConfigPDA.toBase58());
185
+
186
+ const [arConfigInfo, mailboxConfigInfo] = await Promise.all([
187
+ connection.getAccountInfo(assetRouterConfigPDA),
188
+ connection.getAccountInfo(mailboxConfigPDA),
189
+ ]);
190
+
191
+ if (!arConfigInfo) {
192
+ throw new Error('Asset Router config account not found');
193
+ }
194
+ if (!mailboxConfigInfo) {
195
+ throw new Error('Mailbox config account not found');
196
+ }
197
+
198
+ if (arConfigInfo.data.length < 105) {
199
+ throw new Error(
200
+ `Asset Router config account data too short: expected >= 105 bytes, got ${arConfigInfo.data.length}`,
201
+ );
202
+ }
203
+ const paused = arConfigInfo.data[104] !== 0;
204
+ if (paused) {
205
+ throw new Error('Asset Router is paused');
206
+ }
207
+
208
+ if (mailboxConfigInfo.data.length < 104) {
209
+ throw new Error(
210
+ `Mailbox config account data too short: expected >= 104 bytes, got ${mailboxConfigInfo.data.length}`,
211
+ );
212
+ }
213
+ const mailboxTreasury = new PublicKey(
214
+ mailboxConfigInfo.data.subarray(72, 104),
215
+ );
216
+ debugLog('Mailbox treasury:', mailboxTreasury.toBase58());
217
+
218
+ const payerTokenAccount = await getAssociatedTokenAddress(
219
+ mint,
220
+ payer,
221
+ false,
222
+ tokenProgramId,
223
+ ASSOCIATED_TOKEN_PROGRAM_ID,
224
+ );
225
+ const recipientTokenAccount = await getAssociatedTokenAddress(
226
+ destinationMint,
227
+ recipientPubkey,
228
+ false,
229
+ destinationTokenProgramId,
230
+ ASSOCIATED_TOKEN_PROGRAM_ID,
231
+ );
232
+ debugLog('Payer token account:', payerTokenAccount.toBase58());
233
+ debugLog('Recipient token account (payload):', recipientTokenAccount.toBase58());
234
+
235
+ const tokenBalance =
236
+ await connection.getTokenAccountBalance(payerTokenAccount);
237
+ const userBalance = BigInt(tokenBalance.value.amount);
238
+ const parsedAmount = BigInt(amount);
239
+ if (userBalance < parsedAmount) {
240
+ throw new Error(
241
+ `Insufficient balance: have ${tokenBalance.value.uiAmountString}, need ${Number(parsedAmount) / 1e8}`,
242
+ );
243
+ }
244
+
245
+ const assetRouterProgram = new Program(
246
+ getAssetRouterIdl(env),
247
+ { connection },
248
+ );
249
+
250
+ const toLchainIdArray = Array.from(toLchainId);
251
+ const toTokenAddressArray = Array.from(toTokenAddress);
252
+ const recipientArray = Array.from(recipientTokenAccount.toBytes());
253
+
254
+ const MAX_NONCE_RETRIES = 3;
255
+ for (let attempt = 0; attempt < MAX_NONCE_RETRIES; attempt++) {
256
+ const freshMailboxConfig =
257
+ await connection.getAccountInfo(mailboxConfigPDA);
258
+ if (!freshMailboxConfig) {
259
+ throw new Error('Mailbox config account not found');
260
+ }
261
+ if (freshMailboxConfig.data.length < 145) {
262
+ throw new Error(
263
+ `Mailbox config account data too short: expected >= 145 bytes, got ${freshMailboxConfig.data.length}`,
264
+ );
265
+ }
266
+ const globalNonce = freshMailboxConfig.data.readBigUInt64LE(137);
267
+ const nonceBuf = Buffer.alloc(8);
268
+ nonceBuf.writeBigUInt64BE(globalNonce);
269
+
270
+ const [outboundMessagePDA] = PublicKey.findProgramAddressSync(
271
+ [Buffer.from('outbound_message'), nonceBuf],
272
+ mailboxProgramId,
273
+ );
274
+
275
+ debugLog(
276
+ `Attempt ${attempt + 1}: global nonce=${globalNonce}, outbound_message=${outboundMessagePDA.toBase58()}`,
277
+ );
278
+
279
+ const tx = await assetRouterProgram.methods
280
+ .deposit(
281
+ toLchainIdArray,
282
+ toTokenAddressArray,
283
+ recipientArray,
284
+ new BN(amount),
285
+ )
286
+ .accounts({
287
+ payer,
288
+ config: assetRouterConfigPDA,
289
+ tokenRoute: tokenRoutePDA,
290
+ payerTokenAccount,
291
+ tokenProgram: tokenProgramId,
292
+ mint,
293
+ messagingAuthority: messagingAuthorityPDA,
294
+ mailbox: mailboxProgramId,
295
+ mailboxConfig: mailboxConfigPDA,
296
+ outboundMessagePath: outboundMessagePathPDA,
297
+ outboundMessage: outboundMessagePDA,
298
+ senderConfig: senderConfigPDA,
299
+ treasury: mailboxTreasury,
300
+ systemProgram: SystemProgram.programId,
301
+ })
302
+ .transaction();
303
+
304
+ debugLog('Instruction account count:', tx.instructions[0]?.keys.length);
305
+
306
+ try {
307
+ const { signature } = await sendAndConfirmTransaction({
308
+ instruction: tx,
309
+ connection,
310
+ provider,
311
+ debugLabel: 'Asset Router deposit',
312
+ skipPreflight,
313
+ });
314
+
315
+ debugLog('deposit completed, signature:', signature);
316
+ return signature;
317
+ } catch (err: unknown) {
318
+ const isNonceError =
319
+ err instanceof Error && err.message.includes('0x7d6');
320
+ if (isNonceError && attempt < MAX_NONCE_RETRIES - 1) {
321
+ debugLog('Nonce stale (ConstraintSeeds), retrying...');
322
+ continue;
323
+ }
324
+ throw err;
325
+ }
326
+ }
327
+
328
+ throw new Error('Failed after max nonce retries');
329
+ } catch (error: unknown) {
330
+ if (error instanceof Error && debug) {
331
+ error.message = `${error.message}\n\nDebug logs:\n${printLogs()}`;
332
+ }
333
+ throw SolanaSdkError.wrap(
334
+ error,
335
+ ErrorCode.DEPOSIT_REJECTED,
336
+ 'deposit operation failed',
337
+ );
338
+ }
339
+ }
@@ -0,0 +1 @@
1
+ export { deposit, type DepositParams } from './deposit';