@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,226 @@
|
|
|
1
|
+
import { Env } from '@lombard.finance/sdk-common';
|
|
2
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
3
|
+
import { useState } from 'react';
|
|
4
|
+
|
|
5
|
+
import { envToNetwork, getConfig } from '../../const/getConfig';
|
|
6
|
+
import {
|
|
7
|
+
Button,
|
|
8
|
+
CodeBlock,
|
|
9
|
+
ConnectButton,
|
|
10
|
+
ErrorDisplay,
|
|
11
|
+
ResultDisplay,
|
|
12
|
+
SectionCard,
|
|
13
|
+
} from '../../stories/components';
|
|
14
|
+
import { functionType } from '../../stories/decorators/function-type';
|
|
15
|
+
import { useConnect } from '../../stories/hooks/useConnect';
|
|
16
|
+
import useQuery from '../../stories/hooks/useQuery';
|
|
17
|
+
import { redeem } from './redeem';
|
|
18
|
+
|
|
19
|
+
interface RedeemStoryArgs {
|
|
20
|
+
environment: Env;
|
|
21
|
+
amount: string;
|
|
22
|
+
recipient: string;
|
|
23
|
+
tokenMint: string;
|
|
24
|
+
toLchainId: string;
|
|
25
|
+
toTokenAddress: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const StoryView = ({
|
|
29
|
+
environment,
|
|
30
|
+
amount,
|
|
31
|
+
recipient,
|
|
32
|
+
tokenMint,
|
|
33
|
+
toLchainId,
|
|
34
|
+
toTokenAddress,
|
|
35
|
+
}: RedeemStoryArgs) => {
|
|
36
|
+
const network = envToNetwork[environment];
|
|
37
|
+
const config = getConfig(environment);
|
|
38
|
+
const [transactionLogs, setTransactionLogs] = useState<string[] | null>(null);
|
|
39
|
+
|
|
40
|
+
const {
|
|
41
|
+
data: connectionData,
|
|
42
|
+
error: connectError,
|
|
43
|
+
isLoading: isConnecting,
|
|
44
|
+
connect,
|
|
45
|
+
disconnect,
|
|
46
|
+
} = useConnect();
|
|
47
|
+
const isConnected = !!connectionData;
|
|
48
|
+
const address = connectionData?.address;
|
|
49
|
+
const provider = connectionData?.provider;
|
|
50
|
+
|
|
51
|
+
const effectiveMint = tokenMint || config.lbtcTokenMint;
|
|
52
|
+
const effectiveToToken = toTokenAddress || config.btcbTokenMint;
|
|
53
|
+
|
|
54
|
+
const request = async () => {
|
|
55
|
+
if (!provider || !address) throw new Error('Wallet not connected.');
|
|
56
|
+
if (!recipient) throw new Error('Recipient address is required (set in args).');
|
|
57
|
+
const parsedAmount = parseFloat(amount);
|
|
58
|
+
if (!amount || isNaN(parsedAmount) || parsedAmount <= 0)
|
|
59
|
+
throw new Error('Amount must be a positive number in BTC (set in args).');
|
|
60
|
+
|
|
61
|
+
const amountSats = Math.round(parsedAmount * 1e8).toString();
|
|
62
|
+
|
|
63
|
+
setTransactionLogs(null);
|
|
64
|
+
try {
|
|
65
|
+
const result = await redeem(provider, {
|
|
66
|
+
amount: amountSats,
|
|
67
|
+
recipient,
|
|
68
|
+
tokenMint: tokenMint || undefined,
|
|
69
|
+
toLchainId: toLchainId || undefined,
|
|
70
|
+
toTokenAddress: toTokenAddress || undefined,
|
|
71
|
+
network,
|
|
72
|
+
env: environment,
|
|
73
|
+
debug: true,
|
|
74
|
+
});
|
|
75
|
+
return result;
|
|
76
|
+
} catch (err: unknown) {
|
|
77
|
+
if (err instanceof Error && err.message.includes('Debug logs:')) {
|
|
78
|
+
const parts = err.message.split('Debug logs:\n');
|
|
79
|
+
setTransactionLogs(parts[1]?.split('\n') || []);
|
|
80
|
+
}
|
|
81
|
+
throw err;
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const {
|
|
86
|
+
data: txHash,
|
|
87
|
+
error,
|
|
88
|
+
isLoading,
|
|
89
|
+
refetch: handleRedeem,
|
|
90
|
+
} = useQuery(
|
|
91
|
+
request,
|
|
92
|
+
[provider, address, amount, recipient, tokenMint, toLchainId, toTokenAddress, environment],
|
|
93
|
+
false,
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
return (
|
|
97
|
+
<>
|
|
98
|
+
<ConnectButton
|
|
99
|
+
connect={connect}
|
|
100
|
+
disconnect={disconnect}
|
|
101
|
+
isConnected={isConnected}
|
|
102
|
+
isLoading={isConnecting}
|
|
103
|
+
error={connectError}
|
|
104
|
+
walletName={connectionData?.walletName}
|
|
105
|
+
address={connectionData?.address}
|
|
106
|
+
network={network}
|
|
107
|
+
/>
|
|
108
|
+
|
|
109
|
+
{isConnected && (
|
|
110
|
+
<>
|
|
111
|
+
<SectionCard title="Configuration">
|
|
112
|
+
<p>
|
|
113
|
+
<strong>Environment:</strong> {environment}
|
|
114
|
+
</p>
|
|
115
|
+
<p>
|
|
116
|
+
<strong>Network:</strong> {network}
|
|
117
|
+
</p>
|
|
118
|
+
<p>
|
|
119
|
+
<strong>Amount:</strong> {amount} BTC
|
|
120
|
+
</p>
|
|
121
|
+
<p>
|
|
122
|
+
<strong>Recipient:</strong> {recipient || <em>Not set</em>}
|
|
123
|
+
</p>
|
|
124
|
+
<p>
|
|
125
|
+
<strong>Source token mint:</strong>{' '}
|
|
126
|
+
{effectiveMint || <em>Not configured</em>}
|
|
127
|
+
</p>
|
|
128
|
+
<p>
|
|
129
|
+
<strong>Destination token:</strong>{' '}
|
|
130
|
+
{effectiveToToken || <em>Not configured</em>}
|
|
131
|
+
</p>
|
|
132
|
+
</SectionCard>
|
|
133
|
+
|
|
134
|
+
<div className="d-grid gap-2 my-4">
|
|
135
|
+
<Button
|
|
136
|
+
primary
|
|
137
|
+
size="large"
|
|
138
|
+
onClick={handleRedeem}
|
|
139
|
+
isLoading={isLoading}
|
|
140
|
+
actionName={redeem.name}
|
|
141
|
+
/>
|
|
142
|
+
</div>
|
|
143
|
+
|
|
144
|
+
{txHash && (
|
|
145
|
+
<ResultDisplay
|
|
146
|
+
result={txHash}
|
|
147
|
+
title="Redeem Transaction Hash"
|
|
148
|
+
successMessage="Success! Redeem transaction submitted."
|
|
149
|
+
/>
|
|
150
|
+
)}
|
|
151
|
+
{(error || connectError) && (
|
|
152
|
+
<ErrorDisplay
|
|
153
|
+
error={error || connectError}
|
|
154
|
+
title="Redeem Error"
|
|
155
|
+
/>
|
|
156
|
+
)}
|
|
157
|
+
|
|
158
|
+
{transactionLogs && transactionLogs.length > 0 && (
|
|
159
|
+
<SectionCard title="Transaction Logs (Debug)">
|
|
160
|
+
<CodeBlock text={transactionLogs.join('\n')} />
|
|
161
|
+
</SectionCard>
|
|
162
|
+
)}
|
|
163
|
+
</>
|
|
164
|
+
)}
|
|
165
|
+
</>
|
|
166
|
+
);
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
const meta: Meta<typeof StoryView> = {
|
|
170
|
+
title: 'write/redeem (Asset Router)',
|
|
171
|
+
component: StoryView,
|
|
172
|
+
tags: ['autodocs'],
|
|
173
|
+
decorators: [functionType('write')],
|
|
174
|
+
parameters: {
|
|
175
|
+
docs: {
|
|
176
|
+
description: {
|
|
177
|
+
component: `Demonstrates generic token redemption via the Asset Router's \`redeem\` instruction.
|
|
178
|
+
|
|
179
|
+
**Flow:**
|
|
180
|
+
1. Connect a Solana wallet holding the source token (defaults to LBTC)
|
|
181
|
+
2. Enter the recipient address and amount (in BTC)
|
|
182
|
+
3. Optionally override source mint, destination chain ID, and destination token
|
|
183
|
+
4. Call \`redeem\` — burns the source token and sends a GMP message through the Mailbox
|
|
184
|
+
5. The destination token is routed to the recipient's ATA for Asset Router \`native_mint\` (payload carries that token account address)`,
|
|
185
|
+
},
|
|
186
|
+
},
|
|
187
|
+
},
|
|
188
|
+
args: {
|
|
189
|
+
environment: Env.stage,
|
|
190
|
+
amount: '0.0002',
|
|
191
|
+
recipient: '',
|
|
192
|
+
tokenMint: '',
|
|
193
|
+
toLchainId: '',
|
|
194
|
+
toTokenAddress: '',
|
|
195
|
+
},
|
|
196
|
+
argTypes: {
|
|
197
|
+
environment: {
|
|
198
|
+
control: { type: 'select' },
|
|
199
|
+
options: Object.values(Env),
|
|
200
|
+
},
|
|
201
|
+
amount: {
|
|
202
|
+
control: { type: 'text' },
|
|
203
|
+
description: 'Amount to redeem in BTC (e.g. 0.0002)',
|
|
204
|
+
},
|
|
205
|
+
recipient: {
|
|
206
|
+
control: { type: 'text' },
|
|
207
|
+
description:
|
|
208
|
+
'Recipient wallet (owner); SDK uses the associated token account for on-chain native_mint in the redeem payload (Solana base58)',
|
|
209
|
+
},
|
|
210
|
+
tokenMint: {
|
|
211
|
+
control: { type: 'text' },
|
|
212
|
+
description: 'Source token mint override (defaults to LBTC from config)',
|
|
213
|
+
},
|
|
214
|
+
toLchainId: {
|
|
215
|
+
control: { type: 'text' },
|
|
216
|
+
description: 'Destination Lombard routing chain ID (hex). Defaults to Solana routing chain ID',
|
|
217
|
+
},
|
|
218
|
+
toTokenAddress: {
|
|
219
|
+
control: { type: 'text' },
|
|
220
|
+
description: 'Destination token address/mint override (defaults to BTC.b from config)',
|
|
221
|
+
},
|
|
222
|
+
},
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
export default meta;
|
|
226
|
+
type Story = StoryObj<typeof meta>;
|
|
@@ -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 { 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
|
+
const MOCK_RECIPIENT_ATA = 'GRq2yasTvWWPPqSwxCZvqfCTfDhP3MswDH4nW2v6F5To';
|
|
19
|
+
const MOCK_PAYER_LBTC_ATA = 'GfYV1f1bR9vy41mSyQ8quxYbds121kijSBj5A3nG8oDQ';
|
|
20
|
+
const MOCK_TREASURY_LBTC_ATA = '3SG3oyrG3KSvJ9bbxPDu7ZXEe5o1TW1QkgudkKvK6FK4';
|
|
21
|
+
|
|
22
|
+
const fullConfig: IConfig = {
|
|
23
|
+
lbtcTokenMint: MOCK_LBTC_MINT,
|
|
24
|
+
btcbTokenMint: MOCK_BTCB_MINT,
|
|
25
|
+
assetRouter: MOCK_ASSET_ROUTER,
|
|
26
|
+
mailbox: MOCK_MAILBOX,
|
|
27
|
+
solanaRoutingChainId: MOCK_SOLANA_CHAIN_ID,
|
|
28
|
+
bitcoinRoutingChainId: 'ff000008819873e925422c1ff0f99f7cc9bbb232af63a077a480a3633bee1ef6',
|
|
29
|
+
ledgerChainId: MOCK_LEDGER_CHAIN_ID,
|
|
30
|
+
lbtcProgramId: 'HEY7PCJe3GB27UWdopuYb1xDbB5SNtTcYPxRjntvfBSA',
|
|
31
|
+
treasuryAddress: 'ByHNGi4zPJw5StyWZoLQJ9n2wT12oupJF2pTSNKMnnAZ',
|
|
32
|
+
bascule: null,
|
|
33
|
+
basculeData: null,
|
|
34
|
+
admin: '6MKjyWZnkSMitJYAixvJzqhJiVsjTA3hYHX8aP9qNioj',
|
|
35
|
+
lzOftAdapter: 'AEFwQgaSNhQcZhAcGZGM9iTyGML3fsJC2aBvYmzV81FE',
|
|
36
|
+
lzOftStore: '3SG3oyrG3KSvJ9bbxPDu7ZXEe5o1TW1QkgudkKvK6FK4',
|
|
37
|
+
lzMultisig: 'GfYV1f1bR9vy41mSyQ8quxYbds121kijSBj5A3nG8oDQ',
|
|
38
|
+
lzEscrow: 'GRq2yasTvWWPPqSwxCZvqfCTfDhP3MswDH4nW2v6F5To',
|
|
39
|
+
consortium: null,
|
|
40
|
+
ratioOracle: null,
|
|
41
|
+
bridge: null,
|
|
42
|
+
lombardTokenPool: null,
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
// ── Mocks ──
|
|
46
|
+
|
|
47
|
+
vi.mock('../../const/getConfig', () => ({
|
|
48
|
+
DEFAULT_ENV: 'dev' as const,
|
|
49
|
+
networkToEnv: { devnet: 'dev', testnet: 'testnet', 'mainnet-beta': 'prod' },
|
|
50
|
+
getConfig: vi.fn(() => fullConfig),
|
|
51
|
+
}));
|
|
52
|
+
|
|
53
|
+
// Build AR config data through native_mint (offset 105–136) for redeem recipient ATA derivation
|
|
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
|
+
new PublicKey(MOCK_BTCB_MINT).toBuffer().copy(data, 105);
|
|
60
|
+
data.writeBigUInt64LE(42n, 137);
|
|
61
|
+
return data;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function buildMailboxConfigData() {
|
|
65
|
+
const data = Buffer.alloc(145, 0);
|
|
66
|
+
const treasury = new PublicKey(MOCK_PAYER);
|
|
67
|
+
treasury.toBuffer().copy(data, 72);
|
|
68
|
+
data.writeBigUInt64LE(42n, 137);
|
|
69
|
+
return data;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const arConfigPDA = PublicKey.findProgramAddressSync(
|
|
73
|
+
[Buffer.from('asset_router_config')],
|
|
74
|
+
new PublicKey(MOCK_ASSET_ROUTER),
|
|
75
|
+
)[0];
|
|
76
|
+
|
|
77
|
+
function defaultGetAccountInfo(pubkey: PublicKey) {
|
|
78
|
+
if (pubkey.toBase58() === arConfigPDA.toBase58()) {
|
|
79
|
+
return Promise.resolve({ data: buildArConfigData() });
|
|
80
|
+
}
|
|
81
|
+
return Promise.resolve({ data: buildMailboxConfigData() });
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const mockConnection = {
|
|
85
|
+
getAccountInfo: vi.fn().mockImplementation(defaultGetAccountInfo),
|
|
86
|
+
getTokenAccountBalance: vi.fn().mockResolvedValue({
|
|
87
|
+
value: { amount: '999999999', uiAmountString: '9.99' },
|
|
88
|
+
}),
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
vi.mock('../../const/rpcUrls', () => ({
|
|
92
|
+
getConnection: vi.fn(() => mockConnection),
|
|
93
|
+
}));
|
|
94
|
+
|
|
95
|
+
vi.mock('../../utils/tokenAccount', () => ({
|
|
96
|
+
getTokenProgramForMint: vi.fn().mockResolvedValue(new PublicKey('TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA')),
|
|
97
|
+
}));
|
|
98
|
+
|
|
99
|
+
vi.mock('../../idl/getAssetRouterIdl', () => ({
|
|
100
|
+
getAssetRouterIdl: vi.fn(() => ({
|
|
101
|
+
address: MOCK_ASSET_ROUTER,
|
|
102
|
+
metadata: { name: 'asset_router', version: '0.1.0', spec: '0.1.0' },
|
|
103
|
+
instructions: [],
|
|
104
|
+
})),
|
|
105
|
+
}));
|
|
106
|
+
|
|
107
|
+
vi.mock('@solana/spl-token', () => ({
|
|
108
|
+
getAssociatedTokenAddress: vi
|
|
109
|
+
.fn()
|
|
110
|
+
.mockImplementation(
|
|
111
|
+
async (
|
|
112
|
+
mint: PublicKey,
|
|
113
|
+
owner: PublicKey,
|
|
114
|
+
allowOwnerOffCurve?: boolean,
|
|
115
|
+
) => {
|
|
116
|
+
if (
|
|
117
|
+
mint.toBase58() === MOCK_BTCB_MINT &&
|
|
118
|
+
owner.toBase58() === MOCK_RECIPIENT
|
|
119
|
+
) {
|
|
120
|
+
return new PublicKey(MOCK_RECIPIENT_ATA);
|
|
121
|
+
}
|
|
122
|
+
if (
|
|
123
|
+
mint.toBase58() === MOCK_LBTC_MINT &&
|
|
124
|
+
owner.toBase58() === MOCK_PAYER &&
|
|
125
|
+
allowOwnerOffCurve
|
|
126
|
+
) {
|
|
127
|
+
return new PublicKey(MOCK_TREASURY_LBTC_ATA);
|
|
128
|
+
}
|
|
129
|
+
if (
|
|
130
|
+
mint.toBase58() === MOCK_LBTC_MINT &&
|
|
131
|
+
owner.toBase58() === MOCK_PAYER &&
|
|
132
|
+
!allowOwnerOffCurve
|
|
133
|
+
) {
|
|
134
|
+
return new PublicKey(MOCK_PAYER_LBTC_ATA);
|
|
135
|
+
}
|
|
136
|
+
return new PublicKey(MOCK_PAYER);
|
|
137
|
+
},
|
|
138
|
+
),
|
|
139
|
+
ASSOCIATED_TOKEN_PROGRAM_ID: new PublicKey('ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL'),
|
|
140
|
+
}));
|
|
141
|
+
|
|
142
|
+
const mockTx = { instructions: [{ keys: [] }] };
|
|
143
|
+
const mockMethods = {
|
|
144
|
+
redeem: vi.fn().mockReturnValue({
|
|
145
|
+
accounts: vi.fn().mockReturnValue({
|
|
146
|
+
transaction: vi.fn().mockResolvedValue(mockTx),
|
|
147
|
+
}),
|
|
148
|
+
}),
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
vi.mock('@coral-xyz/anchor', () => ({
|
|
152
|
+
Program: vi.fn().mockImplementation(() => ({ methods: mockMethods })),
|
|
153
|
+
BN: vi.fn().mockImplementation((v: string) => ({ toString: () => v })),
|
|
154
|
+
}));
|
|
155
|
+
|
|
156
|
+
vi.mock('../../utils', async () => {
|
|
157
|
+
const actual = await vi.importActual('../../utils');
|
|
158
|
+
return {
|
|
159
|
+
...actual,
|
|
160
|
+
sendAndConfirmTransaction: vi.fn().mockResolvedValue({ signature: 'mock-redeem-sig' }),
|
|
161
|
+
};
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
const baseParams = {
|
|
165
|
+
amount: '100000',
|
|
166
|
+
recipient: MOCK_RECIPIENT,
|
|
167
|
+
network: SolanaNetwork.devnet,
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
describe('redeem', () => {
|
|
171
|
+
let redeemFn: typeof import('./redeem').redeem;
|
|
172
|
+
|
|
173
|
+
beforeEach(async () => {
|
|
174
|
+
vi.clearAllMocks();
|
|
175
|
+
mockConnection.getAccountInfo.mockImplementation(defaultGetAccountInfo);
|
|
176
|
+
mockConnection.getTokenAccountBalance.mockResolvedValue({
|
|
177
|
+
value: { amount: '999999999', uiAmountString: '9.99' },
|
|
178
|
+
});
|
|
179
|
+
const mod = await import('./redeem');
|
|
180
|
+
redeemFn = mod.redeem;
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
afterEach(() => {
|
|
184
|
+
vi.clearAllMocks();
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
// ── Validation ──
|
|
188
|
+
|
|
189
|
+
it('should throw when wallet is not connected', async () => {
|
|
190
|
+
await expect(
|
|
191
|
+
redeemFn({ publicKey: null } as any, baseParams),
|
|
192
|
+
).rejects.toThrow('Wallet not connected');
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it('should throw when Asset Router is not configured', async () => {
|
|
196
|
+
const { getConfig } = await import('../../const/getConfig');
|
|
197
|
+
vi.mocked(getConfig).mockReturnValueOnce({ ...fullConfig, assetRouter: null });
|
|
198
|
+
|
|
199
|
+
await expect(
|
|
200
|
+
redeemFn({ publicKey: MOCK_PAYER } as any, baseParams),
|
|
201
|
+
).rejects.toThrow('Asset Router not configured');
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it('should throw when Mailbox is not configured', async () => {
|
|
205
|
+
const { getConfig } = await import('../../const/getConfig');
|
|
206
|
+
vi.mocked(getConfig).mockReturnValueOnce({ ...fullConfig, mailbox: null });
|
|
207
|
+
|
|
208
|
+
await expect(
|
|
209
|
+
redeemFn({ publicKey: MOCK_PAYER } as any, baseParams),
|
|
210
|
+
).rejects.toThrow('Mailbox not configured');
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it('should throw when Solana routing chain ID is not configured', async () => {
|
|
214
|
+
const { getConfig } = await import('../../const/getConfig');
|
|
215
|
+
vi.mocked(getConfig).mockReturnValueOnce({ ...fullConfig, solanaRoutingChainId: null });
|
|
216
|
+
|
|
217
|
+
await expect(
|
|
218
|
+
redeemFn({ publicKey: MOCK_PAYER } as any, baseParams),
|
|
219
|
+
).rejects.toThrow('Solana routing chain ID not configured');
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it('should throw when source token mint is not resolved', async () => {
|
|
223
|
+
const { getConfig } = await import('../../const/getConfig');
|
|
224
|
+
vi.mocked(getConfig).mockReturnValueOnce({ ...fullConfig, lbtcTokenMint: '' });
|
|
225
|
+
|
|
226
|
+
await expect(
|
|
227
|
+
redeemFn({ publicKey: MOCK_PAYER } as any, baseParams),
|
|
228
|
+
).rejects.toThrow('Source token mint not configured');
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it('should throw when destination token is not configured', async () => {
|
|
232
|
+
const { getConfig } = await import('../../const/getConfig');
|
|
233
|
+
vi.mocked(getConfig).mockReturnValueOnce({ ...fullConfig, btcbTokenMint: '' });
|
|
234
|
+
|
|
235
|
+
await expect(
|
|
236
|
+
redeemFn({ publicKey: MOCK_PAYER } as any, baseParams),
|
|
237
|
+
).rejects.toThrow('Destination token not configured');
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it('should throw when amount is zero', async () => {
|
|
241
|
+
await expect(
|
|
242
|
+
redeemFn({ publicKey: MOCK_PAYER } as any, { ...baseParams, amount: '0' }),
|
|
243
|
+
).rejects.toThrow('greater than zero');
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it('should throw when Asset Router is paused', async () => {
|
|
247
|
+
mockConnection.getAccountInfo.mockImplementation((pubkey: PublicKey) => {
|
|
248
|
+
if (pubkey.toBase58() === arConfigPDA.toBase58()) {
|
|
249
|
+
return Promise.resolve({ data: buildArConfigData(true) });
|
|
250
|
+
}
|
|
251
|
+
return Promise.resolve({ data: buildMailboxConfigData() });
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
await expect(
|
|
255
|
+
redeemFn({ publicKey: MOCK_PAYER } as any, baseParams),
|
|
256
|
+
).rejects.toThrow('Asset Router is paused');
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
it('should throw on insufficient balance', async () => {
|
|
260
|
+
mockConnection.getTokenAccountBalance.mockResolvedValue({
|
|
261
|
+
value: { amount: '10', uiAmountString: '0.0000001' },
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
await expect(
|
|
265
|
+
redeemFn({ publicKey: MOCK_PAYER } as any, baseParams),
|
|
266
|
+
).rejects.toThrow('Insufficient balance');
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
it('should throw when Ledger chain ID is not configured', async () => {
|
|
270
|
+
const { getConfig } = await import('../../const/getConfig');
|
|
271
|
+
vi.mocked(getConfig).mockReturnValueOnce({ ...fullConfig, ledgerChainId: null });
|
|
272
|
+
|
|
273
|
+
await expect(
|
|
274
|
+
redeemFn({ publicKey: MOCK_PAYER } as any, baseParams),
|
|
275
|
+
).rejects.toThrow('Ledger chain ID not configured');
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
// ── Success path ──
|
|
279
|
+
|
|
280
|
+
it('should return transaction signature on success', async () => {
|
|
281
|
+
mockConnection.getAccountInfo.mockImplementation(defaultGetAccountInfo);
|
|
282
|
+
|
|
283
|
+
const sig = await redeemFn({ publicKey: MOCK_PAYER } as any, baseParams);
|
|
284
|
+
|
|
285
|
+
expect(sig).toBe('mock-redeem-sig');
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
it('should pass native_mint ATA bytes as redeem recipient argument', async () => {
|
|
289
|
+
mockConnection.getAccountInfo.mockImplementation(defaultGetAccountInfo);
|
|
290
|
+
|
|
291
|
+
await redeemFn({ publicKey: MOCK_PAYER } as any, baseParams);
|
|
292
|
+
|
|
293
|
+
expect(mockMethods.redeem).toHaveBeenCalled();
|
|
294
|
+
const recipientArg = mockMethods.redeem.mock.calls[0][2] as number[];
|
|
295
|
+
expect(recipientArg).toEqual(
|
|
296
|
+
Array.from(new PublicKey(MOCK_RECIPIENT_ATA).toBytes()),
|
|
297
|
+
);
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
// ── Error wrapping ──
|
|
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 redeemFn({ publicKey: MOCK_PAYER } as any, baseParams);
|
|
308
|
+
expect.unreachable('should have thrown');
|
|
309
|
+
} catch (err) {
|
|
310
|
+
expect(err).toBeInstanceOf(SolanaSdkError);
|
|
311
|
+
expect((err as SolanaSdkError).code).toBe(ErrorCode.REDEEM_REJECTED);
|
|
312
|
+
}
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
it('should use env override when provided', async () => {
|
|
316
|
+
mockConnection.getAccountInfo.mockImplementation(defaultGetAccountInfo);
|
|
317
|
+
|
|
318
|
+
const { getConfig } = await import('../../const/getConfig');
|
|
319
|
+
|
|
320
|
+
await redeemFn({ publicKey: MOCK_PAYER } as any, {
|
|
321
|
+
...baseParams,
|
|
322
|
+
env: 'stage',
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
expect(getConfig).toHaveBeenCalledWith('stage');
|
|
326
|
+
});
|
|
327
|
+
});
|