@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,166 @@
1
+ import { Env } from '@lombard.finance/sdk-common';
2
+ import type { Meta, StoryObj } from '@storybook/react';
3
+
4
+ import { envToNetwork, getConfig } from '../../const/getConfig';
5
+ import {
6
+ Button,
7
+ CodeBlock,
8
+ ErrorDisplay,
9
+ } from '../../stories/components';
10
+ import { functionType } from '../../stories/decorators/function-type';
11
+ import useQuery from '../../stories/hooks/useQuery';
12
+ import {
13
+ getMinRedeemAmountSolana,
14
+ getMinRedeemAmountWithFeeSolana,
15
+ getMintingFeeSolana,
16
+ getRedeemFeeSolana,
17
+ getTokenFeeConfig,
18
+ } from './getTokenFeeConfig';
19
+
20
+ type TokenChoice = 'LBTC' | 'BTC.b';
21
+
22
+ interface StoryArgs {
23
+ environment: Env;
24
+ token: TokenChoice;
25
+ }
26
+
27
+ function resolveTokenMint(
28
+ token: TokenChoice,
29
+ env: Env,
30
+ ): string | undefined {
31
+ const config = getConfig(env);
32
+ return token === 'LBTC'
33
+ ? config.lbtcTokenMint
34
+ : config.btcbTokenMint ?? undefined;
35
+ }
36
+
37
+ export function StoryView({ environment, token }: StoryArgs) {
38
+ const network = envToNetwork[environment];
39
+ const tokenMint = resolveTokenMint(token, environment);
40
+
41
+ const params = { network, env: environment, tokenMint };
42
+
43
+ const configQuery = useQuery(
44
+ () => getTokenFeeConfig(params),
45
+ [environment, token],
46
+ false,
47
+ );
48
+ const redeemFeeQuery = useQuery(
49
+ () => getRedeemFeeSolana(params),
50
+ [environment, token],
51
+ false,
52
+ );
53
+ const mintingFeeQuery = useQuery(
54
+ () => getMintingFeeSolana(params),
55
+ [environment, token],
56
+ false,
57
+ );
58
+ const minRedeemQuery = useQuery(
59
+ () => getMinRedeemAmountSolana(params),
60
+ [environment, token],
61
+ false,
62
+ );
63
+ const minRedeemWithFeeQuery = useQuery(
64
+ () => getMinRedeemAmountWithFeeSolana(params),
65
+ [environment, token],
66
+ false,
67
+ );
68
+
69
+ const isLoading =
70
+ configQuery.isLoading ||
71
+ redeemFeeQuery.isLoading ||
72
+ mintingFeeQuery.isLoading ||
73
+ minRedeemQuery.isLoading ||
74
+ minRedeemWithFeeQuery.isLoading;
75
+
76
+ const fetchAll = () => {
77
+ configQuery.refetch();
78
+ redeemFeeQuery.refetch();
79
+ mintingFeeQuery.refetch();
80
+ minRedeemQuery.refetch();
81
+ minRedeemWithFeeQuery.refetch();
82
+ };
83
+
84
+ const error =
85
+ configQuery.error ||
86
+ redeemFeeQuery.error ||
87
+ mintingFeeQuery.error ||
88
+ minRedeemQuery.error ||
89
+ minRedeemWithFeeQuery.error;
90
+
91
+ const summary = configQuery.data
92
+ ? {
93
+ redeemFee: configQuery.data.redeemFee.toFormat(),
94
+ redeemForBtcMinAmount:
95
+ configQuery.data.redeemForBtcMinAmount.toFormat(),
96
+ maxMintCommission: configQuery.data.maxMintCommission.toFormat(),
97
+ toNativeCommission:
98
+ configQuery.data.toNativeCommission.toFormat(),
99
+ 'getRedeemFeeSolana (toNative + redeem)':
100
+ redeemFeeQuery.data?.toFormat(),
101
+ 'getMintingFeeSolana': mintingFeeQuery.data?.toFormat(),
102
+ 'getMinRedeemAmountSolana': minRedeemQuery.data?.toFormat(),
103
+ 'getMinRedeemAmountWithFeeSolana':
104
+ minRedeemWithFeeQuery.data?.toFormat(),
105
+ }
106
+ : undefined;
107
+
108
+ return (
109
+ <div>
110
+ <p>
111
+ <strong>Network:</strong> {network} | <strong>Token:</strong>{' '}
112
+ {token}
113
+ {tokenMint ? ` (${tokenMint})` : ' — not configured'}
114
+ </p>
115
+
116
+ <Button
117
+ primary
118
+ size="large"
119
+ onClick={fetchAll}
120
+ isLoading={isLoading}
121
+ actionName={getTokenFeeConfig.name}
122
+ />
123
+
124
+ {summary && <CodeBlock text={summary} />}
125
+ {error && <ErrorDisplay error={error} title="Error" />}
126
+ </div>
127
+ );
128
+ }
129
+
130
+ const meta: Meta<typeof StoryView> = {
131
+ title: 'read/getTokenFeeConfig (Asset Router)',
132
+ component: StoryView,
133
+ tags: ['autodocs'],
134
+ decorators: [functionType('read')],
135
+ parameters: {
136
+ docs: {
137
+ description: {
138
+ component: `Reads the Asset Router \`TokenConfig\` account for a given token mint on Solana.
139
+
140
+ Returns protocol fee parameters: **redeem fee**, **min redeem amount**, **max mint commission**, and **native commission**.
141
+
142
+ Also demonstrates the convenience wrappers: \`getRedeemFeeSolana\`, \`getMintingFeeSolana\`, \`getMinRedeemAmountSolana\`, \`getMinRedeemAmountWithFeeSolana\`.`,
143
+ },
144
+ },
145
+ },
146
+ args: {
147
+ environment: Env.stage,
148
+ token: 'LBTC',
149
+ },
150
+ argTypes: {
151
+ environment: {
152
+ control: { type: 'select' },
153
+ options: Object.values(Env),
154
+ },
155
+ token: {
156
+ control: { type: 'select' },
157
+ options: ['LBTC', 'BTC.b'] satisfies TokenChoice[],
158
+ },
159
+ },
160
+ };
161
+ export default meta;
162
+ type Story = StoryObj<typeof meta>;
163
+
164
+ export const Stage: Story = {
165
+ args: { environment: Env.stage, token: 'LBTC' },
166
+ };
@@ -0,0 +1,224 @@
1
+ import { PublicKey } from '@solana/web3.js';
2
+ import BigNumber from 'bignumber.js';
3
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
4
+
5
+ import type { IConfig } from '../../const/getConfig';
6
+ import { SolanaNetwork } from '../../types';
7
+ import { ErrorCode, SolanaSdkError } from '../../utils';
8
+
9
+ // ── Constants ──
10
+
11
+ const MOCK_LBTC_MINT = 'LBTCojyVJ63rsEED2DLEGWMzSxWJyQynXE91LMLgV1J';
12
+ const MOCK_BTCB_MINT = 'BTCB3ripBAut19jM8kDPVbJHb2ZdR2GcZvGZkCmFPtV8';
13
+ const MOCK_ASSET_ROUTER = 'LomVyJDZ91jeVbNnTupJXKJTQFakJVMc87CmwDHYt95';
14
+
15
+ const fullConfig: IConfig = {
16
+ lbtcTokenMint: MOCK_LBTC_MINT,
17
+ btcbTokenMint: MOCK_BTCB_MINT,
18
+ assetRouter: MOCK_ASSET_ROUTER,
19
+ mailbox: 'LomJw912MoUd7iiAesTQAgz1paLcTqi6ndG3w3pnKH9',
20
+ solanaRoutingChainId:
21
+ '0259db5080fc2c6d3bcf7ca90712d3c2e5e6c28f27f0dfbb9953bdb0894c03ab',
22
+ bitcoinRoutingChainId:
23
+ 'ff000008819873e925422c1ff0f99f7cc9bbb232af63a077a480a3633bee1ef6',
24
+ ledgerChainId:
25
+ '031f51c4e4cc1dae1c752d2f8fe2ae045da668a13f2e47a465964d630f5ed22e',
26
+ lbtcProgramId: 'HEY7PCJe3GB27UWdopuYb1xDbB5SNtTcYPxRjntvfBSA',
27
+ treasuryAddress: 'ByHNGi4zPJw5StyWZoLQJ9n2wT12oupJF2pTSNKMnnAZ',
28
+ bascule: null,
29
+ basculeData: null,
30
+ admin: '6MKjyWZnkSMitJYAixvJzqhJiVsjTA3hYHX8aP9qNioj',
31
+ lzOftAdapter: 'AEFwQgaSNhQcZhAcGZGM9iTyGML3fsJC2aBvYmzV81FE',
32
+ lzOftStore: '3SG3oyrG3KSvJ9bbxPDu7ZXEe5o1TW1QkgudkKvK6FK4',
33
+ lzMultisig: 'GfYV1f1bR9vy41mSyQ8quxYbds121kijSBj5A3nG8oDQ',
34
+ lzEscrow: 'GRq2yasTvWWPPqSwxCZvqfCTfDhP3MswDH4nW2v6F5To',
35
+ consortium: null,
36
+ ratioOracle: null,
37
+ bridge: null,
38
+ lombardTokenPool: null,
39
+ };
40
+
41
+ // ── Helpers ──
42
+
43
+ /**
44
+ * Build a fake TokenConfig account buffer.
45
+ *
46
+ * Layout (Anchor): discriminator(8) + redeem_fee(u64) +
47
+ * redeem_for_btc_min_amount(u64) + max_mint_commission(u64) +
48
+ * to_native_commission(u64) + ledger_redeem_handler([u8; 32])
49
+ */
50
+ function buildTokenConfigData(fields: {
51
+ redeemFee: bigint;
52
+ redeemForBtcMinAmount: bigint;
53
+ maxMintCommission: bigint;
54
+ toNativeCommission: bigint;
55
+ }): Buffer {
56
+ const buf = Buffer.alloc(8 + 8 + 8 + 8 + 8 + 32, 0);
57
+ buf.writeBigUInt64LE(fields.redeemFee, 8);
58
+ buf.writeBigUInt64LE(fields.redeemForBtcMinAmount, 16);
59
+ buf.writeBigUInt64LE(fields.maxMintCommission, 24);
60
+ buf.writeBigUInt64LE(fields.toNativeCommission, 32);
61
+ return buf;
62
+ }
63
+
64
+ // ── Mocks ──
65
+
66
+ const mockConnection = {
67
+ getAccountInfo: vi.fn(),
68
+ };
69
+
70
+ vi.mock('../../const/getConfig', () => ({
71
+ DEFAULT_ENV: 'dev' as const,
72
+ getConfig: vi.fn(() => fullConfig),
73
+ }));
74
+
75
+ vi.mock('../../const/rpcUrls', () => ({
76
+ getConnection: vi.fn(() => mockConnection),
77
+ }));
78
+
79
+ // ── Tests ──
80
+
81
+ describe('getTokenFeeConfig', () => {
82
+ let getTokenFeeConfigFn: typeof import('./getTokenFeeConfig').getTokenFeeConfig;
83
+ let getRedeemFeeSolanaFn: typeof import('./getTokenFeeConfig').getRedeemFeeSolana;
84
+ let getMintingFeeSolanaFn: typeof import('./getTokenFeeConfig').getMintingFeeSolana;
85
+ let getMinRedeemAmountSolanaFn: typeof import('./getTokenFeeConfig').getMinRedeemAmountSolana;
86
+ let getMinRedeemAmountWithFeeSolanaFn: typeof import('./getTokenFeeConfig').getMinRedeemAmountWithFeeSolana;
87
+
88
+ const defaultFields = {
89
+ redeemFee: 1000n,
90
+ redeemForBtcMinAmount: 2000n,
91
+ maxMintCommission: 500n,
92
+ toNativeCommission: 300n,
93
+ };
94
+
95
+ beforeEach(async () => {
96
+ vi.clearAllMocks();
97
+
98
+ mockConnection.getAccountInfo.mockResolvedValue({
99
+ data: buildTokenConfigData(defaultFields),
100
+ });
101
+
102
+ const mod = await import('./getTokenFeeConfig');
103
+ getTokenFeeConfigFn = mod.getTokenFeeConfig;
104
+ getRedeemFeeSolanaFn = mod.getRedeemFeeSolana;
105
+ getMintingFeeSolanaFn = mod.getMintingFeeSolana;
106
+ getMinRedeemAmountSolanaFn = mod.getMinRedeemAmountSolana;
107
+ getMinRedeemAmountWithFeeSolanaFn = mod.getMinRedeemAmountWithFeeSolana;
108
+ });
109
+
110
+ afterEach(() => {
111
+ vi.clearAllMocks();
112
+ });
113
+
114
+ const baseParams = { network: SolanaNetwork.devnet };
115
+
116
+ // ── getTokenFeeConfig ──
117
+
118
+ it('should parse all token config fields correctly', async () => {
119
+ const result = await getTokenFeeConfigFn(baseParams);
120
+
121
+ expect(result.redeemFee).toEqual(new BigNumber('0.00001'));
122
+ expect(result.redeemForBtcMinAmount).toEqual(new BigNumber('0.00002'));
123
+ expect(result.maxMintCommission).toEqual(new BigNumber('0.000005'));
124
+ expect(result.toNativeCommission).toEqual(new BigNumber('0.000003'));
125
+ });
126
+
127
+ it('should derive PDA from LBTC mint by default', async () => {
128
+ await getTokenFeeConfigFn(baseParams);
129
+
130
+ const mint = new PublicKey(MOCK_LBTC_MINT);
131
+ const assetRouter = new PublicKey(MOCK_ASSET_ROUTER);
132
+ const [expectedPDA] = PublicKey.findProgramAddressSync(
133
+ [Buffer.from('token_config'), mint.toBuffer()],
134
+ assetRouter,
135
+ );
136
+
137
+ expect(mockConnection.getAccountInfo).toHaveBeenCalledWith(expectedPDA);
138
+ });
139
+
140
+ it('should use custom tokenMint when provided', async () => {
141
+ await getTokenFeeConfigFn({ ...baseParams, tokenMint: MOCK_BTCB_MINT });
142
+
143
+ const mint = new PublicKey(MOCK_BTCB_MINT);
144
+ const assetRouter = new PublicKey(MOCK_ASSET_ROUTER);
145
+ const [expectedPDA] = PublicKey.findProgramAddressSync(
146
+ [Buffer.from('token_config'), mint.toBuffer()],
147
+ assetRouter,
148
+ );
149
+
150
+ expect(mockConnection.getAccountInfo).toHaveBeenCalledWith(expectedPDA);
151
+ });
152
+
153
+ it('should throw when Asset Router is not configured', async () => {
154
+ const { getConfig } = await import('../../const/getConfig');
155
+ vi.mocked(getConfig).mockReturnValueOnce({
156
+ ...fullConfig,
157
+ assetRouter: null,
158
+ });
159
+
160
+ await expect(getTokenFeeConfigFn(baseParams)).rejects.toThrow(
161
+ 'Asset Router not configured',
162
+ );
163
+ });
164
+
165
+ it('should throw when account is not found', async () => {
166
+ mockConnection.getAccountInfo.mockResolvedValueOnce(null);
167
+
168
+ try {
169
+ await getTokenFeeConfigFn(baseParams);
170
+ expect.unreachable('should have thrown');
171
+ } catch (err) {
172
+ expect(err).toBeInstanceOf(SolanaSdkError);
173
+ expect((err as SolanaSdkError).code).toBe(ErrorCode.RPC_ERROR);
174
+ }
175
+ });
176
+
177
+ it('should throw when account data is too short', async () => {
178
+ mockConnection.getAccountInfo.mockResolvedValueOnce({
179
+ data: Buffer.alloc(10, 0),
180
+ });
181
+
182
+ try {
183
+ await getTokenFeeConfigFn(baseParams);
184
+ expect.unreachable('should have thrown');
185
+ } catch (err) {
186
+ expect(err).toBeInstanceOf(SolanaSdkError);
187
+ expect((err as SolanaSdkError).code).toBe(ErrorCode.RPC_ERROR);
188
+ }
189
+ });
190
+
191
+ it('should use env override when provided', async () => {
192
+ const { getConfig } = await import('../../const/getConfig');
193
+
194
+ await getTokenFeeConfigFn({ ...baseParams, env: 'stage' });
195
+
196
+ expect(getConfig).toHaveBeenCalledWith('stage');
197
+ });
198
+
199
+ // ── Convenience wrappers ──
200
+
201
+ it('getRedeemFeeSolana should return toNativeCommission + redeemFee', async () => {
202
+ const result = await getRedeemFeeSolanaFn(baseParams);
203
+ // 300 + 1000 = 1300 satoshi = 0.000013
204
+ expect(result).toEqual(new BigNumber('0.000013'));
205
+ });
206
+
207
+ it('getMintingFeeSolana should return maxMintCommission', async () => {
208
+ const result = await getMintingFeeSolanaFn(baseParams);
209
+ // 500 satoshi = 0.000005
210
+ expect(result).toEqual(new BigNumber('0.000005'));
211
+ });
212
+
213
+ it('getMinRedeemAmountSolana should return redeemForBtcMinAmount', async () => {
214
+ const result = await getMinRedeemAmountSolanaFn(baseParams);
215
+ // 2000 satoshi = 0.00002
216
+ expect(result).toEqual(new BigNumber('0.00002'));
217
+ });
218
+
219
+ it('getMinRedeemAmountWithFeeSolana should return total of all fees + min amount', async () => {
220
+ const result = await getMinRedeemAmountWithFeeSolanaFn(baseParams);
221
+ // 300 + 1000 + 2000 = 3300 satoshi = 0.000033
222
+ expect(result).toEqual(new BigNumber('0.000033'));
223
+ });
224
+ });
@@ -0,0 +1,154 @@
1
+ import { Env } from '@lombard.finance/sdk-common';
2
+ import { PublicKey } from '@solana/web3.js';
3
+ import BigNumber from 'bignumber.js';
4
+
5
+ import { DEFAULT_ENV, getConfig } from '../../const/getConfig';
6
+ import { getConnection } from '../../const/rpcUrls';
7
+ import { SolanaNetwork } from '../../types';
8
+ import { ErrorCode, SolanaSdkError } from '../../utils';
9
+
10
+ const BTC_DECIMALS = 8;
11
+ const SATOSHI_SCALE = new BigNumber(10).pow(BTC_DECIMALS);
12
+ const ANCHOR_DISCRIMINATOR_SIZE = 8;
13
+
14
+ /**
15
+ * Anchor account layout for Asset Router's `TokenConfig`:
16
+ * discriminator (8) | redeem_fee (u64) | redeem_for_btc_min_amount (u64)
17
+ * | max_mint_commission (u64) | to_native_commission (u64)
18
+ * | ledger_redeem_handler ([u8; 32])
19
+ */
20
+ const TOKEN_CONFIG_MIN_SIZE = ANCHOR_DISCRIMINATOR_SIZE + 8 + 8 + 8 + 8 + 32;
21
+
22
+ function fromSatoshi(amount: bigint): BigNumber {
23
+ return new BigNumber(amount.toString()).dividedBy(SATOSHI_SCALE);
24
+ }
25
+
26
+ function readU64LE(buf: Buffer, offset: number): bigint {
27
+ return buf.readBigUInt64LE(offset);
28
+ }
29
+
30
+ export interface GetTokenFeeConfigParams {
31
+ /** SPL token mint address. Defaults to LBTC mint from config. */
32
+ tokenMint?: string;
33
+ network: SolanaNetwork;
34
+ env?: Env;
35
+ rpcUrl?: string;
36
+ }
37
+
38
+ export interface TokenFeeConfigResult {
39
+ redeemFee: BigNumber;
40
+ redeemForBtcMinAmount: BigNumber;
41
+ maxMintCommission: BigNumber;
42
+ toNativeCommission: BigNumber;
43
+ }
44
+
45
+ /**
46
+ * Fetches the Asset Router `TokenConfig` account for a given token mint.
47
+ *
48
+ * The account holds protocol fee parameters set by governance:
49
+ * redeem fee, min redeem amount, max mint commission, and native commission.
50
+ */
51
+ export async function getTokenFeeConfig(
52
+ params: GetTokenFeeConfigParams,
53
+ ): Promise<TokenFeeConfigResult> {
54
+ const { network, env: envOverride, rpcUrl, tokenMint } = params;
55
+ const env = envOverride ?? DEFAULT_ENV;
56
+ const config = getConfig(env);
57
+
58
+ if (!config.assetRouter) {
59
+ throw new SolanaSdkError(
60
+ `Asset Router not configured for env: ${env}`,
61
+ ErrorCode.INVALID_PARAMS,
62
+ );
63
+ }
64
+
65
+ const mintAddress = tokenMint ?? config.lbtcTokenMint;
66
+ const mint = new PublicKey(mintAddress);
67
+ const assetRouterProgramId = new PublicKey(config.assetRouter);
68
+
69
+ const connection = getConnection(network, rpcUrl);
70
+
71
+ const [tokenConfigPDA] = PublicKey.findProgramAddressSync(
72
+ [Buffer.from('token_config'), mint.toBuffer()],
73
+ assetRouterProgramId,
74
+ );
75
+
76
+ try {
77
+ const accountInfo = await connection.getAccountInfo(tokenConfigPDA);
78
+
79
+ if (!accountInfo) {
80
+ throw new Error(
81
+ `TokenConfig account not found for mint ${mintAddress}`,
82
+ );
83
+ }
84
+
85
+ if (accountInfo.data.length < TOKEN_CONFIG_MIN_SIZE) {
86
+ throw new Error(
87
+ `TokenConfig account data too short: expected >= ${TOKEN_CONFIG_MIN_SIZE} bytes, got ${accountInfo.data.length}`,
88
+ );
89
+ }
90
+
91
+ const buf = accountInfo.data;
92
+ const off = ANCHOR_DISCRIMINATOR_SIZE;
93
+
94
+ return {
95
+ redeemFee: fromSatoshi(readU64LE(buf, off)),
96
+ redeemForBtcMinAmount: fromSatoshi(readU64LE(buf, off + 8)),
97
+ maxMintCommission: fromSatoshi(readU64LE(buf, off + 16)),
98
+ toNativeCommission: fromSatoshi(readU64LE(buf, off + 24)),
99
+ };
100
+ } catch (error) {
101
+ throw SolanaSdkError.wrap(
102
+ error,
103
+ ErrorCode.RPC_ERROR,
104
+ `Failed to fetch token config for mint ${mintAddress}`,
105
+ );
106
+ }
107
+ }
108
+
109
+ /**
110
+ * Total redeem fee for burning tokens back to BTC on Solana.
111
+ * Equivalent to EVM `getRedeemFee`: `toNativeCommission + redeemFee`.
112
+ */
113
+ export async function getRedeemFeeSolana(
114
+ params: GetTokenFeeConfigParams,
115
+ ): Promise<BigNumber> {
116
+ const cfg = await getTokenFeeConfig(params);
117
+ return cfg.toNativeCommission.plus(cfg.redeemFee);
118
+ }
119
+
120
+ /**
121
+ * Max minting commission on Solana.
122
+ * Equivalent to EVM `getMintingFee`.
123
+ */
124
+ export async function getMintingFeeSolana(
125
+ params: GetTokenFeeConfigParams,
126
+ ): Promise<BigNumber> {
127
+ const cfg = await getTokenFeeConfig(params);
128
+ return cfg.maxMintCommission;
129
+ }
130
+
131
+ /**
132
+ * Minimum redeem amount (excluding fee) on Solana.
133
+ * Equivalent to EVM `getMinRedeemAmount`.
134
+ */
135
+ export async function getMinRedeemAmountSolana(
136
+ params: GetTokenFeeConfigParams,
137
+ ): Promise<BigNumber> {
138
+ const cfg = await getTokenFeeConfig(params);
139
+ return cfg.redeemForBtcMinAmount;
140
+ }
141
+
142
+ /**
143
+ * Minimum transfer amount the user must provide for a successful redemption.
144
+ * Equals `redeemFee + toNativeCommission + redeemForBtcMinAmount`.
145
+ * Equivalent to EVM `getMinRedeemAmountWithFee`.
146
+ */
147
+ export async function getMinRedeemAmountWithFeeSolana(
148
+ params: GetTokenFeeConfigParams,
149
+ ): Promise<BigNumber> {
150
+ const cfg = await getTokenFeeConfig(params);
151
+ return cfg.toNativeCommission
152
+ .plus(cfg.redeemFee)
153
+ .plus(cfg.redeemForBtcMinAmount);
154
+ }
@@ -0,0 +1,11 @@
1
+ export type {
2
+ GetTokenFeeConfigParams,
3
+ TokenFeeConfigResult,
4
+ } from './getTokenFeeConfig';
5
+ export {
6
+ getMinRedeemAmountSolana,
7
+ getMinRedeemAmountWithFeeSolana,
8
+ getMintingFeeSolana,
9
+ getRedeemFeeSolana,
10
+ getTokenFeeConfig,
11
+ } from './getTokenFeeConfig';
@@ -1,14 +1,13 @@
1
1
  export * from './detectWallet';
2
2
  export * from './getBalance';
3
+ export * from './getTokenFeeConfig';
3
4
  export * from './getUnifiedChainId';
4
5
  export * from './signLbtcDestinationAddrSolana';
5
6
  export * from './signMessage';
6
7
  export * from './signTermsOfService';
7
8
 
8
- // LBTC operations
9
- export * from './claimLBTC';
10
- export * from './unstakeLBTC';
11
-
12
9
  // Asset Router operations (Ledger v2)
13
10
  export * from './claimToken';
11
+ export * from './deposit';
12
+ export * from './redeem';
14
13
  export * from './redeemToken';
@@ -0,0 +1 @@
1
+ export { redeem, type RedeemParams } from './redeem';