@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
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Solana Service Implementation
3
3
  *
4
- * Provides Solana-specific operations for LBTC destination signing and unstaking.
4
+ * Provides Solana-specific operations for LBTC destination signing, redeem and deposit.
5
5
  *
6
6
  * @module services/SolanaServiceImpl
7
7
  */
@@ -9,9 +9,10 @@
9
9
  import type { Env, SolanaService } from '@lombard.finance/sdk-common';
10
10
 
11
11
  import type { ISolanaWalletProvider, SolanaNetwork } from '../types';
12
+ import { deposit } from '../web3Sdk/deposit/deposit';
13
+ import { redeem } from '../web3Sdk/redeem/redeem';
12
14
  import { redeemForBtc } from '../web3Sdk/redeemToken/redeemForBtc';
13
15
  import { signLbtcDestinationAddrSolana } from '../web3Sdk/signLbtcDestinationAddrSolana';
14
- import { unstakeLBTC } from '../web3Sdk/unstakeLBTC/unstakeLBTC';
15
16
 
16
17
  /**
17
18
  * Provider resolver function type
@@ -41,46 +42,81 @@ export class SolanaServiceImpl implements SolanaService {
41
42
  }
42
43
 
43
44
  /**
44
- * Unstake LBTC on Solana to receive BTC
45
+ * Redeem BTC.b or LBTC on Solana to receive BTC
45
46
  *
46
- * Burns LBTC on Solana and releases BTC to the provided Bitcoin address.
47
+ * Burns the source token and sends a GMP message to trigger a BTC payout.
47
48
  */
