@lombard.finance/sdk-solana 2.0.1 → 2.0.2
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/dist/index.cjs +1 -1
- package/dist/index.js +23 -22
- package/dist/index2.cjs +47 -47
- package/dist/index2.js +6103 -6029
- package/dist/sendBridgeTransaction.cjs +1 -1
- package/dist/sendBridgeTransaction.js +19 -19
- package/package.json +1 -1
- package/src/bridge/sendBridgeTransaction.ts +2 -2
- package/src/const/__tests__/rpcUrls.test.ts +73 -0
- package/src/const/errors.ts +0 -1
- package/src/const/getConfig.ts +30 -15
- package/src/const/rpcUrls.ts +53 -7
- package/src/idl/asset_router.json +182 -1532
- package/src/idl/bridge.json +139 -1185
- package/src/idl/consortium.json +62 -498
- package/src/idl/getAssetRouterIdl.ts +1 -3
- package/src/idl/getConsortiumIdl.ts +1 -3
- package/src/idl/getLbtcIdl.ts +4 -1
- package/src/idl/getMailboxIdl.ts +1 -3
- package/src/idl/lombard_token_pool.json +92 -932
- package/src/idl/mailbox.json +114 -1018
- package/src/idl/ratio_oracle.json +35 -332
- package/src/services/SolanaServiceImpl.test.ts +5 -4
- package/src/stories/components/ConnectButton/ConnectButton.tsx +2 -2
- package/src/stories/components/NetworkSelector/NetworkSelector.tsx +1 -1
- package/src/stories/components/OutputSelector/OutputSelector.tsx +4 -4
- package/src/stories/components/SelectField/SelectField.tsx +2 -3
- package/src/stories/hooks/useFetchOutputs.ts +1 -1
- package/src/stories/utils/fromCamelCase.ts +1 -1
- package/src/types/sdkTypes.ts +7 -0
- package/src/utils/createDebugLogger.ts +1 -1
- package/src/web3Sdk/claimToken/claimBtcb.ts +19 -5
- package/src/web3Sdk/claimToken/claimLbtcGmp.ts +55 -25
- package/src/web3Sdk/claimToken/claimToken.stories.tsx +4 -3
- package/src/web3Sdk/claimToken/claimToken.ts +16 -6
- package/src/web3Sdk/claimToken/shared.ts +18 -12
- package/src/web3Sdk/claimToken/utils/__tests__/signatureUtils.test.ts +10 -4
- package/src/web3Sdk/claimToken/utils/signatureUtils.ts +3 -1
- package/src/web3Sdk/deposit/deposit.stories.tsx +1 -4
- package/src/web3Sdk/deposit/deposit.test.ts +67 -37
- package/src/web3Sdk/deposit/deposit.ts +14 -14
- package/src/web3Sdk/detectWallet/detectWallet.test.ts +2 -2
- package/src/web3Sdk/getBalance/getBalance.test.ts +1 -1
- package/src/web3Sdk/getBalance/getBalance.ts +2 -1
- package/src/web3Sdk/getTokenFeeConfig/getTokenFeeConfig.stories.tsx +8 -18
- package/src/web3Sdk/getTokenFeeConfig/getTokenFeeConfig.ts +2 -4
- package/src/web3Sdk/redeem/redeem.stories.tsx +17 -8
- package/src/web3Sdk/redeem/redeem.test.ts +45 -13
- package/src/web3Sdk/redeem/redeem.ts +46 -20
- package/src/web3Sdk/redeemToken/redeemBtcb.ts +28 -12
- package/src/web3Sdk/redeemToken/redeemForBtc.stories.tsx +6 -6
- package/src/web3Sdk/redeemToken/redeemForBtc.test.ts +43 -13
- package/src/web3Sdk/redeemToken/redeemForBtc.ts +27 -14
- package/src/web3Sdk/redeemToken/redeemLbtc.ts +28 -12
- package/src/web3Sdk/redeemToken/shared.test.ts +6 -2
- package/src/web3Sdk/redeemToken/shared.ts +1 -3
|
@@ -2,11 +2,7 @@ import { Env } from '@lombard.finance/sdk-common';
|
|
|
2
2
|
import type { Meta, StoryObj } from '@storybook/react';
|
|
3
3
|
|
|
4
4
|
import { envToNetwork, getConfig } from '../../const/getConfig';
|
|
5
|
-
import {
|
|
6
|
-
Button,
|
|
7
|
-
CodeBlock,
|
|
8
|
-
ErrorDisplay,
|
|
9
|
-
} from '../../stories/components';
|
|
5
|
+
import { Button, CodeBlock, ErrorDisplay } from '../../stories/components';
|
|
10
6
|
import { functionType } from '../../stories/decorators/function-type';
|
|
11
7
|
import useQuery from '../../stories/hooks/useQuery';
|
|
12
8
|
import {
|
|
@@ -24,14 +20,11 @@ interface StoryArgs {
|
|
|
24
20
|
token: TokenChoice;
|
|
25
21
|
}
|
|
26
22
|
|
|
27
|
-
function resolveTokenMint(
|
|
28
|
-
token: TokenChoice,
|
|
29
|
-
env: Env,
|
|
30
|
-
): string | undefined {
|
|
23
|
+
function resolveTokenMint(token: TokenChoice, env: Env): string | undefined {
|
|
31
24
|
const config = getConfig(env);
|
|
32
25
|
return token === 'LBTC'
|
|
33
26
|
? config.lbtcTokenMint
|
|
34
|
-
: config.btcbTokenMint ?? undefined;
|
|
27
|
+
: (config.btcbTokenMint ?? undefined);
|
|
35
28
|
}
|
|
36
29
|
|
|
37
30
|
export function StoryView({ environment, token }: StoryArgs) {
|
|
@@ -94,22 +87,19 @@ export function StoryView({ environment, token }: StoryArgs) {
|
|
|
94
87
|
redeemForBtcMinAmount:
|
|
95
88
|
configQuery.data.redeemForBtcMinAmount.toFormat(),
|
|
96
89
|
maxMintCommission: configQuery.data.maxMintCommission.toFormat(),
|
|
97
|
-
toNativeCommission:
|
|
98
|
-
configQuery.data.toNativeCommission.toFormat(),
|
|
90
|
+
toNativeCommission: configQuery.data.toNativeCommission.toFormat(),
|
|
99
91
|
'getRedeemFeeSolana (toNative + redeem)':
|
|
100
92
|
redeemFeeQuery.data?.toFormat(),
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
minRedeemWithFeeQuery.data?.toFormat(),
|
|
93
|
+
getMintingFeeSolana: mintingFeeQuery.data?.toFormat(),
|
|
94
|
+
getMinRedeemAmountSolana: minRedeemQuery.data?.toFormat(),
|
|
95
|
+
getMinRedeemAmountWithFeeSolana: minRedeemWithFeeQuery.data?.toFormat(),
|
|
105
96
|
}
|
|
106
97
|
: undefined;
|
|
107
98
|
|
|
108
99
|
return (
|
|
109
100
|
<div>
|
|
110
101
|
<p>
|
|
111
|
-
<strong>Network:</strong> {network} | <strong>Token:</strong>{
|
|
112
|
-
{token}
|
|
102
|
+
<strong>Network:</strong> {network} | <strong>Token:</strong> {token}
|
|
113
103
|
{tokenMint ? ` (${tokenMint})` : ' — not configured'}
|
|
114
104
|
</p>
|
|
115
105
|
|
|
@@ -66,7 +66,7 @@ export async function getTokenFeeConfig(
|
|
|
66
66
|
const mint = new PublicKey(mintAddress);
|
|
67
67
|
const assetRouterProgramId = new PublicKey(config.assetRouter);
|
|
68
68
|
|
|
69
|
-
const connection = getConnection(network, rpcUrl);
|
|
69
|
+
const connection = getConnection(network, rpcUrl, env);
|
|
70
70
|
|
|
71
71
|
const [tokenConfigPDA] = PublicKey.findProgramAddressSync(
|
|
72
72
|
[Buffer.from('token_config'), mint.toBuffer()],
|
|
@@ -77,9 +77,7 @@ export async function getTokenFeeConfig(
|
|
|
77
77
|
const accountInfo = await connection.getAccountInfo(tokenConfigPDA);
|
|
78
78
|
|
|
79
79
|
if (!accountInfo) {
|
|
80
|
-
throw new Error(
|
|
81
|
-
`TokenConfig account not found for mint ${mintAddress}`,
|
|
82
|
-
);
|
|
80
|
+
throw new Error(`TokenConfig account not found for mint ${mintAddress}`);
|
|
83
81
|
}
|
|
84
82
|
|
|
85
83
|
if (accountInfo.data.length < TOKEN_CONFIG_MIN_SIZE) {
|
|
@@ -53,7 +53,8 @@ export const StoryView = ({
|
|
|
53
53
|
|
|
54
54
|
const request = async () => {
|
|
55
55
|
if (!provider || !address) throw new Error('Wallet not connected.');
|
|
56
|
-
if (!recipient)
|
|
56
|
+
if (!recipient)
|
|
57
|
+
throw new Error('Recipient address is required (set in args).');
|
|
57
58
|
const parsedAmount = parseFloat(amount);
|
|
58
59
|
if (!amount || isNaN(parsedAmount) || parsedAmount <= 0)
|
|
59
60
|
throw new Error('Amount must be a positive number in BTC (set in args).');
|
|
@@ -89,7 +90,16 @@ export const StoryView = ({
|
|
|
89
90
|
refetch: handleRedeem,
|
|
90
91
|
} = useQuery(
|
|
91
92
|
request,
|
|
92
|
-
[
|
|
93
|
+
[
|
|
94
|
+
provider,
|
|
95
|
+
address,
|
|
96
|
+
amount,
|
|
97
|
+
recipient,
|
|
98
|
+
tokenMint,
|
|
99
|
+
toLchainId,
|
|
100
|
+
toTokenAddress,
|
|
101
|
+
environment,
|
|
102
|
+
],
|
|
93
103
|
false,
|
|
94
104
|
);
|
|
95
105
|
|
|
@@ -149,10 +159,7 @@ export const StoryView = ({
|
|
|
149
159
|
/>
|
|
150
160
|
)}
|
|
151
161
|
{(error || connectError) && (
|
|
152
|
-
<ErrorDisplay
|
|
153
|
-
error={error || connectError}
|
|
154
|
-
title="Redeem Error"
|
|
155
|
-
/>
|
|
162
|
+
<ErrorDisplay error={error || connectError} title="Redeem Error" />
|
|
156
163
|
)}
|
|
157
164
|
|
|
158
165
|
{transactionLogs && transactionLogs.length > 0 && (
|
|
@@ -213,11 +220,13 @@ const meta: Meta<typeof StoryView> = {
|
|
|
213
220
|
},
|
|
214
221
|
toLchainId: {
|
|
215
222
|
control: { type: 'text' },
|
|
216
|
-
description:
|
|
223
|
+
description:
|
|
224
|
+
'Destination Lombard routing chain ID (hex). Defaults to Solana routing chain ID',
|
|
217
225
|
},
|
|
218
226
|
toTokenAddress: {
|
|
219
227
|
control: { type: 'text' },
|
|
220
|
-
description:
|
|
228
|
+
description:
|
|
229
|
+
'Destination token address/mint override (defaults to BTC.b from config)',
|
|
221
230
|
},
|
|
222
231
|
},
|
|
223
232
|
};
|
|
@@ -11,8 +11,10 @@ const MOCK_LBTC_MINT = 'LBTCojyVJ63rsEED2DLEGWMzSxWJyQynXE91LMLgV1J';
|
|
|
11
11
|
const MOCK_BTCB_MINT = 'BTCB3ripBAut19jM8kDPVbJHb2ZdR2GcZvGZkCmFPtV8';
|
|
12
12
|
const MOCK_ASSET_ROUTER = 'LomVyJDZ91jeVbNnTupJXKJTQFakJVMc87CmwDHYt95';
|
|
13
13
|
const MOCK_MAILBOX = 'LomJw912MoUd7iiAesTQAgz1paLcTqi6ndG3w3pnKH9';
|
|
14
|
-
const MOCK_SOLANA_CHAIN_ID =
|
|
15
|
-
|
|
14
|
+
const MOCK_SOLANA_CHAIN_ID =
|
|
15
|
+
'0259db5080fc2c6d3bcf7ca90712d3c2e5e6c28f27f0dfbb9953bdb0894c03ab';
|
|
16
|
+
const MOCK_LEDGER_CHAIN_ID =
|
|
17
|
+
'031f51c4e4cc1dae1c752d2f8fe2ae045da668a13f2e47a465964d630f5ed22e';
|
|
16
18
|
const MOCK_PAYER = '8yarEiDaJVikHZbk3PQSoWiDn2T3oM1FHZN1Jv4VZFdr';
|
|
17
19
|
const MOCK_RECIPIENT = 'DVMiNi7uxHEPABTBt1nLMoxnPniPKbLAFj4MPJq1RDjg';
|
|
18
20
|
const MOCK_RECIPIENT_ATA = 'GRq2yasTvWWPPqSwxCZvqfCTfDhP3MswDH4nW2v6F5To';
|
|
@@ -25,7 +27,8 @@ const fullConfig: IConfig = {
|
|
|
25
27
|
assetRouter: MOCK_ASSET_ROUTER,
|
|
26
28
|
mailbox: MOCK_MAILBOX,
|
|
27
29
|
solanaRoutingChainId: MOCK_SOLANA_CHAIN_ID,
|
|
28
|
-
bitcoinRoutingChainId:
|
|
30
|
+
bitcoinRoutingChainId:
|
|
31
|
+
'ff000008819873e925422c1ff0f99f7cc9bbb232af63a077a480a3633bee1ef6',
|
|
29
32
|
ledgerChainId: MOCK_LEDGER_CHAIN_ID,
|
|
30
33
|
lbtcProgramId: 'HEY7PCJe3GB27UWdopuYb1xDbB5SNtTcYPxRjntvfBSA',
|
|
31
34
|
treasuryAddress: 'ByHNGi4zPJw5StyWZoLQJ9n2wT12oupJF2pTSNKMnnAZ',
|
|
@@ -93,7 +96,11 @@ vi.mock('../../const/rpcUrls', () => ({
|
|
|
93
96
|
}));
|
|
94
97
|
|
|
95
98
|
vi.mock('../../utils/tokenAccount', () => ({
|
|
96
|
-
getTokenProgramForMint: vi
|
|
99
|
+
getTokenProgramForMint: vi
|
|
100
|
+
.fn()
|
|
101
|
+
.mockResolvedValue(
|
|
102
|
+
new PublicKey('TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA'),
|
|
103
|
+
),
|
|
97
104
|
}));
|
|
98
105
|
|
|
99
106
|
vi.mock('../../idl/getAssetRouterIdl', () => ({
|
|
@@ -136,7 +143,9 @@ vi.mock('@solana/spl-token', () => ({
|
|
|
136
143
|
return new PublicKey(MOCK_PAYER);
|
|
137
144
|
},
|
|
138
145
|
),
|
|
139
|
-
ASSOCIATED_TOKEN_PROGRAM_ID: new PublicKey(
|
|
146
|
+
ASSOCIATED_TOKEN_PROGRAM_ID: new PublicKey(
|
|
147
|
+
'ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL',
|
|
148
|
+
),
|
|
140
149
|
}));
|
|
141
150
|
|
|
142
151
|
const mockTx = { instructions: [{ keys: [] }] };
|
|
@@ -157,7 +166,9 @@ vi.mock('../../utils', async () => {
|
|
|
157
166
|
const actual = await vi.importActual('../../utils');
|
|
158
167
|
return {
|
|
159
168
|
...actual,
|
|
160
|
-
sendAndConfirmTransaction: vi
|
|
169
|
+
sendAndConfirmTransaction: vi
|
|
170
|
+
.fn()
|
|
171
|
+
.mockResolvedValue({ signature: 'mock-redeem-sig' }),
|
|
161
172
|
};
|
|
162
173
|
});
|
|
163
174
|
|
|
@@ -194,7 +205,10 @@ describe('redeem', () => {
|
|
|
194
205
|
|
|
195
206
|
it('should throw when Asset Router is not configured', async () => {
|
|
196
207
|
const { getConfig } = await import('../../const/getConfig');
|
|
197
|
-
vi.mocked(getConfig).mockReturnValueOnce({
|
|
208
|
+
vi.mocked(getConfig).mockReturnValueOnce({
|
|
209
|
+
...fullConfig,
|
|
210
|
+
assetRouter: null,
|
|
211
|
+
});
|
|
198
212
|
|
|
199
213
|
await expect(
|
|
200
214
|
redeemFn({ publicKey: MOCK_PAYER } as any, baseParams),
|
|
@@ -212,7 +226,10 @@ describe('redeem', () => {
|
|
|
212
226
|
|
|
213
227
|
it('should throw when Solana routing chain ID is not configured', async () => {
|
|
214
228
|
const { getConfig } = await import('../../const/getConfig');
|
|
215
|
-
vi.mocked(getConfig).mockReturnValueOnce({
|
|
229
|
+
vi.mocked(getConfig).mockReturnValueOnce({
|
|
230
|
+
...fullConfig,
|
|
231
|
+
solanaRoutingChainId: null,
|
|
232
|
+
});
|
|
216
233
|
|
|
217
234
|
await expect(
|
|
218
235
|
redeemFn({ publicKey: MOCK_PAYER } as any, baseParams),
|
|
@@ -221,7 +238,10 @@ describe('redeem', () => {
|
|
|
221
238
|
|
|
222
239
|
it('should throw when source token mint is not resolved', async () => {
|
|
223
240
|
const { getConfig } = await import('../../const/getConfig');
|
|
224
|
-
vi.mocked(getConfig).mockReturnValueOnce({
|
|
241
|
+
vi.mocked(getConfig).mockReturnValueOnce({
|
|
242
|
+
...fullConfig,
|
|
243
|
+
lbtcTokenMint: '',
|
|
244
|
+
});
|
|
225
245
|
|
|
226
246
|
await expect(
|
|
227
247
|
redeemFn({ publicKey: MOCK_PAYER } as any, baseParams),
|
|
@@ -230,7 +250,10 @@ describe('redeem', () => {
|
|
|
230
250
|
|
|
231
251
|
it('should throw when destination token is not configured', async () => {
|
|
232
252
|
const { getConfig } = await import('../../const/getConfig');
|
|
233
|
-
vi.mocked(getConfig).mockReturnValueOnce({
|
|
253
|
+
vi.mocked(getConfig).mockReturnValueOnce({
|
|
254
|
+
...fullConfig,
|
|
255
|
+
btcbTokenMint: '',
|
|
256
|
+
});
|
|
234
257
|
|
|
235
258
|
await expect(
|
|
236
259
|
redeemFn({ publicKey: MOCK_PAYER } as any, baseParams),
|
|
@@ -239,7 +262,10 @@ describe('redeem', () => {
|
|
|
239
262
|
|
|
240
263
|
it('should throw when amount is zero', async () => {
|
|
241
264
|
await expect(
|
|
242
|
-
redeemFn({ publicKey: MOCK_PAYER } as any, {
|
|
265
|
+
redeemFn({ publicKey: MOCK_PAYER } as any, {
|
|
266
|
+
...baseParams,
|
|
267
|
+
amount: '0',
|
|
268
|
+
}),
|
|
243
269
|
).rejects.toThrow('greater than zero');
|
|
244
270
|
});
|
|
245
271
|
|
|
@@ -268,7 +294,10 @@ describe('redeem', () => {
|
|
|
268
294
|
|
|
269
295
|
it('should throw when Ledger chain ID is not configured', async () => {
|
|
270
296
|
const { getConfig } = await import('../../const/getConfig');
|
|
271
|
-
vi.mocked(getConfig).mockReturnValueOnce({
|
|
297
|
+
vi.mocked(getConfig).mockReturnValueOnce({
|
|
298
|
+
...fullConfig,
|
|
299
|
+
ledgerChainId: null,
|
|
300
|
+
});
|
|
272
301
|
|
|
273
302
|
await expect(
|
|
274
303
|
redeemFn({ publicKey: MOCK_PAYER } as any, baseParams),
|
|
@@ -301,7 +330,10 @@ describe('redeem', () => {
|
|
|
301
330
|
|
|
302
331
|
it('should wrap errors with SolanaSdkError', async () => {
|
|
303
332
|
const { getConfig } = await import('../../const/getConfig');
|
|
304
|
-
vi.mocked(getConfig).mockReturnValueOnce({
|
|
333
|
+
vi.mocked(getConfig).mockReturnValueOnce({
|
|
334
|
+
...fullConfig,
|
|
335
|
+
assetRouter: null,
|
|
336
|
+
});
|
|
305
337
|
|
|
306
338
|
try {
|
|
307
339
|
await redeemFn({ publicKey: MOCK_PAYER } as any, baseParams);
|
|
@@ -67,9 +67,13 @@ export async function redeem(
|
|
|
67
67
|
params: RedeemParams,
|
|
68
68
|
): Promise<string> {
|
|
69
69
|
const {
|
|
70
|
-
amount,
|
|
71
|
-
|
|
72
|
-
|
|
70
|
+
amount,
|
|
71
|
+
recipient,
|
|
72
|
+
network,
|
|
73
|
+
env: envOverride,
|
|
74
|
+
rpcUrl,
|
|
75
|
+
debug = false,
|
|
76
|
+
skipPreflight = true,
|
|
73
77
|
} = params;
|
|
74
78
|
const { debugLog, printLogs } = createDebugLogger({ debug });
|
|
75
79
|
|
|
@@ -88,31 +92,40 @@ export async function redeem(
|
|
|
88
92
|
throw new Error(`Mailbox not configured for network: ${network}`);
|
|
89
93
|
}
|
|
90
94
|
if (!config.solanaRoutingChainId) {
|
|
91
|
-
throw new Error(
|
|
95
|
+
throw new Error(
|
|
96
|
+
`Solana routing chain ID not configured for network: ${network}`,
|
|
97
|
+
);
|
|
92
98
|
}
|
|
93
99
|
|
|
94
100
|
// Resolve source token mint (defaults to LBTC)
|
|
95
101
|
const mintAddress = params.tokenMint || config.lbtcTokenMint;
|
|
96
102
|
if (!mintAddress) {
|
|
97
|
-
throw new Error(
|
|
103
|
+
throw new Error(
|
|
104
|
+
`Source token mint not configured for network: ${network}`,
|
|
105
|
+
);
|
|
98
106
|
}
|
|
99
107
|
|
|
100
108
|
// Resolve destination chain and token (defaults to Solana + BTC.b)
|
|
101
109
|
const toLchainIdHex = params.toLchainId || config.solanaRoutingChainId;
|
|
102
110
|
const toTokenAddressStr = params.toTokenAddress || config.btcbTokenMint;
|
|
103
111
|
if (!toTokenAddressStr) {
|
|
104
|
-
throw new Error(
|
|
112
|
+
throw new Error(
|
|
113
|
+
`Destination token not configured for network: ${network}`,
|
|
114
|
+
);
|
|
105
115
|
}
|
|
106
116
|
|
|
107
117
|
validateAmount(amount);
|
|
108
118
|
|
|
109
|
-
const connection = getConnection(network, rpcUrl);
|
|
119
|
+
const connection = getConnection(network, rpcUrl, env);
|
|
110
120
|
const payer = new PublicKey(provider.publicKey);
|
|
111
121
|
const mint = new PublicKey(mintAddress);
|
|
112
122
|
const recipientPubkey = new PublicKey(recipient);
|
|
113
123
|
const assetRouterProgramId = new PublicKey(config.assetRouter);
|
|
114
124
|
const mailboxProgramId = new PublicKey(config.mailbox);
|
|
115
|
-
const solanaRoutingChainId = Buffer.from(
|
|
125
|
+
const solanaRoutingChainId = Buffer.from(
|
|
126
|
+
config.solanaRoutingChainId,
|
|
127
|
+
'hex',
|
|
128
|
+
);
|
|
116
129
|
const toLchainId = Buffer.from(toLchainIdHex, 'hex');
|
|
117
130
|
const toTokenAddress = new PublicKey(toTokenAddressStr).toBytes();
|
|
118
131
|
|
|
@@ -204,7 +217,10 @@ export async function redeem(
|
|
|
204
217
|
throw new Error('Asset Router is paused');
|
|
205
218
|
}
|
|
206
219
|
debugLog('Asset Router treasury:', arTreasury.toBase58());
|
|
207
|
-
debugLog(
|
|
220
|
+
debugLog(
|
|
221
|
+
'Native mint (recipient ATA mint):',
|
|
222
|
+
nativeMintFromConfig.toBase58(),
|
|
223
|
+
);
|
|
208
224
|
|
|
209
225
|
if (mailboxConfigInfo.data.length < 104) {
|
|
210
226
|
throw new Error(
|
|
@@ -246,10 +262,14 @@ export async function redeem(
|
|
|
246
262
|
|
|
247
263
|
debugLog('Payer token account:', payerTokenAccount.toBase58());
|
|
248
264
|
debugLog('Treasury token account:', treasuryTokenAccount.toBase58());
|
|
249
|
-
debugLog(
|
|
265
|
+
debugLog(
|
|
266
|
+
'Recipient token account (payload):',
|
|
267
|
+
recipientTokenAccount.toBase58(),
|
|
268
|
+
);
|
|
250
269
|
|
|
251
270
|
// ── Balance check ──
|
|
252
|
-
const tokenBalance =
|
|
271
|
+
const tokenBalance =
|
|
272
|
+
await connection.getTokenAccountBalance(payerTokenAccount);
|
|
253
273
|
const userBalance = BigInt(tokenBalance.value.amount);
|
|
254
274
|
const parsedAmount = BigInt(amount);
|
|
255
275
|
if (userBalance < parsedAmount) {
|
|
@@ -258,10 +278,9 @@ export async function redeem(
|
|
|
258
278
|
);
|
|
259
279
|
}
|
|
260
280
|
|
|
261
|
-
const assetRouterProgram = new Program(
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
);
|
|
281
|
+
const assetRouterProgram = new Program(getAssetRouterIdl(env), {
|
|
282
|
+
connection,
|
|
283
|
+
});
|
|
265
284
|
|
|
266
285
|
// ── Instruction args ──
|
|
267
286
|
const toLchainIdArray = Array.from(toLchainId);
|
|
@@ -271,7 +290,8 @@ export async function redeem(
|
|
|
271
290
|
// ── Build & send with nonce retry ──
|
|
272
291
|
const MAX_NONCE_RETRIES = 3;
|
|
273
292
|
for (let attempt = 0; attempt < MAX_NONCE_RETRIES; attempt++) {
|
|
274
|
-
const freshMailboxConfig =
|
|
293
|
+
const freshMailboxConfig =
|
|
294
|
+
await connection.getAccountInfo(mailboxConfigPDA);
|
|
275
295
|
if (!freshMailboxConfig) {
|
|
276
296
|
throw new Error('Mailbox config account not found');
|
|
277
297
|
}
|
|
@@ -289,10 +309,17 @@ export async function redeem(
|
|
|
289
309
|
mailboxProgramId,
|
|
290
310
|
);
|
|
291
311
|
|
|
292
|
-
debugLog(
|
|
312
|
+
debugLog(
|
|
313
|
+
`Attempt ${attempt + 1}: global nonce=${globalNonce}, outbound_message=${outboundMessagePDA.toBase58()}`,
|
|
314
|
+
);
|
|
293
315
|
|
|
294
316
|
const tx = await assetRouterProgram.methods
|
|
295
|
-
.redeem(
|
|
317
|
+
.redeem(
|
|
318
|
+
toLchainIdArray,
|
|
319
|
+
toTokenAddressArray,
|
|
320
|
+
recipientArray,
|
|
321
|
+
new BN(amount),
|
|
322
|
+
)
|
|
296
323
|
.accounts({
|
|
297
324
|
payer,
|
|
298
325
|
config: assetRouterConfigPDA,
|
|
@@ -328,8 +355,7 @@ export async function redeem(
|
|
|
328
355
|
return signature;
|
|
329
356
|
} catch (err: unknown) {
|
|
330
357
|
const isNonceError =
|
|
331
|
-
err instanceof Error &&
|
|
332
|
-
err.message.includes('0x7d6'); // ConstraintSeeds
|
|
358
|
+
err instanceof Error && err.message.includes('0x7d6'); // ConstraintSeeds
|
|
333
359
|
if (isNonceError && attempt < MAX_NONCE_RETRIES - 1) {
|
|
334
360
|
debugLog(`Nonce stale (ConstraintSeeds), retrying...`);
|
|
335
361
|
continue;
|
|
@@ -16,12 +16,23 @@ import { BTC_NATIVE_TOKEN_ADDRESS, RedeemContext } from './shared';
|
|
|
16
16
|
*/
|
|
17
17
|
export async function redeemBtcbForBtc(ctx: RedeemContext): Promise<string> {
|
|
18
18
|
const {
|
|
19
|
-
provider,
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
19
|
+
provider,
|
|
20
|
+
params,
|
|
21
|
+
config,
|
|
22
|
+
connection,
|
|
23
|
+
payer,
|
|
24
|
+
mint,
|
|
25
|
+
tokenProgramId,
|
|
26
|
+
scriptPubKey,
|
|
27
|
+
assetRouterProgramId,
|
|
28
|
+
mailboxProgramId,
|
|
29
|
+
solanaRoutingChainId,
|
|
30
|
+
bitcoinRoutingChainId,
|
|
31
|
+
assetRouterProgram,
|
|
32
|
+
assetRouterConfigPDA,
|
|
33
|
+
mailboxConfigPDA,
|
|
34
|
+
arTreasury,
|
|
35
|
+
mailboxTreasury,
|
|
25
36
|
debugLog,
|
|
26
37
|
} = ctx;
|
|
27
38
|
|
|
@@ -53,7 +64,9 @@ export async function redeemBtcbForBtc(ctx: RedeemContext): Promise<string> {
|
|
|
53
64
|
|
|
54
65
|
// ── Mailbox PDAs ──
|
|
55
66
|
if (!config.ledgerChainId) {
|
|
56
|
-
throw new Error(
|
|
67
|
+
throw new Error(
|
|
68
|
+
`Ledger chain ID not configured for network: ${params.network}`,
|
|
69
|
+
);
|
|
57
70
|
}
|
|
58
71
|
const ledgerChainId = Buffer.from(config.ledgerChainId, 'hex');
|
|
59
72
|
const [outboundMessagePathPDA] = PublicKey.findProgramAddressSync(
|
|
@@ -88,7 +101,8 @@ export async function redeemBtcbForBtc(ctx: RedeemContext): Promise<string> {
|
|
|
88
101
|
debugLog('Treasury token account:', treasuryTokenAccount.toBase58());
|
|
89
102
|
|
|
90
103
|
// ── Balance check ──
|
|
91
|
-
const tokenBalance =
|
|
104
|
+
const tokenBalance =
|
|
105
|
+
await connection.getTokenAccountBalance(payerTokenAccount);
|
|
92
106
|
const userBalance = BigInt(tokenBalance.value.amount);
|
|
93
107
|
const parsedAmount = BigInt(amount);
|
|
94
108
|
if (userBalance < parsedAmount) {
|
|
@@ -102,7 +116,8 @@ export async function redeemBtcbForBtc(ctx: RedeemContext): Promise<string> {
|
|
|
102
116
|
// reads. Retry up to 3 times if the nonce becomes stale.
|
|
103
117
|
const MAX_NONCE_RETRIES = 3;
|
|
104
118
|
for (let attempt = 0; attempt < MAX_NONCE_RETRIES; attempt++) {
|
|
105
|
-
const freshMailboxConfig =
|
|
119
|
+
const freshMailboxConfig =
|
|
120
|
+
await connection.getAccountInfo(mailboxConfigPDA);
|
|
106
121
|
if (!freshMailboxConfig) {
|
|
107
122
|
throw new Error('Mailbox config account not found');
|
|
108
123
|
}
|
|
@@ -121,7 +136,9 @@ export async function redeemBtcbForBtc(ctx: RedeemContext): Promise<string> {
|
|
|
121
136
|
mailboxProgramId,
|
|
122
137
|
);
|
|
123
138
|
|
|
124
|
-
debugLog(
|
|
139
|
+
debugLog(
|
|
140
|
+
`Attempt ${attempt + 1}: global nonce=${globalNonce}, outbound_message=${outboundMessagePDA.toBase58()}`,
|
|
141
|
+
);
|
|
125
142
|
|
|
126
143
|
const tx = await assetRouterProgram.methods
|
|
127
144
|
.redeemForBtc(scriptPubKey, new BN(amount))
|
|
@@ -160,8 +177,7 @@ export async function redeemBtcbForBtc(ctx: RedeemContext): Promise<string> {
|
|
|
160
177
|
return signature;
|
|
161
178
|
} catch (err: unknown) {
|
|
162
179
|
const isNonceError =
|
|
163
|
-
err instanceof Error &&
|
|
164
|
-
err.message.includes('0x7d6'); // ConstraintSeeds
|
|
180
|
+
err instanceof Error && err.message.includes('0x7d6'); // ConstraintSeeds
|
|
165
181
|
if (isNonceError && attempt < MAX_NONCE_RETRIES - 1) {
|
|
166
182
|
debugLog(`Nonce stale (ConstraintSeeds), retrying...`);
|
|
167
183
|
continue;
|
|
@@ -25,8 +25,11 @@ interface RedeemForBtcStoryArgs {
|
|
|
25
25
|
token: RedeemToken;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
const getTokenMint = (
|
|
29
|
-
token
|
|
28
|
+
const getTokenMint = (
|
|
29
|
+
token: RedeemToken,
|
|
30
|
+
config: ReturnType<typeof getConfig>,
|
|
31
|
+
): string | undefined =>
|
|
32
|
+
token === 'LBTC' ? config.lbtcTokenMint : (config.btcbTokenMint ?? undefined);
|
|
30
33
|
|
|
31
34
|
export const StoryView = ({
|
|
32
35
|
environment,
|
|
@@ -149,10 +152,7 @@ export const StoryView = ({
|
|
|
149
152
|
/>
|
|
150
153
|
)}
|
|
151
154
|
{(error || connectError) && (
|
|
152
|
-
<ErrorDisplay
|
|
153
|
-
error={error || connectError}
|
|
154
|
-
title="Redeem Error"
|
|
155
|
-
/>
|
|
155
|
+
<ErrorDisplay error={error || connectError} title="Redeem Error" />
|
|
156
156
|
)}
|
|
157
157
|
|
|
158
158
|
{transactionLogs && transactionLogs.length > 0 && (
|
|
@@ -10,16 +10,23 @@ import { ErrorCode, SolanaSdkError } from '../../utils';
|
|
|
10
10
|
const mockRedeemBtcbForBtc = vi.fn().mockResolvedValue('btcb-sig');
|
|
11
11
|
const mockRedeemLbtcForBtc = vi.fn().mockResolvedValue('lbtc-sig');
|
|
12
12
|
|
|
13
|
-
vi.mock('./redeemBtcb', () => ({
|
|
14
|
-
|
|
13
|
+
vi.mock('./redeemBtcb', () => ({
|
|
14
|
+
redeemBtcbForBtc: (...a: unknown[]) => mockRedeemBtcbForBtc(...a),
|
|
15
|
+
}));
|
|
16
|
+
vi.mock('./redeemLbtc', () => ({
|
|
17
|
+
redeemLbtcForBtc: (...a: unknown[]) => mockRedeemLbtcForBtc(...a),
|
|
18
|
+
}));
|
|
15
19
|
|
|
16
20
|
const MOCK_LBTC_MINT = 'LBTCojyVJ63rsEED2DLEGWMzSxWJyQynXE91LMLgV1J';
|
|
17
21
|
const MOCK_BTCB_MINT = 'BTCB3ripBAut19jM8kDPVbJHb2ZdR2GcZvGZkCmFPtV8';
|
|
18
22
|
const MOCK_ASSET_ROUTER = 'LomVyJDZ91jeVbNnTupJXKJTQFakJVMc87CmwDHYt95';
|
|
19
23
|
const MOCK_MAILBOX = 'LomJw912MoUd7iiAesTQAgz1paLcTqi6ndG3w3pnKH9';
|
|
20
|
-
const MOCK_SOLANA_CHAIN_ID =
|
|
21
|
-
|
|
22
|
-
const
|
|
24
|
+
const MOCK_SOLANA_CHAIN_ID =
|
|
25
|
+
'0259db5080fc2c6d3bcf7ca90712d3c2e5e6c28f27f0dfbb9953bdb0894c03ab';
|
|
26
|
+
const MOCK_BITCOIN_CHAIN_ID =
|
|
27
|
+
'ff000008819873e925422c1ff0f99f7cc9bbb232af63a077a480a3633bee1ef6';
|
|
28
|
+
const MOCK_LEDGER_CHAIN_ID =
|
|
29
|
+
'031f51c4e4cc1dae1c752d2f8fe2ae045da668a13f2e47a465964d630f5ed22e';
|
|
23
30
|
const MOCK_PAYER = '8yarEiDaJVikHZbk3PQSoWiDn2T3oM1FHZN1Jv4VZFdr';
|
|
24
31
|
|
|
25
32
|
const fullConfig: IConfig = {
|
|
@@ -60,7 +67,11 @@ vi.mock('@lombard.finance/sdk-common', () => ({
|
|
|
60
67
|
}));
|
|
61
68
|
|
|
62
69
|
vi.mock('../../utils/tokenAccount', () => ({
|
|
63
|
-
getTokenProgramForMint: vi
|
|
70
|
+
getTokenProgramForMint: vi
|
|
71
|
+
.fn()
|
|
72
|
+
.mockResolvedValue(
|
|
73
|
+
new PublicKey('TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA'),
|
|
74
|
+
),
|
|
64
75
|
}));
|
|
65
76
|
|
|
66
77
|
vi.mock('../../idl/getAssetRouterIdl', () => ({
|
|
@@ -79,7 +90,9 @@ vi.mock('@coral-xyz/anchor', () => ({
|
|
|
79
90
|
// Build AR config data: 8 (disc) + 32 (admin) + 32 (pending_admin) + 32 (treasury) + 1 (paused=0)
|
|
80
91
|
function buildArConfigData(paused = false) {
|
|
81
92
|
const data = Buffer.alloc(105, 0);
|
|
82
|
-
const treasury = new PublicKey(
|
|
93
|
+
const treasury = new PublicKey(
|
|
94
|
+
'8yarEiDaJVikHZbk3PQSoWiDn2T3oM1FHZN1Jv4VZFdr',
|
|
95
|
+
);
|
|
83
96
|
treasury.toBuffer().copy(data, 72);
|
|
84
97
|
data[104] = paused ? 1 : 0;
|
|
85
98
|
return data;
|
|
@@ -88,7 +101,9 @@ function buildArConfigData(paused = false) {
|
|
|
88
101
|
// Build mailbox config data: 8 (disc) + 32 (admin) + 32 (pending_admin) + 32 (treasury) = 104
|
|
89
102
|
function buildMailboxConfigData() {
|
|
90
103
|
const data = Buffer.alloc(104, 0);
|
|
91
|
-
const treasury = new PublicKey(
|
|
104
|
+
const treasury = new PublicKey(
|
|
105
|
+
'8yarEiDaJVikHZbk3PQSoWiDn2T3oM1FHZN1Jv4VZFdr',
|
|
106
|
+
);
|
|
92
107
|
treasury.toBuffer().copy(data, 72);
|
|
93
108
|
return data;
|
|
94
109
|
}
|
|
@@ -154,7 +169,10 @@ describe('redeemForBtc', () => {
|
|
|
154
169
|
|
|
155
170
|
it('should throw when Asset Router is not configured', async () => {
|
|
156
171
|
const { getConfig } = await import('../../const/getConfig');
|
|
157
|
-
vi.mocked(getConfig).mockReturnValueOnce({
|
|
172
|
+
vi.mocked(getConfig).mockReturnValueOnce({
|
|
173
|
+
...fullConfig,
|
|
174
|
+
assetRouter: null,
|
|
175
|
+
});
|
|
158
176
|
|
|
159
177
|
await expect(
|
|
160
178
|
redeemForBtcFn(mockProvider as any, baseParams),
|
|
@@ -172,7 +190,10 @@ describe('redeemForBtc', () => {
|
|
|
172
190
|
|
|
173
191
|
it('should throw when Solana routing chain ID is not configured', async () => {
|
|
174
192
|
const { getConfig } = await import('../../const/getConfig');
|
|
175
|
-
vi.mocked(getConfig).mockReturnValueOnce({
|
|
193
|
+
vi.mocked(getConfig).mockReturnValueOnce({
|
|
194
|
+
...fullConfig,
|
|
195
|
+
solanaRoutingChainId: null,
|
|
196
|
+
});
|
|
176
197
|
|
|
177
198
|
await expect(
|
|
178
199
|
redeemForBtcFn(mockProvider as any, baseParams),
|
|
@@ -181,7 +202,10 @@ describe('redeemForBtc', () => {
|
|
|
181
202
|
|
|
182
203
|
it('should throw when Bitcoin routing chain ID is not configured', async () => {
|
|
183
204
|
const { getConfig } = await import('../../const/getConfig');
|
|
184
|
-
vi.mocked(getConfig).mockReturnValueOnce({
|
|
205
|
+
vi.mocked(getConfig).mockReturnValueOnce({
|
|
206
|
+
...fullConfig,
|
|
207
|
+
bitcoinRoutingChainId: null,
|
|
208
|
+
});
|
|
185
209
|
|
|
186
210
|
await expect(
|
|
187
211
|
redeemForBtcFn(mockProvider as any, baseParams),
|
|
@@ -190,7 +214,10 @@ describe('redeemForBtc', () => {
|
|
|
190
214
|
|
|
191
215
|
it('should throw when tokenMint is BTC.b but env has no BTC.b mint configured', async () => {
|
|
192
216
|
const { getConfig } = await import('../../const/getConfig');
|
|
193
|
-
vi.mocked(getConfig).mockReturnValueOnce({
|
|
217
|
+
vi.mocked(getConfig).mockReturnValueOnce({
|
|
218
|
+
...fullConfig,
|
|
219
|
+
btcbTokenMint: null,
|
|
220
|
+
});
|
|
194
221
|
|
|
195
222
|
await expect(
|
|
196
223
|
redeemForBtcFn(mockProvider as any, baseParams),
|
|
@@ -282,7 +309,10 @@ describe('redeemForBtc', () => {
|
|
|
282
309
|
|
|
283
310
|
it('should wrap errors with SolanaSdkError', async () => {
|
|
284
311
|
const { getConfig } = await import('../../const/getConfig');
|
|
285
|
-
vi.mocked(getConfig).mockReturnValueOnce({
|
|
312
|
+
vi.mocked(getConfig).mockReturnValueOnce({
|
|
313
|
+
...fullConfig,
|
|
314
|
+
assetRouter: null,
|
|
315
|
+
});
|
|
286
316
|
|
|
287
317
|
try {
|
|
288
318
|
await redeemForBtcFn(mockProvider as any, baseParams);
|