@lombard.finance/sdk-sui 1.1.3 → 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/dist/index.cjs +2 -2
- package/dist/index.js +1728 -1462
- package/package.json +2 -1
- package/src/const/getConfig.ts +4 -4
- package/src/index.ts +1 -0
- package/src/web3Sdk/claimLBTC/claimLBTC.ts +29 -0
- package/src/web3Sdk/getBasculeDepositStatus/__tests__/getBasculeDepositStatus.test.ts +111 -0
- package/src/web3Sdk/getBasculeDepositStatus/getBasculeDepositStatus.ts +300 -0
- package/src/web3Sdk/getBasculeDepositStatus/index.ts +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lombard.finance/sdk-sui",
|
|
3
|
-
"version": "1.
|
|
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"
|
package/src/const/getConfig.ts
CHANGED
|
@@ -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
|
-
//
|
|
70
|
+
// lbtc_v5_current
|
|
71
71
|
target:
|
|
72
|
-
'
|
|
72
|
+
'0x124477c9cffb667be781e867fd29579d3498a47e6b69d06155841ca85bcb02d0::treasury::mint_v2',
|
|
73
73
|
denyList: '0x403',
|
|
74
74
|
},
|
|
75
75
|
redeem: {
|
|
76
|
-
//
|
|
76
|
+
// lbtc_v5_current
|
|
77
77
|
target:
|
|
78
|
-
'
|
|
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,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';
|