48
- async unstake(args: {
49
+ async redeemForBtc(args: {
49
50
  amount: string;
50
51
  btcAddress: string;
51
52
  network: string;
52
- }): Promise<{ txHash: string }> {
53
+ env?: Env;
54
+ tokenMint: string;
55
+ }): Promise<{ signature: string }> {
53
56
  const provider = (await this.getProvider()) as ISolanaWalletProvider;
54
57
 
55
- const txHash = await unstakeLBTC(provider, {
58
+ const signature = await redeemForBtc(provider, {
56
59
  amount: args.amount,
57
60
  btcAddress: args.btcAddress,
58
61
  network: args.network as SolanaNetwork,
62
+ env: args.env,
63
+ tokenMint: args.tokenMint,
59
64
  });
60
65
 
61
- return { txHash };
66
+ return { signature };
62
67
  }
63
68
 
64
69
  /**
65
- * Redeem BTC.b on Solana to receive BTC
66
- *
67
- * Burns BTC.b and sends a GMP message to trigger a BTC payout.
70
+ * Generic redeem via Asset Router (default: LBTC → BTC.b)
68
71
  */
69
- async redeemForBtc(args: {
72
+ async redeem(args: {
70
73
  amount: string;
71
- btcAddress: string;
74
+ recipient: string;
72
75
  network: string;
73
76
  env?: Env;
74
- }): Promise<{ txHash: string }> {
77
+ tokenMint?: string;
78
+ toLchainId?: string;
79
+ toTokenAddress?: string;
80
+ }): Promise<{ signature: string }> {
75
81
  const provider = (await this.getProvider()) as ISolanaWalletProvider;
76
82
 
77
- const txHash = await redeemForBtc(provider, {
83
+ const signature = await redeem(provider, {
78
84
  amount: args.amount,
79
- btcAddress: args.btcAddress,
85
+ recipient: args.recipient,
86
+ network: args.network as SolanaNetwork,
87
+ env: args.env,
88
+ tokenMint: args.tokenMint,
89
+ toLchainId: args.toLchainId,
90
+ toTokenAddress: args.toTokenAddress,
91
+ });
92
+
93
+ return { signature };
94
+ }
95
+
96
+ /**
97
+ * Deposit via Asset Router (default: BTC.b → LBTC)
98
+ */
99
+ async deposit(args: {
100
+ amount: string;
101
+ recipient: string;
102
+ network: string;
103
+ env?: Env;
104
+ sourceTokenMint?: string;
105
+ toLchainId?: string;
106
+ toTokenAddress?: string;
107
+ }): Promise<{ signature: string }> {
108
+ const provider = (await this.getProvider()) as ISolanaWalletProvider;
109
+
110
+ const signature = await deposit(provider, {
111
+ amount: args.amount,
112
+ recipient: args.recipient,
80
113
  network: args.network as SolanaNetwork,
81
114
  env: args.env,
115
+ sourceTokenMint: args.sourceTokenMint,
116
+ toLchainId: args.toLchainId,
117
+ toTokenAddress: args.toTokenAddress,
82
118
  });
83
119
 
84
- return { txHash };
120
+ return { signature };
85
121
  }
86
122
  }
@@ -41,6 +41,7 @@ export const OutputSelector: React.FC<OutputSelectorProps> = ({
41
41
  case 'NOTARIZATION_STATUS_SESSION_APPROVED':
42
42
  return 'Ready to mint';
43
43
  case 'NOTARIZATION_STATUS_PENDING':
44
+ case 'NOTARIZATION_STATUS_GMP_PENDING':
44
45
  return 'Pending';
45
46
  case 'NOTARIZATION_STATUS_SUBMITTED':
46
47
  return 'Submitted';
@@ -40,6 +40,8 @@ export enum ErrorCode {
40
40
  INSUFFICIENT_FUNDS = 'INSUFFICIENT_FUNDS',
41
41
  SIGNING_REJECTED = 'SIGNING_REJECTED',
42
42
  CLAIM_REJECTED = 'CLAIM_REJECTED',
43
+ DEPOSIT_REJECTED = 'DEPOSIT_REJECTED',
44
+ REDEEM_REJECTED = 'REDEEM_REJECTED',
43
45
  UNSTAKE_REJECTED = 'UNSTAKE_REJECTED',
44
46
 
45
47
  // Network errors
@@ -25,33 +25,26 @@ export function createDebugLogger(options: CreateDebugLoggerOptions = {}) {
25
25
  const { debug = false, prefix = '' } = options;
26
26
  const logs: string[] = [];
27
27
 
28
- // Return the actual logger function
29
28
  const debugLog = (...args: unknown[]): void => {
30
- // Only proceed if debugging is enabled
31
- if (!debug) {
32
- return;
33
- }
34
-
35
- // Format arguments: stringify objects, keep others as strings
36
29
  const formattedArgs = args.map(arg =>
37
30
  typeof arg === 'object' && arg !== null
38
31
  ? JSON.stringify(arg)
39
32
  : String(arg),
40
33
  );
41
34
 
42
- // Construct the final log message with optional prefix
43
35
  const logMessage = prefix
44
36
  ? `${prefix} ${formattedArgs.join(' ')}`
45
37
  : formattedArgs.join(' ');
46
38
 
47
- // Log to the console
48
- console.log(logMessage);
49
- };
39
+ logs.push(logMessage);
50
40
 
51
- const printLogs = (): void => {
52
- console.log(logs.join('\n'));
41
+ if (debug) {
42
+ console.log(logMessage);
43
+ }
53
44
  };
54
45
 
46
+ const printLogs = (): string => logs.join('\n');
47
+
55
48
  return { debugLog, printLogs };
56
49
  }
57
50
 
@@ -11,6 +11,8 @@ export enum ErrorCode {
11
11
  CONNECTION_ERROR = 'CONNECTION_ERROR',
12
12
  CONNECTION_REJECTED = 'CONNECTION_REJECTED',
13
13
  CONNECTION_TIMEOUT = 'CONNECTION_TIMEOUT',
14
+ DEPOSIT_REJECTED = 'DEPOSIT_REJECTED',
15
+ REDEEM_REJECTED = 'REDEEM_REJECTED',
14
16
  INSUFFICIENT_FUNDS = 'INSUFFICIENT_FUNDS',
15
17
  INVALID_ADDRESS = 'INVALID_ADDRESS',
16
18
  INVALID_AMOUNT = 'INVALID_AMOUNT',
@@ -51,11 +51,13 @@ export async function createOrGetAssociatedTokenAccount({
51
51
  connection,
52
52
  ownerAddress,
53
53
  mintAddress,
54
+ allowOwnerOffCurve = false,
54
55
  }: {
55
56
  provider: ISolanaWalletProvider;
56
57
  connection: Connection;
57
58
  ownerAddress: string;
58
59
  mintAddress: string;
60
+ allowOwnerOffCurve?: boolean;
59
61
  }): Promise<string> {
60
62
  const mintPubkey = new PublicKey(mintAddress);
61
63
  const ownerPubkey = new PublicKey(ownerAddress);
@@ -65,7 +67,7 @@ export async function createOrGetAssociatedTokenAccount({
65
67
  const associatedTokenAddress = await getAssociatedTokenAddress(
66
68
  mintPubkey,
67
69
  ownerPubkey,
68
- true,
70
+ allowOwnerOffCurve,
69
71
  tokenProgramId,
70
72
  ASSOCIATED_TOKEN_PROGRAM_ID,
71
73
  );
@@ -12,7 +12,7 @@ export async function sendAndConfirmTransaction({
12
12
  connection,
13
13
  provider,
14
14
  debugLabel = 'Transaction',
15
- skipPreflight = false,
15
+ skipPreflight = true,
16
16
  }: {
17
17
  instruction: Transaction | TransactionInstruction;
18
18
  connection: Connection;
@@ -3,7 +3,7 @@ import { PublicKey, SystemProgram } from '@solana/web3.js';
3
3
 
4
4
  import { sendAndConfirmTransaction } from '../../utils';
5
5
  import { createOrGetAssociatedTokenAccount } from '../../utils/tokenAccount';
6
- import { ALREADY_MINTED_TX_HASH } from '../claimLBTC';
6
+ import { ALREADY_MINTED_TX_HASH } from './constants';
7
7
  import {
8
8
  assertBtcbDepositRecipientMatchesWallet,
9
9
  ClaimContext,
@@ -80,57 +80,66 @@ export async function claimBtcbFromPayload(ctx: ClaimContext): Promise<string> {
80
80
  connection,
81
81
  ownerAddress: params.recipientAddress,
82
82
  mintAddress: mint.toBase58(),
83
+ allowOwnerOffCurve: true,
83
84
  });
84
85
 
85
- // Build mint_from_payload instruction
86
+ // Build mint_from_payload instruction.
87
+ // Optional bascule accounts must be passed explicitly (null when disabled),
88
+ // otherwise Anchor builder throws "Account <x> not provided".
86
89
  debugLog('mint_from_payload...');
87
90
  const mintPayloadArray = Array.from(payloadBytes);
88
- const mintIx = await assetRouterProgram.methods
89
- .mintFromPayload(mintPayloadArray, payloadHashArray)
90
- .accounts({
91
- payer: provider.publicKey,
92
- config: assetRouterConfigPDA,
93
- tokenProgram: tokenProgramId,
94
- recipient: recipientTokenAccount,
95
- mint,
96
- mintAuthority,
97
- tokenAuthority: tokenAuthorityPDA,
98
- consortiumValidatedPayload: validatedPayloadPDA,
99
- depositPayloadSpent: depositPayloadSpentPDA,
100
- systemProgram: SystemProgram.programId,
101
- })
102
- .instruction();
103
91
 
104
- // Append bascule remaining accounts
92
+ let basculeValidatorPDA: PublicKey | undefined;
93
+ let basculeDataPDA: PublicKey | undefined;
94
+ let basculeDepositPDA: PublicKey | undefined;
95
+
105
96
  if (effectiveBasculeProgramId) {
106
- const [basculeValidatorPDA] = PublicKey.findProgramAddressSync(
97
+ [basculeValidatorPDA] = PublicKey.findProgramAddressSync(
107
98
  [Buffer.from('bascule_validator')],
108
99
  assetRouterProgramId,
109
100
  );
110
- const [basculeDataPDA] = PublicKey.findProgramAddressSync(
101
+ [basculeDataPDA] = PublicKey.findProgramAddressSync(
111
102
  [Buffer.from('bascule')],
112
103
  effectiveBasculeProgramId,
113
104
  );
114
105
  const depositId = computeDepositIdFromPayload(payloadBytes);
115
106
  debugLog('Deposit ID:', Buffer.from(depositId).toString('hex'));
116
107
 
117
- const [basculeDepositPDA] = PublicKey.findProgramAddressSync(
108
+ [basculeDepositPDA] = PublicKey.findProgramAddressSync(
118
109
  [Buffer.from('deposit'), depositId],
119
110
  effectiveBasculeProgramId,
120
111
  );
121
112
 
113
+ debugLog('Asset router program:', assetRouterProgramId.toBase58());
114
+ debugLog('Bascule program:', effectiveBasculeProgramId.toBase58());
122
115
  debugLog('Bascule validator PDA:', basculeValidatorPDA.toBase58());
123
116
  debugLog('Bascule data PDA:', basculeDataPDA.toBase58());
124
117
  debugLog('Bascule deposit PDA:', basculeDepositPDA.toBase58());
125
-
126
- mintIx.keys.push(
127
- { pubkey: basculeValidatorPDA, isSigner: false, isWritable: false },
128
- { pubkey: effectiveBasculeProgramId, isSigner: false, isWritable: false },
129
- { pubkey: basculeDataPDA, isSigner: false, isWritable: true },
130
- { pubkey: basculeDepositPDA, isSigner: false, isWritable: true },
131
- );
132
118
  }
133
119
 
120
+ // Anchor optional accounts: when disabled, pass the program's own ID as a
121
+ // "None" sentinel — this is the convention anchor-client uses for optional accounts.
122
+ const basculeSentinel = assetRouterProgramId;
123
+ const mintIx = await assetRouterProgram.methods
124
+ .mintFromPayload(mintPayloadArray, payloadHashArray)
125
+ .accountsPartial({
126
+ payer: provider.publicKey,
127
+ config: assetRouterConfigPDA,
128
+ tokenProgram: tokenProgramId,
129
+ recipient: recipientTokenAccount,
130
+ mint,
131
+ mintAuthority,
132
+ tokenAuthority: tokenAuthorityPDA,
133
+ consortiumValidatedPayload: validatedPayloadPDA,
134
+ depositPayloadSpent: depositPayloadSpentPDA,
135
+ systemProgram: SystemProgram.programId,
136
+ basculeValidator: basculeValidatorPDA ?? basculeSentinel,
137
+ basculeProgram: effectiveBasculeProgramId ?? basculeSentinel,
138
+ basculeData: basculeDataPDA ?? basculeSentinel,
139
+ basculeDeposit: basculeDepositPDA ?? basculeSentinel,
140
+ })
141
+ .instruction();
142
+
134
143
  debugLog('Instruction account count:', mintIx.keys.length);
135
144
 
136
145
  const { signature } = await sendAndConfirmTransaction({
@@ -1,13 +1,44 @@
1
1
  import { Program } from '@coral-xyz/anchor';
2
2
  import { getMint } from '@solana/spl-token';
3
3
  import { PublicKey, SystemProgram } from '@solana/web3.js';
4
+ import { keccak256 } from 'js-sha3';
4
5
 
5
6
  import { getMailboxIdl } from '../../idl/getMailboxIdl';
6
7
  import { sendAndConfirmTransaction } from '../../utils';
7
8
  import { createOrGetAssociatedTokenAccount } from '../../utils/tokenAccount';
8
- import { ALREADY_MINTED_TX_HASH } from '../claimLBTC';
9
+ import { ALREADY_MINTED_TX_HASH } from './constants';
9
10
  import { ClaimContext, executeConsortiumSession } from './shared';
10
11
 
12
+ /**
13
+ * Mirrors bascule_gmp MintMessage.mint_id():
14
+ * keccak256( nonce_u256_be || chain_id(32) || recipient(32) || token(32) || amount_u256_be )
15
+ * Payload: nonce u256 at [36:68], token at [232:264], recipient at [264:296], amount u256 at [296:328].
16
+ * Only the low 8 bytes of each u256 are used (placed in the last 8 bytes of the 32-byte slot).
17
+ */
18
+ function computeBasculeGmpMintId(
19
+ payload: Buffer,
20
+ chainId: Buffer,
21
+ ): Uint8Array {
22
+ if (payload.length < 328) {
23
+ throw new Error(
24
+ `payload too short for bascule_gmp mint_id: ${payload.length} bytes`,
25
+ );
26
+ }
27
+ if (chainId.length !== 32) {
28
+ throw new Error(`chain_id must be 32 bytes, got ${chainId.length}`);
29
+ }
30
+
31
+ const data = Buffer.alloc(160);
32
+ payload.copy(data, 24, 60, 68);
33
+ chainId.copy(data, 32);
34
+ payload.copy(data, 64, 264, 296);
35
+ payload.copy(data, 96, 232, 264);
36
+ payload.copy(data, 152, 320, 328);
37
+
38
+ const hash = keccak256(new Uint8Array(data));
39
+ return new Uint8Array(Buffer.from(hash, 'hex'));
40
+ }
41
+
11
42
  /**
12
43
  * LBTC GMP flow via Consortium + Mailbox + Asset Router gmp_receive.
13
44
  *
@@ -25,9 +56,9 @@ export async function claimLbtcGmp(ctx: ClaimContext): Promise<string> {
25
56
  validatedPayloadPDA, arConfig, debugLog,
26
57
  } = ctx;
27
58
 
28
- if (payloadBytes.length < 296) {
59
+ if (payloadBytes.length < 328) {
29
60
  throw new Error(
30
- `LBTC GMP payload too short: expected >= 296 bytes, got ${payloadBytes.length}`,
61
+ `LBTC GMP payload too short: expected >= 328 bytes, got ${payloadBytes.length}`,
31
62
  );
32
63
  }
33
64
 
@@ -74,7 +105,7 @@ export async function claimLbtcGmp(ctx: ClaimContext): Promise<string> {
74
105
  connection,
75
106
  provider,
76
107
  debugLabel: 'Consortium post_session_payload',
77
- skipPreflight: params.skipPreflight ?? false,
108
+ skipPreflight: params.skipPreflight ?? true,
78
109
  });
79
110
  debugLog('post_session_payload completed');
80
111
  }
@@ -133,7 +164,7 @@ export async function claimLbtcGmp(ctx: ClaimContext): Promise<string> {
133
164
  connection,
134
165
  provider,
135
166
  debugLabel: 'Mailbox deliver_message',
136
- skipPreflight: params.skipPreflight ?? false,
167
+ skipPreflight: params.skipPreflight ?? true,
137
168
  });
138
169
  debugLog('deliver_message completed');
139
170
  }
@@ -169,6 +200,7 @@ export async function claimLbtcGmp(ctx: ClaimContext): Promise<string> {
169
200
  connection,
170
201
  ownerAddress: tokenRecipient.toBase58(),
171
202
  mintAddress: mint.toBase58(),
203
+ allowOwnerOffCurve: true,
172
204
  });
173
205
 
174
206
  // Build handle_message instruction
@@ -210,16 +242,31 @@ export async function claimLbtcGmp(ctx: ClaimContext): Promise<string> {
210
242
  effectiveBasculeGmpProgramId,
211
243
  );
212
244
  const [basculeGmpAccountRolesPDA] = PublicKey.findProgramAddressSync(
213
- [Buffer.from('account_roles'), basculeGmpConfigPDA.toBytes()],
245
+ [Buffer.from('account_roles'), basculeValidatorPDA.toBytes()],
214
246
  effectiveBasculeGmpProgramId,
215
247
  );
248
+
249
+ if (!config.solanaRoutingChainId) {
250
+ throw new Error(
251
+ `Solana routing chain ID not configured for network: ${params.network}`,
252
+ );
253
+ }
254
+ const solanaChainId = Buffer.from(config.solanaRoutingChainId, 'hex');
255
+ const mintId = computeBasculeGmpMintId(payloadBytes, solanaChainId);
216
256
  const [basculeGmpMintPayloadPDA] = PublicKey.findProgramAddressSync(
217
- [Buffer.from('mint_payload'), payloadHash],
257
+ [Buffer.from('mint_payload'), Buffer.from(mintId)],
218
258
  effectiveBasculeGmpProgramId,
219
259
  );
220
260
 
261
+ debugLog('Asset router program:', assetRouterProgramId.toBase58());
262
+ debugLog('Bascule GMP program:', effectiveBasculeGmpProgramId.toBase58());
263
+ debugLog('Bascule validator PDA:', basculeValidatorPDA.toBase58());
264
+ debugLog('Bascule GMP config PDA:', basculeGmpConfigPDA.toBase58());
265
+ debugLog('Bascule GMP account roles PDA:', basculeGmpAccountRolesPDA.toBase58());
266
+ debugLog('Bascule GMP mint payload PDA:', basculeGmpMintPayloadPDA.toBase58());
267
+
221
268
  handleIx.keys.push(
222
- { pubkey: basculeValidatorPDA, isSigner: false, isWritable: false },
269
+ { pubkey: basculeValidatorPDA, isSigner: false, isWritable: true },
223
270
  { pubkey: effectiveBasculeGmpProgramId, isSigner: false, isWritable: false },
224
271
  { pubkey: basculeGmpConfigPDA, isSigner: false, isWritable: false },
225
272
  { pubkey: basculeGmpAccountRolesPDA, isSigner: false, isWritable: false },
@@ -235,6 +282,17 @@ export async function claimLbtcGmp(ctx: ClaimContext): Promise<string> {
235
282
  }
236
283
 
237
284
  debugLog('handle_message account count:', handleIx.keys.length);
285
+ debugLog(
286
+ 'handle_message program:', handleIx.programId.toBase58(),
287
+ );
288
+ debugLog(
289
+ 'handle_message data (hex):', Buffer.from(handleIx.data).toString('hex'),
290
+ );
291
+ handleIx.keys.forEach((k, i) => {
292
+ debugLog(
293
+ ` [${i}] ${k.pubkey.toBase58()} signer=${k.isSigner} writable=${k.isWritable}`,
294
+ );
295
+ });
238
296
 
239
297
  const { signature } = await sendAndConfirmTransaction({
240
298
  instruction: handleIx,
@@ -206,8 +206,8 @@ const meta: Meta<typeof StoryView> = {
206
206
  3. Wait for backend to notarize the deposit (Consortium validation)
207
207
  4. Call \`claimToken\` to mint tokens via Asset Router's \`mint_from_payload\`
208
208
 
209
- Unlike the legacy \`claimLBTC\` (which uses a 3-step on-chain process), this function
210
- performs a single transaction — the Consortium validation is handled entirely by the backend.`,
209
+ The Consortium validation is handled entirely by the backend, so the on-chain flow
210
+ is a single transaction.`,
211
211
  },
212
212
  },
213
213
  },
@@ -1,7 +1,7 @@
1
1
  import { AnchorProvider, Program, setProvider } from '@coral-xyz/anchor';
2
2
  import { PublicKey } from '@solana/web3.js';
3
3
 
4
- import { getConfig, networkToEnv } from '../../const/getConfig';
4
+ import { DEFAULT_ENV, getConfig } from '../../const/getConfig';
5
5
  import { getConnection } from '../../const/rpcUrls';
6
6
  import { getAssetRouterIdl } from '../../idl/getAssetRouterIdl';
7
7
  import { getConsortiumIdl } from '../../idl/getConsortiumIdl';
@@ -14,11 +14,11 @@ import {
14
14
  ClaimTokenParams,
15
15
  computePayloadHash,
16
16
  DEPOSIT_SELECTOR_V1,
17
+ fetchAssetRouterConfig,
17
18
  fetchCurrentEpoch,
18
19
  getConsortiumConfigPDA,
19
20
  getConsortiumSessionPDA,
20
21
  GMP_MESSAGE_V1_SELECTOR,
21
- parseAssetRouterConfig,
22
22
  } from './shared';
23
23
 
24
24
  export type { ClaimTokenParams } from './shared';
@@ -34,7 +34,7 @@ export async function claimToken(
34
34
  provider: ISolanaWalletProvider,
35
35
  params: ClaimTokenParams,
36
36
  ): Promise<string> {
37
- const { network, env: envOverride, rawPayload, rpcUrl, debug = false } = params;
37
+ const { network, env = DEFAULT_ENV, rawPayload, rpcUrl, debug = false } = params;
38
38
  const { debugLog, printLogs } = createDebugLogger({ debug });
39
39
 
40
40
  try {
@@ -42,8 +42,8 @@ export async function claimToken(
42
42
  throw new Error('Wallet not found');
43
43
  }
44
44
 
45
- const env = envOverride ?? networkToEnv[network];
46
45
  const config = getConfig(env);
46
+
47
47
  if (!config.assetRouter) {
48
48
  throw new Error(`Asset Router not configured for network: ${network}`);
49
49
  }
@@ -72,7 +72,10 @@ export async function claimToken(
72
72
  const consortiumProgramId = new PublicKey(config.consortium);
73
73
 
74
74
  // Parse payload
75
- const payloadBytes = Buffer.from(rawPayload, 'hex');
75
+ const cleanPayload = rawPayload.startsWith('0x')
76
+ ? rawPayload.slice(2)
77
+ : rawPayload;
78
+ const payloadBytes = Buffer.from(cleanPayload, 'hex');
76
79
  if (payloadBytes.length < 4) {
77
80
  throw new Error(`Payload too short: ${payloadBytes.length} bytes`);
78
81
  }
@@ -92,7 +95,7 @@ export async function claimToken(
92
95
 
93
96
  // Fetch current epoch from on-chain consortium config
94
97
  const currentEpoch = await fetchCurrentEpoch(
95
- connection,
98
+ consortiumProgram,
96
99
  consortiumConfigPDA,
97
100
  );
98
101
  debugLog('Current consortium epoch:', currentEpoch.toString());
@@ -116,18 +119,19 @@ export async function claimToken(
116
119
  assetRouterProgramId,
117
120
  );
118
121
 
119
- // Read on-chain Asset Router config
122
+ // Read on-chain Asset Router config via Anchor (IDL-based deserialization)
120
123
  debugLog('Asset Router program ID:', assetRouterProgramId.toBase58());
121
124
  debugLog('Asset Router config PDA:', assetRouterConfigPDA.toBase58());
122
- const configAccountInfo =
123
- await connection.getAccountInfo(assetRouterConfigPDA);
124
- debugLog('Config account exists:', !!configAccountInfo);
125
- if (!configAccountInfo) {
126
- throw new Error(
127
- `Asset Router config account not found at ${assetRouterConfigPDA.toBase58()} (program: ${assetRouterProgramId.toBase58()})`,
128
- );
129
- }
130
- const arConfig = parseAssetRouterConfig(configAccountInfo.data);
125
+ const arConfig = await fetchAssetRouterConfig(
126
+ assetRouterProgram,
127
+ assetRouterConfigPDA,
128
+ );
129
+ debugLog(
130
+ 'Asset Router config paused:', arConfig.paused,
131
+ 'nativeMint:', arConfig.nativeMint.toBase58(),
132
+ 'bascule:', arConfig.basculeProgramId?.toBase58() ?? 'null',
133
+ 'basculeGmp:', arConfig.basculeGmpProgramId?.toBase58() ?? 'null',
134
+ );
131
135
 
132
136
  if (arConfig.paused) {
133
137
  throw new Error('Asset Router contract is paused');
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Sentinel tx hash returned when a payload has already been processed on-chain.
3
+ * Used by BTC.b mint and LBTC GMP flows to signal an idempotent no-op to callers.
4
+ */
5
+ export const ALREADY_MINTED_TX_HASH = 'ALREADY_MINTED';
@@ -1 +1,2 @@
1
1
  export * from './claimToken';
2
+ export { ALREADY_MINTED_TX_HASH } from './constants';