@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.
- package/README.md +48 -15
- package/dist/index.cjs +1 -1
- package/dist/index.js +37 -31
- package/dist/index2.cjs +57 -54
- package/dist/index2.js +7608 -7206
- package/package.json +2 -2
- package/src/const/errors.ts +0 -4
- package/src/const/getConfig.ts +43 -20
- package/src/const/rpcUrls.ts +2 -2
- package/src/idl/asset_router.json +548 -179
- package/src/idl/consortium.json +24 -43
- package/src/idl/mailbox.json +118 -107
- package/src/index.ts +1 -3
- package/src/services/SolanaServiceImpl.test.ts +123 -0
- package/src/services/SolanaServiceImpl.ts +53 -17
- package/src/stories/components/OutputSelector/OutputSelector.tsx +1 -0
- package/src/types/errors.ts +2 -0
- package/src/utils/createDebugLogger.ts +6 -13
- package/src/utils/errors.ts +2 -0
- package/src/utils/tokenAccount.ts +3 -1
- package/src/utils/transactions.ts +1 -1
- package/src/web3Sdk/claimToken/claimBtcb.ts +37 -28
- package/src/web3Sdk/claimToken/claimLbtcGmp.ts +66 -8
- package/src/web3Sdk/claimToken/claimToken.stories.tsx +2 -2
- package/src/web3Sdk/claimToken/claimToken.ts +20 -16
- package/src/web3Sdk/claimToken/constants.ts +5 -0
- package/src/web3Sdk/claimToken/index.ts +1 -0
- package/src/web3Sdk/claimToken/shared.ts +88 -80
- package/src/web3Sdk/deposit/deposit.stories.tsx +240 -0
- package/src/web3Sdk/deposit/deposit.test.ts +327 -0
- package/src/web3Sdk/deposit/deposit.ts +339 -0
- package/src/web3Sdk/deposit/index.ts +1 -0
- package/src/web3Sdk/getTokenFeeConfig/getTokenFeeConfig.stories.tsx +166 -0
- package/src/web3Sdk/getTokenFeeConfig/getTokenFeeConfig.test.ts +224 -0
- package/src/web3Sdk/getTokenFeeConfig/getTokenFeeConfig.ts +154 -0
- package/src/web3Sdk/getTokenFeeConfig/index.ts +11 -0
- package/src/web3Sdk/index.ts +3 -4
- package/src/web3Sdk/redeem/index.ts +1 -0
- package/src/web3Sdk/redeem/redeem.stories.tsx +226 -0
- package/src/web3Sdk/redeem/redeem.test.ts +327 -0
- package/src/web3Sdk/redeem/redeem.ts +352 -0
- package/src/web3Sdk/redeemToken/redeemBtcb.ts +174 -0
- package/src/web3Sdk/redeemToken/redeemForBtc.stories.tsx +35 -21
- package/src/web3Sdk/redeemToken/redeemForBtc.test.ts +306 -0
- package/src/web3Sdk/redeemToken/redeemForBtc.ts +54 -215
- package/src/web3Sdk/redeemToken/redeemLbtc.ts +174 -0
- package/src/web3Sdk/redeemToken/shared.test.ts +45 -0
- package/src/web3Sdk/redeemToken/shared.ts +97 -0
- package/src/web3Sdk/claimLBTC/claimLBTC.stories.tsx +0 -189
- package/src/web3Sdk/claimLBTC/claimLBTC.ts +0 -225
- package/src/web3Sdk/claimLBTC/index.ts +0 -1
- package/src/web3Sdk/claimLBTC/utils/generateDepositId.ts +0 -75
- package/src/web3Sdk/claimLBTC/utils/index.ts +0 -2
- package/src/web3Sdk/claimLBTC/utils/parseTransactionLogs.ts +0 -44
- package/src/web3Sdk/claimLBTC/utils/payloadUtils.ts +0 -58
- package/src/web3Sdk/claimLBTC/utils/postMintSignatures.ts +0 -50
- package/src/web3Sdk/unstakeLBTC/index.ts +0 -1
- package/src/web3Sdk/unstakeLBTC/unstakeLBTC.stories.tsx +0 -141
- package/src/web3Sdk/unstakeLBTC/unstakeLBTC.ts +0 -140
- /package/src/web3Sdk/{claimLBTC → claimToken}/utils/__tests__/signatureUtils.test.ts +0 -0
- /package/src/web3Sdk/{claimLBTC → claimToken}/utils/signatureUtils.ts +0 -0
|
@@ -0,0 +1,306 @@
|
|
|
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 { SolanaNetwork } from '../../types';
|
|
6
|
+
import { ErrorCode, SolanaSdkError } from '../../utils';
|
|
7
|
+
|
|
8
|
+
// ── Mocks ──
|
|
9
|
+
|
|
10
|
+
const mockRedeemBtcbForBtc = vi.fn().mockResolvedValue('btcb-sig');
|
|
11
|
+
const mockRedeemLbtcForBtc = vi.fn().mockResolvedValue('lbtc-sig');
|
|
12
|
+
|
|
13
|
+
vi.mock('./redeemBtcb', () => ({ redeemBtcbForBtc: (...a: unknown[]) => mockRedeemBtcbForBtc(...a) }));
|
|
14
|
+
vi.mock('./redeemLbtc', () => ({ redeemLbtcForBtc: (...a: unknown[]) => mockRedeemLbtcForBtc(...a) }));
|
|
15
|
+
|
|
16
|
+
const MOCK_LBTC_MINT = 'LBTCojyVJ63rsEED2DLEGWMzSxWJyQynXE91LMLgV1J';
|
|
17
|
+
const MOCK_BTCB_MINT = 'BTCB3ripBAut19jM8kDPVbJHb2ZdR2GcZvGZkCmFPtV8';
|
|
18
|
+
const MOCK_ASSET_ROUTER = 'LomVyJDZ91jeVbNnTupJXKJTQFakJVMc87CmwDHYt95';
|
|
19
|
+
const MOCK_MAILBOX = 'LomJw912MoUd7iiAesTQAgz1paLcTqi6ndG3w3pnKH9';
|
|
20
|
+
const MOCK_SOLANA_CHAIN_ID = '0259db5080fc2c6d3bcf7ca90712d3c2e5e6c28f27f0dfbb9953bdb0894c03ab';
|
|
21
|
+
const MOCK_BITCOIN_CHAIN_ID = 'ff000008819873e925422c1ff0f99f7cc9bbb232af63a077a480a3633bee1ef6';
|
|
22
|
+
const MOCK_LEDGER_CHAIN_ID = '031f51c4e4cc1dae1c752d2f8fe2ae045da668a13f2e47a465964d630f5ed22e';
|
|
23
|
+
const MOCK_PAYER = '8yarEiDaJVikHZbk3PQSoWiDn2T3oM1FHZN1Jv4VZFdr';
|
|
24
|
+
|
|
25
|
+
const fullConfig: IConfig = {
|
|
26
|
+
lbtcTokenMint: MOCK_LBTC_MINT,
|
|
27
|
+
btcbTokenMint: MOCK_BTCB_MINT,
|
|
28
|
+
assetRouter: MOCK_ASSET_ROUTER,
|
|
29
|
+
mailbox: MOCK_MAILBOX,
|
|
30
|
+
solanaRoutingChainId: MOCK_SOLANA_CHAIN_ID,
|
|
31
|
+
bitcoinRoutingChainId: MOCK_BITCOIN_CHAIN_ID,
|
|
32
|
+
ledgerChainId: MOCK_LEDGER_CHAIN_ID,
|
|
33
|
+
lbtcProgramId: 'HEY7PCJe3GB27UWdopuYb1xDbB5SNtTcYPxRjntvfBSA',
|
|
34
|
+
treasuryAddress: 'ByHNGi4zPJw5StyWZoLQJ9n2wT12oupJF2pTSNKMnnAZ',
|
|
35
|
+
bascule: null,
|
|
36
|
+
basculeData: null,
|
|
37
|
+
admin: '6MKjyWZnkSMitJYAixvJzqhJiVsjTA3hYHX8aP9qNioj',
|
|
38
|
+
lzOftAdapter: 'AEFwQgaSNhQcZhAcGZGM9iTyGML3fsJC2aBvYmzV81FE',
|
|
39
|
+
lzOftStore: '3SG3oyrG3KSvJ9bbxPDu7ZXEe5o1TW1QkgudkKvK6FK4',
|
|
40
|
+
lzMultisig: 'GfYV1f1bR9vy41mSyQ8quxYbds121kijSBj5A3nG8oDQ',
|
|
41
|
+
lzEscrow: 'GRq2yasTvWWPPqSwxCZvqfCTfDhP3MswDH4nW2v6F5To',
|
|
42
|
+
consortium: null,
|
|
43
|
+
ratioOracle: null,
|
|
44
|
+
bridge: null,
|
|
45
|
+
lombardTokenPool: null,
|
|
46
|
+
};
|
|
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
|
+
vi.mock('../../const/rpcUrls', () => ({
|
|
55
|
+
getConnection: vi.fn(() => mockConnection),
|
|
56
|
+
}));
|
|
57
|
+
|
|
58
|
+
vi.mock('@lombard.finance/sdk-common', () => ({
|
|
59
|
+
getOutputScript: vi.fn().mockResolvedValue('0x001234abcd'),
|
|
60
|
+
}));
|
|
61
|
+
|
|
62
|
+
vi.mock('../../utils/tokenAccount', () => ({
|
|
63
|
+
getTokenProgramForMint: vi.fn().mockResolvedValue(new PublicKey('TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA')),
|
|
64
|
+
}));
|
|
65
|
+
|
|
66
|
+
vi.mock('../../idl/getAssetRouterIdl', () => ({
|
|
67
|
+
getAssetRouterIdl: vi.fn(() => ({
|
|
68
|
+
address: MOCK_ASSET_ROUTER,
|
|
69
|
+
metadata: { name: 'asset_router', version: '0.1.0', spec: '0.1.0' },
|
|
70
|
+
instructions: [],
|
|
71
|
+
})),
|
|
72
|
+
}));
|
|
73
|
+
|
|
74
|
+
// Mock Program constructor — needed by redeemForBtc to create assetRouterProgram
|
|
75
|
+
vi.mock('@coral-xyz/anchor', () => ({
|
|
76
|
+
Program: vi.fn().mockImplementation(() => ({})),
|
|
77
|
+
}));
|
|
78
|
+
|
|
79
|
+
// Build AR config data: 8 (disc) + 32 (admin) + 32 (pending_admin) + 32 (treasury) + 1 (paused=0)
|
|
80
|
+
function buildArConfigData(paused = false) {
|
|
81
|
+
const data = Buffer.alloc(105, 0);
|
|
82
|
+
const treasury = new PublicKey('8yarEiDaJVikHZbk3PQSoWiDn2T3oM1FHZN1Jv4VZFdr');
|
|
83
|
+
treasury.toBuffer().copy(data, 72);
|
|
84
|
+
data[104] = paused ? 1 : 0;
|
|
85
|
+
return data;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Build mailbox config data: 8 (disc) + 32 (admin) + 32 (pending_admin) + 32 (treasury) = 104
|
|
89
|
+
function buildMailboxConfigData() {
|
|
90
|
+
const data = Buffer.alloc(104, 0);
|
|
91
|
+
const treasury = new PublicKey('8yarEiDaJVikHZbk3PQSoWiDn2T3oM1FHZN1Jv4VZFdr');
|
|
92
|
+
treasury.toBuffer().copy(data, 72);
|
|
93
|
+
return data;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const mockConnection = {
|
|
97
|
+
getAccountInfo: vi.fn().mockImplementation((pubkey: PublicKey) => {
|
|
98
|
+
const key = pubkey.toBase58();
|
|
99
|
+
const [arPDA] = PublicKey.findProgramAddressSync(
|
|
100
|
+
[Buffer.from('asset_router_config')],
|
|
101
|
+
new PublicKey(MOCK_ASSET_ROUTER),
|
|
102
|
+
);
|
|
103
|
+
if (key === arPDA.toBase58()) {
|
|
104
|
+
return Promise.resolve({ data: buildArConfigData() });
|
|
105
|
+
}
|
|
106
|
+
return Promise.resolve({ data: buildMailboxConfigData() });
|
|
107
|
+
}),
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const mockProvider = { publicKey: MOCK_PAYER };
|
|
111
|
+
|
|
112
|
+
const baseParams = {
|
|
113
|
+
amount: '100000',
|
|
114
|
+
btcAddress: 'bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4',
|
|
115
|
+
network: SolanaNetwork.devnet,
|
|
116
|
+
tokenMint: MOCK_BTCB_MINT,
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
describe('redeemForBtc', () => {
|
|
120
|
+
let redeemForBtcFn: typeof import('./redeemForBtc').redeemForBtc;
|
|
121
|
+
|
|
122
|
+
beforeEach(async () => {
|
|
123
|
+
vi.clearAllMocks();
|
|
124
|
+
|
|
125
|
+
// Re-establish default mock implementations cleared by previous tests
|
|
126
|
+
mockConnection.getAccountInfo.mockImplementation((pubkey: PublicKey) => {
|
|
127
|
+
const [arPDA] = PublicKey.findProgramAddressSync(
|
|
128
|
+
[Buffer.from('asset_router_config')],
|
|
129
|
+
new PublicKey(MOCK_ASSET_ROUTER),
|
|
130
|
+
);
|
|
131
|
+
if (pubkey.toBase58() === arPDA.toBase58()) {
|
|
132
|
+
return Promise.resolve({ data: buildArConfigData() });
|
|
133
|
+
}
|
|
134
|
+
return Promise.resolve({ data: buildMailboxConfigData() });
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
const mod = await import('./redeemForBtc');
|
|
138
|
+
redeemForBtcFn = mod.redeemForBtc;
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
afterEach(() => {
|
|
142
|
+
vi.clearAllMocks();
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
// ── Validation ──
|
|
146
|
+
|
|
147
|
+
it('should throw when wallet is not connected', async () => {
|
|
148
|
+
const noWalletProvider = { publicKey: null };
|
|
149
|
+
|
|
150
|
+
await expect(
|
|
151
|
+
redeemForBtcFn(noWalletProvider as any, baseParams),
|
|
152
|
+
).rejects.toThrow('Wallet not connected');
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('should throw when Asset Router is not configured', async () => {
|
|
156
|
+
const { getConfig } = await import('../../const/getConfig');
|
|
157
|
+
vi.mocked(getConfig).mockReturnValueOnce({ ...fullConfig, assetRouter: null });
|
|
158
|
+
|
|
159
|
+
await expect(
|
|
160
|
+
redeemForBtcFn(mockProvider as any, baseParams),
|
|
161
|
+
).rejects.toThrow('Asset Router not configured');
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it('should throw when Mailbox is not configured', async () => {
|
|
165
|
+
const { getConfig } = await import('../../const/getConfig');
|
|
166
|
+
vi.mocked(getConfig).mockReturnValueOnce({ ...fullConfig, mailbox: null });
|
|
167
|
+
|
|
168
|
+
await expect(
|
|
169
|
+
redeemForBtcFn(mockProvider as any, baseParams),
|
|
170
|
+
).rejects.toThrow('Mailbox not configured');
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it('should throw when Solana routing chain ID is not configured', async () => {
|
|
174
|
+
const { getConfig } = await import('../../const/getConfig');
|
|
175
|
+
vi.mocked(getConfig).mockReturnValueOnce({ ...fullConfig, solanaRoutingChainId: null });
|
|
176
|
+
|
|
177
|
+
await expect(
|
|
178
|
+
redeemForBtcFn(mockProvider as any, baseParams),
|
|
179
|
+
).rejects.toThrow('Solana routing chain ID not configured');
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('should throw when Bitcoin routing chain ID is not configured', async () => {
|
|
183
|
+
const { getConfig } = await import('../../const/getConfig');
|
|
184
|
+
vi.mocked(getConfig).mockReturnValueOnce({ ...fullConfig, bitcoinRoutingChainId: null });
|
|
185
|
+
|
|
186
|
+
await expect(
|
|
187
|
+
redeemForBtcFn(mockProvider as any, baseParams),
|
|
188
|
+
).rejects.toThrow('Bitcoin routing chain ID not configured');
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it('should throw when tokenMint is BTC.b but env has no BTC.b mint configured', async () => {
|
|
192
|
+
const { getConfig } = await import('../../const/getConfig');
|
|
193
|
+
vi.mocked(getConfig).mockReturnValueOnce({ ...fullConfig, btcbTokenMint: null });
|
|
194
|
+
|
|
195
|
+
await expect(
|
|
196
|
+
redeemForBtcFn(mockProvider as any, baseParams),
|
|
197
|
+
).rejects.toThrow(/Unsupported tokenMint for redeemForBtc/);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it('should throw when tokenMint override is not LBTC or BTC.b for the network', async () => {
|
|
201
|
+
const foreignMint = 'So11111111111111111111111111111111111111112';
|
|
202
|
+
|
|
203
|
+
await expect(
|
|
204
|
+
redeemForBtcFn(mockProvider as any, {
|
|
205
|
+
...baseParams,
|
|
206
|
+
tokenMint: foreignMint,
|
|
207
|
+
}),
|
|
208
|
+
).rejects.toThrow(/Unsupported tokenMint for redeemForBtc/);
|
|
209
|
+
|
|
210
|
+
expect(mockRedeemBtcbForBtc).not.toHaveBeenCalled();
|
|
211
|
+
expect(mockRedeemLbtcForBtc).not.toHaveBeenCalled();
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it('should throw when amount is invalid', async () => {
|
|
215
|
+
await expect(
|
|
216
|
+
redeemForBtcFn(mockProvider as any, { ...baseParams, amount: '0' }),
|
|
217
|
+
).rejects.toThrow('greater than zero');
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it('should throw when Asset Router is paused', async () => {
|
|
221
|
+
const arPDA = PublicKey.findProgramAddressSync(
|
|
222
|
+
[Buffer.from('asset_router_config')],
|
|
223
|
+
new PublicKey(MOCK_ASSET_ROUTER),
|
|
224
|
+
)[0];
|
|
225
|
+
|
|
226
|
+
mockConnection.getAccountInfo.mockImplementation((pubkey: PublicKey) => {
|
|
227
|
+
if (pubkey.toBase58() === arPDA.toBase58()) {
|
|
228
|
+
return Promise.resolve({ data: buildArConfigData(true) });
|
|
229
|
+
}
|
|
230
|
+
return Promise.resolve({ data: buildMailboxConfigData() });
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
await expect(
|
|
234
|
+
redeemForBtcFn(mockProvider as any, baseParams),
|
|
235
|
+
).rejects.toThrow('Asset Router is paused');
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
// ── Routing ──
|
|
239
|
+
|
|
240
|
+
it('should route to redeemBtcbForBtc when tokenMint is the configured BTC.b mint', async () => {
|
|
241
|
+
await redeemForBtcFn(mockProvider as any, baseParams);
|
|
242
|
+
|
|
243
|
+
expect(mockRedeemBtcbForBtc).toHaveBeenCalledTimes(1);
|
|
244
|
+
expect(mockRedeemLbtcForBtc).not.toHaveBeenCalled();
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it('should route to redeemLbtcForBtc when tokenMint equals LBTC mint', async () => {
|
|
248
|
+
await redeemForBtcFn(mockProvider as any, {
|
|
249
|
+
...baseParams,
|
|
250
|
+
tokenMint: MOCK_LBTC_MINT,
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
expect(mockRedeemLbtcForBtc).toHaveBeenCalledTimes(1);
|
|
254
|
+
expect(mockRedeemBtcbForBtc).not.toHaveBeenCalled();
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
it('should route to redeemBtcbForBtc when tokenMint is explicitly BTC.b', async () => {
|
|
258
|
+
await redeemForBtcFn(mockProvider as any, {
|
|
259
|
+
...baseParams,
|
|
260
|
+
tokenMint: MOCK_BTCB_MINT,
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
expect(mockRedeemBtcbForBtc).toHaveBeenCalledTimes(1);
|
|
264
|
+
expect(mockRedeemLbtcForBtc).not.toHaveBeenCalled();
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it('should pass correct RedeemContext to sub-flow', async () => {
|
|
268
|
+
await redeemForBtcFn(mockProvider as any, baseParams);
|
|
269
|
+
|
|
270
|
+
const ctx = mockRedeemBtcbForBtc.mock.calls[0][0];
|
|
271
|
+
expect(ctx.payer).toEqual(new PublicKey(MOCK_PAYER));
|
|
272
|
+
expect(ctx.mint).toEqual(new PublicKey(MOCK_BTCB_MINT));
|
|
273
|
+
expect(ctx.assetRouterProgramId).toEqual(new PublicKey(MOCK_ASSET_ROUTER));
|
|
274
|
+
expect(ctx.mailboxProgramId).toEqual(new PublicKey(MOCK_MAILBOX));
|
|
275
|
+
expect(ctx.env).toBe('dev');
|
|
276
|
+
expect(ctx.config).toBe(fullConfig);
|
|
277
|
+
expect(ctx.scriptPubKey).toBeInstanceOf(Buffer);
|
|
278
|
+
expect(ctx.scriptPubKey.length).toBeGreaterThan(0);
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
// ── Error wrapping ──
|
|
282
|
+
|
|
283
|
+
it('should wrap errors with SolanaSdkError', async () => {
|
|
284
|
+
const { getConfig } = await import('../../const/getConfig');
|
|
285
|
+
vi.mocked(getConfig).mockReturnValueOnce({ ...fullConfig, assetRouter: null });
|
|
286
|
+
|
|
287
|
+
try {
|
|
288
|
+
await redeemForBtcFn(mockProvider as any, baseParams);
|
|
289
|
+
expect.unreachable('should have thrown');
|
|
290
|
+
} catch (err) {
|
|
291
|
+
expect(err).toBeInstanceOf(SolanaSdkError);
|
|
292
|
+
expect((err as SolanaSdkError).code).toBe(ErrorCode.REDEEM_REJECTED);
|
|
293
|
+
}
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
it('should use env override when provided', async () => {
|
|
297
|
+
const { getConfig } = await import('../../const/getConfig');
|
|
298
|
+
|
|
299
|
+
await redeemForBtcFn(mockProvider as any, {
|
|
300
|
+
...baseParams,
|
|
301
|
+
env: 'stage',
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
expect(getConfig).toHaveBeenCalledWith('stage');
|
|
305
|
+
});
|
|
306
|
+
});
|
|
@@ -1,76 +1,35 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
ASSOCIATED_TOKEN_PROGRAM_ID,
|
|
5
|
-
getAssociatedTokenAddress,
|
|
6
|
-
} from '@solana/spl-token';
|
|
7
|
-
import { PublicKey, SystemProgram } from '@solana/web3.js';
|
|
1
|
+
import { Program } from '@coral-xyz/anchor';
|
|
2
|
+
import { getOutputScript } from '@lombard.finance/sdk-common';
|
|
3
|
+
import { PublicKey } from '@solana/web3.js';
|
|
8
4
|
|
|
9
5
|
import { DEFAULT_ENV, getConfig, networkToEnv } from '../../const/getConfig';
|
|
10
6
|
import { getConnection } from '../../const/rpcUrls';
|
|
11
7
|
import { getAssetRouterIdl } from '../../idl/getAssetRouterIdl';
|
|
12
|
-
import { ISolanaWalletProvider
|
|
8
|
+
import { ISolanaWalletProvider } from '../../types';
|
|
13
9
|
import {
|
|
14
10
|
ErrorCode,
|
|
15
|
-
sendAndConfirmTransaction,
|
|
16
11
|
SolanaSdkError,
|
|
17
12
|
} from '../../utils';
|
|
18
13
|
import { createDebugLogger } from '../../utils/createDebugLogger';
|
|
19
14
|
import { getTokenProgramForMint } from '../../utils/tokenAccount';
|
|
15
|
+
import { redeemBtcbForBtc } from './redeemBtcb';
|
|
16
|
+
import { redeemLbtcForBtc } from './redeemLbtc';
|
|
17
|
+
import { RedeemContext, RedeemForBtcParams, validateAmount } from './shared';
|
|
20
18
|
|
|
21
|
-
|
|
22
|
-
* BTC native token address in Lombard protocol (to_token_address for BTC in token_route PDA).
|
|
23
|
-
* BTC is represented as 0x...01 (32 bytes, value 1).
|
|
24
|
-
*/
|
|
25
|
-
const BTC_NATIVE_TOKEN_ADDRESS = (() => {
|
|
26
|
-
const buf = Buffer.alloc(32, 0);
|
|
27
|
-
buf[31] = 1;
|
|
28
|
-
return buf;
|
|
29
|
-
})();
|
|
30
|
-
|
|
31
|
-
export interface RedeemForBtcParams {
|
|
32
|
-
amount: string;
|
|
33
|
-
btcAddress: string;
|
|
34
|
-
/**
|
|
35
|
-
* BTC.b mint address override. Defaults to config.btcbTokenMint.
|
|
36
|
-
*/
|
|
37
|
-
tokenMint?: string;
|
|
38
|
-
network: SolanaNetwork;
|
|
39
|
-
/**
|
|
40
|
-
* Optional environment override. When provided, used instead of
|
|
41
|
-
* the default `networkToEnv[network]` mapping to resolve config.
|
|
42
|
-
* Useful when multiple environments share the same Solana network
|
|
43
|
-
* (e.g. both 'dev' and 'stage' use devnet).
|
|
44
|
-
*/
|
|
45
|
-
env?: Env;
|
|
46
|
-
rpcUrl?: string;
|
|
47
|
-
debug?: boolean;
|
|
48
|
-
/**
|
|
49
|
-
* Skip preflight transaction simulation before broadcast.
|
|
50
|
-
*
|
|
51
|
-
* Defaults to `false` (simulation enabled). Set to `true` if preflight
|
|
52
|
-
* simulation gives false negatives — for example, when the simulation node
|
|
53
|
-
* has not yet seen the latest global nonce for the `outbound_message` PDA,
|
|
54
|
-
* leading to a spurious `ConstraintSeeds (0x7d6)` failure even though the
|
|
55
|
-
* transaction would land correctly on-chain. The retry loop inside
|
|
56
|
-
* `redeemForBtc` already handles the `0x7d6` error returned by
|
|
57
|
-
* `confirmTransaction`, so enabling preflight only adds a redundant check
|
|
58
|
-
* at the cost of potential false negatives on lagging RPC nodes.
|
|
59
|
-
*/
|
|
60
|
-
skipPreflight?: boolean;
|
|
61
|
-
}
|
|
19
|
+
export type { RedeemForBtcParams } from './shared';
|
|
62
20
|
|
|
63
21
|
/**
|
|
64
|
-
* Redeem BTC.b → BTC on Solana via Asset Router
|
|
22
|
+
* Redeem tokens (BTC.b or LBTC) → BTC on Solana via Asset Router.
|
|
65
23
|
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
24
|
+
* Routes to the appropriate flow based on the token mint:
|
|
25
|
+
* - BTC.b mint → burns BTC.b via `redeem_for_btc`
|
|
26
|
+
* - LBTC mint → burns LBTC via LBTC-specific redemption flow
|
|
68
27
|
*/
|
|
69
28
|
export async function redeemForBtc(
|
|
70
29
|
provider: ISolanaWalletProvider,
|
|
71
30
|
params: RedeemForBtcParams,
|
|
72
31
|
): Promise<string> {
|
|
73
|
-
const { amount, btcAddress, network, env: envOverride, rpcUrl, debug = false
|
|
32
|
+
const { amount, btcAddress, network, env: envOverride, rpcUrl, debug = false } = params;
|
|
74
33
|
const { debugLog, printLogs } = createDebugLogger({ debug });
|
|
75
34
|
|
|
76
35
|
try {
|
|
@@ -94,28 +53,18 @@ export async function redeemForBtc(
|
|
|
94
53
|
throw new Error(`Bitcoin routing chain ID not configured for network: ${network}`);
|
|
95
54
|
}
|
|
96
55
|
|
|
97
|
-
const mintAddress = params.tokenMint
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
// Validate amount: must be a non-zero positive integer within u64 range
|
|
103
|
-
const U64_MAX = 18446744073709551615n;
|
|
104
|
-
if (!/^\d+$/.test(amount)) {
|
|
105
|
-
throw new Error(
|
|
106
|
-
`Invalid amount "${amount}": must be a positive integer string (lamports, no decimals or signs)`,
|
|
107
|
-
);
|
|
108
|
-
}
|
|
109
|
-
const parsedAmount = BigInt(amount);
|
|
110
|
-
if (parsedAmount === 0n) {
|
|
111
|
-
throw new Error('Amount must be greater than zero');
|
|
112
|
-
}
|
|
113
|
-
if (parsedAmount > U64_MAX) {
|
|
56
|
+
const mintAddress = params.tokenMint;
|
|
57
|
+
const supportedMints = [config.lbtcTokenMint, config.btcbTokenMint].filter(
|
|
58
|
+
(m): m is string => m != null,
|
|
59
|
+
);
|
|
60
|
+
if (!supportedMints.includes(mintAddress)) {
|
|
114
61
|
throw new Error(
|
|
115
|
-
`
|
|
62
|
+
`Unsupported tokenMint for redeemForBtc: ${mintAddress}. Use the configured LBTC or BTC.b mint for network: ${network}.`,
|
|
116
63
|
);
|
|
117
64
|
}
|
|
118
65
|
|
|
66
|
+
validateAmount(amount);
|
|
67
|
+
|
|
119
68
|
const connection = getConnection(network, rpcUrl);
|
|
120
69
|
const payer = new PublicKey(provider.publicKey);
|
|
121
70
|
const mint = new PublicKey(mintAddress);
|
|
@@ -140,56 +89,20 @@ export async function redeemForBtc(
|
|
|
140
89
|
const tokenProgramId = await getTokenProgramForMint(connection, mint);
|
|
141
90
|
debugLog('Token program:', tokenProgramId.toBase58());
|
|
142
91
|
|
|
143
|
-
// ── Asset Router
|
|
92
|
+
// ── Asset Router config PDA ──
|
|
144
93
|
const [assetRouterConfigPDA] = PublicKey.findProgramAddressSync(
|
|
145
94
|
[Buffer.from('asset_router_config')],
|
|
146
95
|
assetRouterProgramId,
|
|
147
96
|
);
|
|
148
|
-
const [tokenConfigPDA] = PublicKey.findProgramAddressSync(
|
|
149
|
-
[Buffer.from('token_config'), mint.toBuffer()],
|
|
150
|
-
assetRouterProgramId,
|
|
151
|
-
);
|
|
152
|
-
const [tokenRoutePDA] = PublicKey.findProgramAddressSync(
|
|
153
|
-
[
|
|
154
|
-
Buffer.from('token_route'),
|
|
155
|
-
solanaRoutingChainId,
|
|
156
|
-
mint.toBuffer(),
|
|
157
|
-
bitcoinRoutingChainId,
|
|
158
|
-
BTC_NATIVE_TOKEN_ADDRESS,
|
|
159
|
-
],
|
|
160
|
-
assetRouterProgramId,
|
|
161
|
-
);
|
|
162
|
-
const [messagingAuthorityPDA] = PublicKey.findProgramAddressSync(
|
|
163
|
-
[Buffer.from('messaging_authority')],
|
|
164
|
-
assetRouterProgramId,
|
|
165
|
-
);
|
|
166
|
-
|
|
167
|
-
debugLog('Asset Router config PDA:', assetRouterConfigPDA.toBase58());
|
|
168
|
-
debugLog('Token config PDA:', tokenConfigPDA.toBase58());
|
|
169
|
-
debugLog('Token route PDA:', tokenRoutePDA.toBase58());
|
|
170
|
-
debugLog('Messaging authority PDA:', messagingAuthorityPDA.toBase58());
|
|
171
97
|
|
|
172
|
-
// ── Mailbox
|
|
98
|
+
// ── Mailbox config PDA ──
|
|
173
99
|
const [mailboxConfigPDA] = PublicKey.findProgramAddressSync(
|
|
174
100
|
[Buffer.from('mailbox_config')],
|
|
175
101
|
mailboxProgramId,
|
|
176
102
|
);
|
|
177
|
-
if (!config.ledgerChainId) {
|
|
178
|
-
throw new Error(`Ledger chain ID not configured for network: ${network}`);
|
|
179
|
-
}
|
|
180
|
-
const ledgerChainId = Buffer.from(config.ledgerChainId, 'hex');
|
|
181
|
-
const [outboundMessagePathPDA] = PublicKey.findProgramAddressSync(
|
|
182
|
-
[Buffer.from('outbound_message_path'), ledgerChainId],
|
|
183
|
-
mailboxProgramId,
|
|
184
|
-
);
|
|
185
|
-
const [senderConfigPDA] = PublicKey.findProgramAddressSync(
|
|
186
|
-
[Buffer.from('sender_config'), assetRouterProgramId.toBuffer()],
|
|
187
|
-
mailboxProgramId,
|
|
188
|
-
);
|
|
189
103
|
|
|
104
|
+
debugLog('Asset Router config PDA:', assetRouterConfigPDA.toBase58());
|
|
190
105
|
debugLog('Mailbox config PDA:', mailboxConfigPDA.toBase58());
|
|
191
|
-
debugLog('Outbound message path PDA:', outboundMessagePathPDA.toBase58());
|
|
192
|
-
debugLog('Sender config PDA:', senderConfigPDA.toBase58());
|
|
193
106
|
|
|
194
107
|
// ── Read on-chain state ──
|
|
195
108
|
const [arConfigInfo, mailboxConfigInfo] = await Promise.all([
|
|
@@ -230,124 +143,50 @@ export async function redeemForBtc(
|
|
|
230
143
|
);
|
|
231
144
|
debugLog('Mailbox treasury:', mailboxTreasury.toBase58());
|
|
232
145
|
|
|
233
|
-
// ── Token accounts ──
|
|
234
|
-
const payerTokenAccount = await getAssociatedTokenAddress(
|
|
235
|
-
mint,
|
|
236
|
-
payer,
|
|
237
|
-
false,
|
|
238
|
-
tokenProgramId,
|
|
239
|
-
ASSOCIATED_TOKEN_PROGRAM_ID,
|
|
240
|
-
);
|
|
241
|
-
const treasuryTokenAccount = await getAssociatedTokenAddress(
|
|
242
|
-
mint,
|
|
243
|
-
arTreasury,
|
|
244
|
-
true,
|
|
245
|
-
tokenProgramId,
|
|
246
|
-
ASSOCIATED_TOKEN_PROGRAM_ID,
|
|
247
|
-
);
|
|
248
|
-
|
|
249
|
-
debugLog('Payer token account:', payerTokenAccount.toBase58());
|
|
250
|
-
debugLog('Treasury token account:', treasuryTokenAccount.toBase58());
|
|
251
|
-
|
|
252
|
-
// ── Balance check ──
|
|
253
|
-
const tokenBalance = await connection.getTokenAccountBalance(
|
|
254
|
-
payerTokenAccount,
|
|
255
|
-
);
|
|
256
|
-
const userBalance = BigInt(tokenBalance.value.amount);
|
|
257
|
-
if (userBalance < parsedAmount) {
|
|
258
|
-
throw new Error(
|
|
259
|
-
`Insufficient BTC.b balance: have ${tokenBalance.value.uiAmountString}, need ${Number(parsedAmount) / 1e8}`,
|
|
260
|
-
);
|
|
261
|
-
}
|
|
262
|
-
|
|
263
146
|
const assetRouterProgram = new Program(
|
|
264
147
|
getAssetRouterIdl(env),
|
|
265
148
|
{ connection },
|
|
266
149
|
);
|
|
267
150
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
[Buffer.from('outbound_message'), nonceBuf],
|
|
290
|
-
mailboxProgramId,
|
|
291
|
-
);
|
|
292
|
-
|
|
293
|
-
debugLog(`Attempt ${attempt + 1}: global nonce=${globalNonce}, outbound_message=${outboundMessagePDA.toBase58()}`);
|
|
294
|
-
|
|
295
|
-
const tx = await assetRouterProgram.methods
|
|
296
|
-
.redeemForBtc(scriptPubKey, new BN(amount))
|
|
297
|
-
.accounts({
|
|
298
|
-
payer,
|
|
299
|
-
config: assetRouterConfigPDA,
|
|
300
|
-
tokenConfig: tokenConfigPDA,
|
|
301
|
-
tokenRoute: tokenRoutePDA,
|
|
302
|
-
payerTokenAccount,
|
|
303
|
-
tokenProgram: tokenProgramId,
|
|
304
|
-
mint,
|
|
305
|
-
treasuryTokenAccount,
|
|
306
|
-
messagingAuthority: messagingAuthorityPDA,
|
|
307
|
-
mailbox: mailboxProgramId,
|
|
308
|
-
mailboxConfig: mailboxConfigPDA,
|
|
309
|
-
outboundMessagePath: outboundMessagePathPDA,
|
|
310
|
-
outboundMessage: outboundMessagePDA,
|
|
311
|
-
senderConfig: senderConfigPDA,
|
|
312
|
-
treasury: mailboxTreasury,
|
|
313
|
-
systemProgram: SystemProgram.programId,
|
|
314
|
-
})
|
|
315
|
-
.transaction();
|
|
316
|
-
|
|
317
|
-
debugLog('Instruction account count:', tx.instructions[0]?.keys.length);
|
|
318
|
-
|
|
319
|
-
try {
|
|
320
|
-
const { signature } = await sendAndConfirmTransaction({
|
|
321
|
-
instruction: tx,
|
|
322
|
-
connection,
|
|
323
|
-
provider,
|
|
324
|
-
debugLabel: 'Asset Router redeem_for_btc',
|
|
325
|
-
skipPreflight,
|
|
326
|
-
});
|
|
151
|
+
const ctx: RedeemContext = {
|
|
152
|
+
provider,
|
|
153
|
+
params,
|
|
154
|
+
env,
|
|
155
|
+
config,
|
|
156
|
+
connection,
|
|
157
|
+
payer,
|
|
158
|
+
mint,
|
|
159
|
+
tokenProgramId,
|
|
160
|
+
scriptPubKey,
|
|
161
|
+
assetRouterProgramId,
|
|
162
|
+
mailboxProgramId,
|
|
163
|
+
solanaRoutingChainId,
|
|
164
|
+
bitcoinRoutingChainId,
|
|
165
|
+
assetRouterProgram,
|
|
166
|
+
assetRouterConfigPDA,
|
|
167
|
+
mailboxConfigPDA,
|
|
168
|
+
arTreasury,
|
|
169
|
+
mailboxTreasury,
|
|
170
|
+
debugLog,
|
|
171
|
+
};
|
|
327
172
|
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
err.message.includes('0x7d6'); // ConstraintSeeds
|
|
334
|
-
if (isNonceError && attempt < MAX_NONCE_RETRIES - 1) {
|
|
335
|
-
debugLog(`Nonce stale (ConstraintSeeds), retrying...`);
|
|
336
|
-
continue;
|
|
337
|
-
}
|
|
338
|
-
throw err;
|
|
339
|
-
}
|
|
173
|
+
// Route based on token mint
|
|
174
|
+
const isLbtc = mintAddress === config.lbtcTokenMint;
|
|
175
|
+
if (isLbtc) {
|
|
176
|
+
debugLog('Flow: LBTC → BTC redemption');
|
|
177
|
+
return await redeemLbtcForBtc(ctx);
|
|
340
178
|
}
|
|
341
179
|
|
|
342
|
-
|
|
180
|
+
debugLog('Flow: BTC.b → BTC redemption');
|
|
181
|
+
return await redeemBtcbForBtc(ctx);
|
|
343
182
|
} catch (error: unknown) {
|
|
344
183
|
if (error instanceof Error && debug) {
|
|
345
184
|
error.message = `${error.message}\n\nDebug logs:\n${printLogs()}`;
|
|
346
185
|
}
|
|
347
186
|
throw SolanaSdkError.wrap(
|
|
348
187
|
error,
|
|
349
|
-
ErrorCode.
|
|
350
|
-
'
|
|
188
|
+
ErrorCode.REDEEM_REJECTED,
|
|
189
|
+
'redeem_for_btc operation failed',
|
|
351
190
|
);
|
|
352
191
|
}
|
|
353
192
|
}
|