@lombard.finance/sdk-sui 1.1.2 → 1.2.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lombard.finance/sdk-sui",
3
- "version": "1.1.2",
3
+ "version": "1.2.0",
4
4
  "description": "Sui integration for the Lombard SDK - stake BTC and manage LBTC on Sui",
5
5
  "exports": {
6
6
  ".": {
@@ -61,6 +61,7 @@
61
61
  "@lombard.finance/sdk-common": "4.0.0",
62
62
  "@mysten/sui": "^1.21.1",
63
63
  "@mysten/wallet-standard": "^0.13.7",
64
+ "@noble/hashes": "^1.8.0",
64
65
  "@wallet-standard/base": "^1.1.0",
65
66
  "@wallet-standard/core": "^1.1.0",
66
67
  "vite": "^6.3.5"
@@ -9,8 +9,9 @@ type EnvParameter = {
9
9
  env?: Env;
10
10
  };
11
11
 
12
- export interface CommonWriteParameters<WF extends Wallet['features']>
13
- extends EnvParameter {
12
+ export interface CommonWriteParameters<
13
+ WF extends Wallet['features'],
14
+ > extends EnvParameter {
14
15
  walletClient: WalletWithFeatures<WF>;
15
16
  account: WalletAccount;
16
17
  }
@@ -19,13 +19,13 @@ const stageConfig: IConfig = {
19
19
  mint: {
20
20
  target:
21
21
  // lbtc_v2_current
22
- '0x2721ad6e939baca77b36f415ab91edb1c91b256cbc8614f8f6c84bf06faf74af::treasury::mint_v2',
22
+ '0x1269ce3b46b5647d0e46024185dd24bdb7492ec7840ebc40fdd209150b89a33d::treasury::mint_v2',
23
23
  denyList: '0x403',
24
24
  },
25
25
  redeem: {
26
26
  // lbtc_v2_current
27
27
  target:
28
- '0x2721ad6e939baca77b36f415ab91edb1c91b256cbc8614f8f6c84bf06faf74af::treasury::redeem',
28
+ '0x1269ce3b46b5647d0e46024185dd24bdb7492ec7840ebc40fdd209150b89a33d::treasury::redeem',
29
29
  },
30
30
  // lbtc_v1_deprecated - This is intentional
31
31
  LBTC: '0x2d66430a27565b912f21be970e5ae1e8c0359f0b518c3235b751c75976791ce0::lbtc::LBTC',
@@ -67,15 +67,15 @@ const testnetConfig: IConfig = {
67
67
  // https://github.com/lombard-finance/sui-contracts/blob/main/mainnet.json
68
68
  const prodConfig: IConfig = {
69
69
  mint: {
70
- // lbtc_v2_current
70
+ // lbtc_v5_current
71
71
  target:
72
- '0x818430a456ff977f7320f78650d19801f90758d200a01dd3c2c679472c521357::treasury::mint_v2',
72
+ '0x124477c9cffb667be781e867fd29579d3498a47e6b69d06155841ca85bcb02d0::treasury::mint_v2',
73
73
  denyList: '0x403',
74
74
  },
75
75
  redeem: {
76
- // lbtc_v2_current
76
+ // lbtc_v5_current
77
77
  target:
78
- '0x818430a456ff977f7320f78650d19801f90758d200a01dd3c2c679472c521357::treasury::redeem',
78
+ '0x124477c9cffb667be781e867fd29579d3498a47e6b69d06155841ca85bcb02d0::treasury::redeem',
79
79
  },
80
80
  // lbtc_v1_deprecated - This is intentional
81
81
  LBTC: '0x3e8e9423d80e1774a7ca128fccd8bf5f1f7753be658c5e645929037f7c819040::lbtc::LBTC',
package/src/index.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  export * from './const';
2
2
  export * from './web3Sdk/claimLBTC';
3
3
  export * from './web3Sdk/getBalance';
4
+ export * from './web3Sdk/getBasculeDepositStatus';
4
5
  export * from './web3Sdk/signLbtcDestionationAddrSui';
5
6
  export * from './web3Sdk/signTermsOfService';
6
7
  export * from './web3Sdk/unstakeLBTC';
@@ -7,7 +7,7 @@
7
7
  */
8
8
 
9
9
  import type { Env, SuiService } from '@lombard.finance/sdk-common';
10
- import { getFullnodeUrl,SuiClient } from '@mysten/sui/client';
10
+ import { getFullnodeUrl, SuiClient } from '@mysten/sui/client';
11
11
  import type {
12
12
  SuiChain,
13
13
  SuiSignPersonalMessageFeature,
@@ -28,7 +28,8 @@ type ProviderResolver = () => Promise<unknown>;
28
28
  /**
29
29
  * Sui wallet features required for signing and transactions
30
30
  */
31
- type SuiWalletFeatures = SuiSignPersonalMessageFeature & SuiSignTransactionFeature;
31
+ type SuiWalletFeatures = SuiSignPersonalMessageFeature &
32
+ SuiSignTransactionFeature;
32
33
 
33
34
  /**
34
35
  * Sui wallet provider interface
@@ -41,7 +42,9 @@ interface SuiWalletProvider {
41
42
  /**
42
43
  * Get Sui network from chain ID
43
44
  */
44
- function getSuiNetworkFromChainId(chainId: string): 'mainnet' | 'testnet' | 'devnet' {
45
+ function getSuiNetworkFromChainId(
46
+ chainId: string,
47
+ ): 'mainnet' | 'testnet' | 'devnet' {
45
48
  if (chainId.includes('mainnet')) return 'mainnet';
46
49
  if (chainId.includes('testnet')) return 'testnet';
47
50
  return 'devnet';
@@ -7,6 +7,10 @@ import { WalletWithFeatures } from '@wallet-standard/base';
7
7
  import type { WalletAccount } from '@wallet-standard/core';
8
8
 
9
9
  import { getConfig } from '../../const';
10
+ import {
11
+ getBasculeDepositStatus,
12
+ SuiBasculeDepositStatus,
13
+ } from '../getBasculeDepositStatus';
10
14
 
11
15
  type Not0xPrefixedHex = string;
12
16
 
@@ -34,6 +38,31 @@ export async function claimLBTC({
34
38
  client,
35
39
  env = DEFAULT_ENV,
36
40
  }: IClaimLBTCParams): Promise<SuiTransactionBlockResponse> {
41
+ // Pre-flight the Bascule status so we surface a clear error instead of an
42
+ // opaque on-chain abort from bascule::validate_withdrawal (mirrors the EVM
43
+ // SDK's claimLBTC). The mint targets the same Bascule object this checks.
44
+ const basculeStatus = await getBasculeDepositStatus({ client, payload, env });
45
+ if (basculeStatus !== SuiBasculeDepositStatus.REPORTED) {
46
+ switch (basculeStatus) {
47
+ case SuiBasculeDepositStatus.UNREPORTED:
48
+ throw new Error(
49
+ 'The deposit cannot be claimed because it is unreported or potentially still pending, please try again later.',
50
+ );
51
+ case SuiBasculeDepositStatus.WITHDRAWN:
52
+ throw new Error(
53
+ 'The deposit cannot be claimed because it is withdrawn already.',
54
+ );
55
+ case SuiBasculeDepositStatus.PAUSED:
56
+ throw new Error(
57
+ 'The deposit cannot be claimed because the bridge is paused, please try again later.',
58
+ );
59
+ default:
60
+ throw new Error(
61
+ 'The deposit cannot be claimed because it is blocked by bridge security.',
62
+ );
63
+ }
64
+ }
65
+
37
66
  const transaction = new Transaction();
38
67
 
39
68
  const {
@@ -0,0 +1,111 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+
3
+ import {
4
+ deriveDepositId,
5
+ getBasculeDepositStatus,
6
+ SuiBasculeDepositStatus,
7
+ } from '../getBasculeDepositStatus';
8
+
9
+ // A synthetic mint payload: 4-byte selector + 5 big-endian 32-byte words
10
+ // (to_chain, recipient, amount=100000, tx_id, vout=3). The expected deposit id
11
+ // is computed independently (pure-python keccak mirroring Move's to_deposit_id
12
+ // and sui-claimer's payload.go) and pinned here as a regression vector.
13
+ const PAYLOAD =
14
+ 'ce25e7c2' +
15
+ '0000000000000000000000000000000000000000000000000000000000000001' + // to_chain
16
+ 'a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1' + // recipient
17
+ '00000000000000000000000000000000000000000000000000000000000186a0' + // amount = 100000
18
+ 'b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2' + // tx_id
19
+ '0000000000000000000000000000000000000000000000000000000000000003'; // vout = 3
20
+
21
+ const EXPECTED_DEPOSIT_ID =
22
+ '103946673116742332945389452905418796987698243721899433814538596425314178876115';
23
+
24
+ describe('deriveDepositId', () => {
25
+ it('matches the on-chain to_deposit_id derivation', () => {
26
+ expect(deriveDepositId(PAYLOAD)).toBe(EXPECTED_DEPOSIT_ID);
27
+ });
28
+
29
+ it('accepts a 0x-prefixed payload', () => {
30
+ expect(deriveDepositId(`0x${PAYLOAD}`)).toBe(EXPECTED_DEPOSIT_ID);
31
+ });
32
+
33
+ it('rejects a payload of the wrong length', () => {
34
+ expect(() => deriveDepositId('ce25e7c2')).toThrow(/length/);
35
+ });
36
+
37
+ it('rejects a payload with non-hex characters', () => {
38
+ // Correct length, but the final byte 'ag' has a non-hex low nibble.
39
+ // Number.parseInt used to accept this as 0x0a; strict validation rejects it.
40
+ expect(() => deriveDepositId(`${PAYLOAD.slice(0, -2)}ag`)).toThrow(
41
+ /non-hex/,
42
+ );
43
+ });
44
+ });
45
+
46
+ describe('getBasculeDepositStatus', () => {
47
+ const makeClient = (overrides: {
48
+ paused?: boolean;
49
+ tableId?: string;
50
+ variant?: string | null;
51
+ }) =>
52
+ ({
53
+ getObject: vi.fn().mockResolvedValue({
54
+ data: {
55
+ content: {
56
+ dataType: 'moveObject',
57
+ fields: {
58
+ mIsPaused: overrides.paused ?? false,
59
+ mDepositHistory: {
60
+ fields: { id: { id: overrides.tableId ?? '0xtable' } },
61
+ },
62
+ },
63
+ },
64
+ },
65
+ }),
66
+ getDynamicFieldObject: vi.fn().mockResolvedValue(
67
+ overrides.variant
68
+ ? {
69
+ data: {
70
+ content: {
71
+ dataType: 'moveObject',
72
+ fields: { value: { variant: overrides.variant } },
73
+ },
74
+ },
75
+ }
76
+ : { data: null, error: { code: 'dynamicFieldNotFound' } },
77
+ ),
78
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
79
+ }) as any;
80
+
81
+ it('returns REPORTED for a reported deposit', async () => {
82
+ const status = await getBasculeDepositStatus({
83
+ client: makeClient({ variant: 'Reported' }),
84
+ payload: PAYLOAD,
85
+ });
86
+ expect(status).toBe(SuiBasculeDepositStatus.REPORTED);
87
+ });
88
+
89
+ it('returns WITHDRAWN for an already-withdrawn deposit', async () => {
90
+ const status = await getBasculeDepositStatus({
91
+ client: makeClient({ variant: 'Withdrawn' }),
92
+ payload: PAYLOAD,
93
+ });
94
+ expect(status).toBe(SuiBasculeDepositStatus.WITHDRAWN);
95
+ });
96
+
97
+ it('returns UNREPORTED when the deposit is not in the table', async () => {
98
+ const status = await getBasculeDepositStatus({
99
+ client: makeClient({ variant: null }),
100
+ payload: PAYLOAD,
101
+ });
102
+ expect(status).toBe(SuiBasculeDepositStatus.UNREPORTED);
103
+ });
104
+
105
+ it('returns PAUSED without looking up the deposit when paused', async () => {
106
+ const client = makeClient({ paused: true, variant: 'Reported' });
107
+ const status = await getBasculeDepositStatus({ client, payload: PAYLOAD });
108
+ expect(status).toBe(SuiBasculeDepositStatus.PAUSED);
109
+ expect(client.getDynamicFieldObject).not.toHaveBeenCalled();
110
+ });
111
+ });
@@ -0,0 +1,300 @@
1
+ import { DEFAULT_ENV, Env } from '@lombard.finance/sdk-common';
2
+ import type { SuiClient } from '@mysten/sui/client';
3
+ import { keccak_256 } from '@noble/hashes/sha3';
4
+
5
+ import { getConfig } from '../../const';
6
+
7
+ type Not0xPrefixedHex = string;
8
+
9
+ /**
10
+ * The Bascule deposit status on Sui, mirroring the EVM SDK's
11
+ * `BasculeDepositStatus`. `PAUSED` is Sui-specific: the on-chain
12
+ * `validate_withdrawal` aborts for every deposit while the Bascule is paused,
13
+ * so there is no point inspecting an individual deposit.
14
+ */
15
+ export enum SuiBasculeDepositStatus {
16
+ /** Deposit is not reported yet (or unknown) — minting would abort. */
17
+ UNREPORTED = 'UNREPORTED',
18
+ /** Deposit is reported and may be minted. */
19
+ REPORTED = 'REPORTED',
20
+ /** Deposit has already been withdrawn (already minted). */
21
+ WITHDRAWN = 'WITHDRAWN',
22
+ /** The Bascule is paused — no deposit can be minted right now. */
23
+ PAUSED = 'PAUSED',
24
+ }
25
+
26
+ // On-chain DepositState enum variant names (see bascule::bascule::DepositState).
27
+ const DEPOSIT_REPORTED = 'Reported';
28
+ const DEPOSIT_WITHDRAWN = 'Withdrawn';
29
+
30
+ // Mint payload layout (matches consortium::payload_decoder::decode_mint_payload):
31
+ // a 4-byte action selector followed by 5 big-endian 32-byte words:
32
+ // to_chain || recipient || amount || tx_id || vout.
33
+ const SELECTOR_LEN = 4;
34
+ const WORD_LEN = 32;
35
+ const WORDS = 5;
36
+ const PAYLOAD_LEN = SELECTOR_LEN + WORDS * WORD_LEN;
37
+
38
+ export interface IGetSuiBasculeDepositStatusParameters {
39
+ /** Sui RPC client. */
40
+ client: SuiClient;
41
+ /**
42
+ * The raw mint payload (hex, with or without `0x` prefix), see
43
+ * `Deposit.rawPayload`.
44
+ */
45
+ payload: Not0xPrefixedHex;
46
+ /** The optional environment identifier (defaults to the SDK default). */
47
+ env?: Env;
48
+ }
49
+
50
+ /**
51
+ * Returns the Bascule deposit status for a Sui mint payload, so a claim can be
52
+ * pre-flighted before submitting the transaction (the on-chain
53
+ * `bascule::validate_withdrawal` would otherwise abort the mint).
54
+ *
55
+ * The deposit id is derived exactly as `bascule::bascule::to_deposit_id` does
56
+ * on-chain (and as `sui-claimer` derives it server-side):
57
+ *
58
+ * keccak256( zero32 || 0x03,0x53,0x55,0x49 || to || u64_le(amount) || tx_id || u32_le(index) )
59
+ *
60
+ * read back as a little-endian u256.
61
+ */
62
+ export async function getBasculeDepositStatus({
63
+ client,
64
+ payload,
65
+ env = DEFAULT_ENV,
66
+ }: IGetSuiBasculeDepositStatusParameters): Promise<SuiBasculeDepositStatus> {
67
+ const { bascule } = getConfig(env);
68
+
69
+ // No Bascule configured for this env — treat as reported (parity with the EVM
70
+ // SDK, which returns REPORTED when the bascule address is the zero address).
71
+ if (!bascule) {
72
+ return SuiBasculeDepositStatus.REPORTED;
73
+ }
74
+
75
+ const depositId = deriveDepositId(payload);
76
+ const { paused, depositTableId } = await getBasculeState(client, bascule);
77
+
78
+ // While paused, validate_withdrawal aborts for every deposit, so there is no
79
+ // point looking up the individual deposit status.
80
+ if (paused) {
81
+ return SuiBasculeDepositStatus.PAUSED;
82
+ }
83
+
84
+ const status = await getDepositStatus(client, depositTableId, depositId);
85
+ switch (status) {
86
+ case DEPOSIT_REPORTED:
87
+ return SuiBasculeDepositStatus.REPORTED;
88
+ case DEPOSIT_WITHDRAWN:
89
+ return SuiBasculeDepositStatus.WITHDRAWN;
90
+ default:
91
+ // Not found, or an unrecognized status — not mintable.
92
+ return SuiBasculeDepositStatus.UNREPORTED;
93
+ }
94
+ }
95
+
96
+ interface BasculeState {
97
+ depositTableId: string;
98
+ paused: boolean;
99
+ }
100
+
101
+ /**
102
+ * Reads the shared Bascule object and extracts the deposit-history table id and
103
+ * the pause flag.
104
+ */
105
+ async function getBasculeState(
106
+ client: SuiClient,
107
+ basculeAddress: string,
108
+ ): Promise<BasculeState> {
109
+ const resp = await client.getObject({
110
+ id: basculeAddress,
111
+ options: { showContent: true },
112
+ });
113
+
114
+ const content = resp.data?.content;
115
+ if (!content || content.dataType !== 'moveObject') {
116
+ throw new Error(`Bascule object ${basculeAddress} has no content`);
117
+ }
118
+
119
+ const fields = content.fields as Record<string, unknown>;
120
+
121
+ const paused = fields.mIsPaused;
122
+ if (typeof paused !== 'boolean') {
123
+ throw new Error(
124
+ `Bascule object ${basculeAddress} has invalid mIsPaused field`,
125
+ );
126
+ }
127
+
128
+ const depositTableId = nestedString(fields, [
129
+ 'mDepositHistory',
130
+ 'fields',
131
+ 'id',
132
+ 'id',
133
+ ]);
134
+
135
+ return { depositTableId, paused };
136
+ }
137
+
138
+ /**
139
+ * Looks up a deposit id in the Bascule deposit-history table. Returns the
140
+ * DepositState variant name (`Reported`/`Withdrawn`) or `undefined` if there is
141
+ * no entry. A missing dynamic field is surfaced by the RPC as an error / empty
142
+ * data rather than a thrown transport error.
143
+ */
144
+ async function getDepositStatus(
145
+ client: SuiClient,
146
+ tableId: string,
147
+ depositId: string,
148
+ ): Promise<string | undefined> {
149
+ const resp = await client.getDynamicFieldObject({
150
+ parentId: tableId,
151
+ name: { type: 'u256', value: depositId },
152
+ });
153
+
154
+ const content = resp.data?.content;
155
+ if (!content || content.dataType !== 'moveObject') {
156
+ return undefined;
157
+ }
158
+
159
+ const fields = content.fields as Record<string, unknown>;
160
+ const value = fields.value;
161
+ if (value && typeof value === 'object' && 'variant' in value) {
162
+ const variant = (value as { variant: unknown }).variant;
163
+ if (typeof variant === 'string') {
164
+ return variant;
165
+ }
166
+ }
167
+ return undefined;
168
+ }
169
+
170
+ /**
171
+ * Derives the Bascule deposit id (a u256, returned as a decimal string) exactly
172
+ * as `bascule::bascule::to_deposit_id` does on-chain.
173
+ */
174
+ export function deriveDepositId(payloadHex: Not0xPrefixedHex): string {
175
+ const raw = hexToBytes(payloadHex);
176
+ if (raw.length !== PAYLOAD_LEN) {
177
+ throw new Error(
178
+ `Invalid mint payload length ${raw.length}, want ${PAYLOAD_LEN}`,
179
+ );
180
+ }
181
+
182
+ const body = raw.subarray(SELECTOR_LEN);
183
+ const word = (i: number) => body.subarray(i * WORD_LEN, (i + 1) * WORD_LEN);
184
+ // word(0) = to_chain (unused for the deposit id)
185
+
186
+ const to = word(1); // recipient, 32-byte big-endian address bytes
187
+ const amount = bigEndianWordToUint(word(2), 8, 'amount'); // u64
188
+ // tx_id = bcs::to_bytes(&txid_u256): the payload word is big-endian, BCS of a
189
+ // u256 is little-endian, so bascule sees the word reversed.
190
+ const txId = reversed(word(3));
191
+ const index = bigEndianWordToUint(word(4), 4, 'vout'); // u32
192
+
193
+ const preimage = concatBytes(
194
+ new Uint8Array(32), // CODESYNC(non-evm-prefix): 32 zero bytes
195
+ Uint8Array.from([0x03, 0x53, 0x55, 0x49]), // CODESYNC(sui-unique-id)
196
+ to,
197
+ uintToLeBytes(amount, 8),
198
+ txId,
199
+ uintToLeBytes(index, 4),
200
+ );
201
+
202
+ const digest = keccak_256(preimage);
203
+
204
+ // peel_u256 reads the digest as a little-endian u256.
205
+ let value = 0n;
206
+ for (let i = digest.length - 1; i >= 0; i--) {
207
+ value = (value << 8n) | BigInt(digest[i]);
208
+ }
209
+ return value.toString();
210
+ }
211
+
212
+ // --- byte helpers -----------------------------------------------------------
213
+
214
+ function hexToBytes(hex: string): Uint8Array {
215
+ const clean = hex.startsWith('0x') ? hex.slice(2) : hex;
216
+ if (clean.length % 2 !== 0) {
217
+ throw new Error('Invalid payload hex: odd length');
218
+ }
219
+ // Reject any non-hex character outright, matching Go's strict
220
+ // hex.DecodeString. A per-byte Number.parseInt would silently accept a
221
+ // malformed low nibble (e.g. 'ag' -> 0x0a), yielding a wrong deposit id.
222
+ if (!/^[0-9a-fA-F]*$/.test(clean)) {
223
+ throw new Error('Invalid payload hex: non-hex characters');
224
+ }
225
+ const out = new Uint8Array(clean.length / 2);
226
+ for (let i = 0; i < out.length; i++) {
227
+ out[i] = Number.parseInt(clean.slice(i * 2, i * 2 + 2), 16);
228
+ }
229
+ return out;
230
+ }
231
+
232
+ /**
233
+ * Mirrors `u256.try_as_u64()/try_as_u32()`: the value must fit in `byteWidth`
234
+ * bytes, i.e. the upper bytes of the big-endian word must be zero.
235
+ */
236
+ function bigEndianWordToUint(
237
+ word: Uint8Array,
238
+ byteWidth: number,
239
+ label: string,
240
+ ): bigint {
241
+ for (let i = 0; i < word.length - byteWidth; i++) {
242
+ if (word[i] !== 0) {
243
+ throw new Error(`${label} does not fit in u${byteWidth * 8}`);
244
+ }
245
+ }
246
+ let value = 0n;
247
+ for (let i = word.length - byteWidth; i < word.length; i++) {
248
+ value = (value << 8n) | BigInt(word[i]);
249
+ }
250
+ return value;
251
+ }
252
+
253
+ function uintToLeBytes(value: bigint, byteWidth: number): Uint8Array {
254
+ const out = new Uint8Array(byteWidth);
255
+ let v = value;
256
+ for (let i = 0; i < byteWidth; i++) {
257
+ out[i] = Number(v & 0xffn);
258
+ v >>= 8n;
259
+ }
260
+ return out;
261
+ }
262
+
263
+ function reversed(bytes: Uint8Array): Uint8Array {
264
+ const out = new Uint8Array(bytes.length);
265
+ for (let i = 0; i < bytes.length; i++) {
266
+ out[i] = bytes[bytes.length - 1 - i];
267
+ }
268
+ return out;
269
+ }
270
+
271
+ function concatBytes(...parts: Uint8Array[]): Uint8Array {
272
+ const total = parts.reduce((n, p) => n + p.length, 0);
273
+ const out = new Uint8Array(total);
274
+ let off = 0;
275
+ for (const p of parts) {
276
+ out.set(p, off);
277
+ off += p.length;
278
+ }
279
+ return out;
280
+ }
281
+
282
+ function nestedString(
283
+ fields: Record<string, unknown>,
284
+ keys: string[],
285
+ ): string {
286
+ let cur: unknown = fields;
287
+ for (let i = 0; i < keys.length; i++) {
288
+ if (cur === null || typeof cur !== 'object') {
289
+ throw new Error(`key "${keys[i - 1]}" is not an object`);
290
+ }
291
+ cur = (cur as Record<string, unknown>)[keys[i]];
292
+ if (cur === undefined) {
293
+ throw new Error(`missing key "${keys[i]}"`);
294
+ }
295
+ }
296
+ if (typeof cur !== 'string') {
297
+ throw new Error(`key "${keys[keys.length - 1]}" is not a string`);
298
+ }
299
+ return cur;
300
+ }
@@ -0,0 +1 @@
1
+ export * from './getBasculeDepositStatus';
@@ -34,9 +34,7 @@ export async function prepareCoinsTransaction({
34
34
  const decimals = coinMetadata?.decimals ?? LBTC_DECIMALS;
35
35
 
36
36
  const unstakeAmount = BigInt(
37
- amount
38
- .multipliedBy(new BigNumber(10).pow(decimals))
39
- .toString(10),
37
+ amount.multipliedBy(new BigNumber(10).pow(decimals)).toString(10),
40
38
  );
41
39
 
42
40
  const selectedCoins = [] as CoinStruct[];
@@ -49,7 +47,7 @@ export async function prepareCoinsTransaction({
49
47
  } else if (selectedAmount + BigInt(coin.balance) === unstakeAmount) {
50
48
  selectedCoins.push(coin);
51
49
 
52
- const coinObjects = selectedCoins.map(coin =>
50
+ const coinObjects = selectedCoins.map((coin) =>
53
51
  transaction.object(coin.coinObjectId),
54
52
  );
55
53
 
@@ -73,7 +71,7 @@ export async function prepareCoinsTransaction({
73
71
 
74
72
  transaction.mergeCoins(
75
73
  splitCoin,
76
- selectedCoins.map(coin => transaction.object(coin.coinObjectId)),
74
+ selectedCoins.map((coin) => transaction.object(coin.coinObjectId)),
77
75
  );
78
76
 
79
77
  return transaction.object(splitCoin);
@@ -30,16 +30,18 @@ const WALLETS_FORCE_V1 = ['OKX Wallet', 'OKX'];
30
30
  * Check if we should force V1 for this wallet.
31
31
  * Some wallets advertise V2 support but don't properly handle the new Transaction class.
32
32
  */
33
- function shouldForceV1(wallet: WalletWithFeatures<SuiSignTransactionFeature>): boolean {
33
+ function shouldForceV1(
34
+ wallet: WalletWithFeatures<SuiSignTransactionFeature>,
35
+ ): boolean {
34
36
  const walletName = wallet.name || '';
35
- return WALLETS_FORCE_V1.some(name =>
36
- walletName.toLowerCase().includes(name.toLowerCase())
37
+ return WALLETS_FORCE_V1.some((name) =>
38
+ walletName.toLowerCase().includes(name.toLowerCase()),
37
39
  );
38
40
  }
39
41
 
40
42
  /**
41
43
  * Unstake LBTC.
42
- *
44
+ *
43
45
  * Supports both new (sui:signTransaction) and old (sui:signTransactionBlock) wallet interfaces.
44
46
  * Some wallets (like OKX) advertise V2 support but fail with the new Transaction class,
45
47
  * so we force V1 for known problematic wallets.
@@ -58,11 +60,11 @@ export async function unstakeLBTC({
58
60
  // Cast to any to access both V1 and V2 features
59
61
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
60
62
  const walletFeatures = (wallet as any).features || {};
61
-
63
+
62
64
  const walletHasV2 = !!walletFeatures[SIGN_TRANSACTION_V2_FEATURE];
63
65
  const walletHasV1 = !!walletFeatures[SIGN_TRANSACTION_V1_FEATURE];
64
66
  const forceV1 = shouldForceV1(wallet);
65
-
67
+
66
68
  // Use V2 only if available AND wallet is not in the force-V1 list
67
69
  const useV2 = walletHasV2 && !forceV1;
68
70
 
@@ -75,7 +77,10 @@ export async function unstakeLBTC({
75
77
  });
76
78
 
77
79
  const scriptPubKey = Array.from(
78
- Buffer.from((await getOutputScript(btcAddress, env)).replace(/^0x/, ''), 'hex'),
80
+ Buffer.from(
81
+ (await getOutputScript(btcAddress, env)).replace(/^0x/, ''),
82
+ 'hex',
83
+ ),
79
84
  );
80
85
 
81
86
  transaction.moveCall({
@@ -114,7 +119,9 @@ export async function unstakeLBTC({
114
119
  // Use V1 signTransactionBlock (OKX, and other wallets with V2 issues)
115
120
  transaction.setSender(walletAccount.address);
116
121
 
117
- const signedTransaction = await walletFeatures[SIGN_TRANSACTION_V1_FEATURE].signTransactionBlock({
122
+ const signedTransaction = await walletFeatures[
123
+ SIGN_TRANSACTION_V1_FEATURE
124
+ ].signTransactionBlock({
118
125
  chain: chainId,
119
126
  transactionBlock: transaction,
120
127
  account: walletAccount,