@lombard.finance/sdk 2.0.7 → 2.0.9

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.
@@ -0,0 +1,185 @@
1
+ import { FormControl, InputLabel, MenuItem, Select, TextField } from '@mui/material';
2
+ import type { Meta } from '@storybook/react';
3
+ import { useEffect, useState } from 'react';
4
+ import { OChainId } from '../../common/types/types';
5
+ import { Button } from '../../stories/components/Button';
6
+ import { CodeBlock } from '../../stories/components/CodeBlock';
7
+ import { useConnect } from '../../stories/hooks/useConnect';
8
+ import useQuery from '../../stories/hooks/useQuery';
9
+ import { VAULT_CONTRACTS } from '../../web3Sdk/signStakeAndBake/contracts';
10
+ import { signStakeAndBake } from '../../web3Sdk/signStakeAndBake/signStakeAndBake';
11
+
12
+ const EXPIRY_OPTIONS = {
13
+ '10 seconds': 10,
14
+ '1 minute': 60,
15
+ '1 hour': 3600,
16
+ '1 day': 86400,
17
+ '1 year': 31536000,
18
+ } as const;
19
+
20
+ type ExpiryOption = keyof typeof EXPIRY_OPTIONS;
21
+
22
+ const AVAILABLE_CHAINS = Object.keys(VAULT_CONTRACTS).map(Number);
23
+
24
+ const meta = {
25
+ title: 'SDK/storeStakeAndBakeSignature',
26
+ component: StoryView,
27
+ tags: ['autodocs'],
28
+ } satisfies Meta<typeof StoryView>;
29
+
30
+ export default meta;
31
+
32
+ export function StoryView() {
33
+ const [selectedExpiry, setSelectedExpiry] =
34
+ useState<ExpiryOption>('10 seconds');
35
+ const [selectedChain, setSelectedChain] = useState(OChainId.holesky);
36
+ const [spender, setSpender] = useState(VAULT_CONTRACTS[OChainId.holesky].SPENDER);
37
+ const [verifyingContract, setVerifyingContract] = useState(
38
+ VAULT_CONTRACTS[OChainId.holesky].VERIFYING_CONTRACT,
39
+ );
40
+
41
+ const {
42
+ data: connectData,
43
+ error: connectError,
44
+ isLoading: isConnectLoading,
45
+ connect,
46
+ } = useConnect();
47
+
48
+ useEffect(() => {
49
+ const contracts = VAULT_CONTRACTS[selectedChain];
50
+ setSpender(contracts.SPENDER);
51
+ setVerifyingContract(contracts.VERIFYING_CONTRACT);
52
+ }, [selectedChain]);
53
+
54
+ const request = async () => {
55
+ if (!connectData || !connectData.provider) {
56
+ return;
57
+ }
58
+
59
+ const expiry =
60
+ Math.floor(Date.now() / 1000) + EXPIRY_OPTIONS[selectedExpiry];
61
+
62
+ return signStakeAndBake({
63
+ provider: connectData.provider,
64
+ address: connectData.account,
65
+ chainId: selectedChain,
66
+ value: '1999',
67
+ expiry,
68
+ spender,
69
+ verifyingContract,
70
+ });
71
+ };
72
+
73
+ const { data, error, isLoading, refetch } = useQuery(
74
+ request,
75
+ [selectedExpiry, selectedChain, spender, verifyingContract],
76
+ false,
77
+ );
78
+
79
+ const formattedConnectData = connectData && {
80
+ account: connectData.account,
81
+ chainId: connectData.chainId,
82
+ };
83
+
84
+ return (
85
+ <>
86
+ <p>
87
+ This method stores the stake and bake signature in the backend. The
88
+ signature is used to approve spending of tokens.
89
+ </p>
90
+
91
+ <div className="mb-4">
92
+ <Button
93
+ onClick={connect}
94
+ disabled={isConnectLoading}
95
+ isLoading={isConnectLoading}
96
+ >
97
+ Connect
98
+ </Button>
99
+
100
+ <CodeBlock text={connectError || formattedConnectData} />
101
+ </div>
102
+
103
+ <div className="mb-4">
104
+ <FormControl fullWidth>
105
+ <InputLabel id="chain-select-label">Chain</InputLabel>
106
+ <Select
107
+ labelId="chain-select-label"
108
+ value={selectedChain}
109
+ label="Chain"
110
+ onChange={e =>
111
+ setSelectedChain(Number(e.target.value) as typeof selectedChain)
112
+ }
113
+ >
114
+ {AVAILABLE_CHAINS.map(chainId => (
115
+ <MenuItem key={chainId} value={chainId}>
116
+ {chainId} Holesky
117
+ </MenuItem>
118
+ ))}
119
+ </Select>
120
+ </FormControl>
121
+ </div>
122
+
123
+ <div className="mb-4">
124
+ <FormControl fullWidth>
125
+ <TextField
126
+ label="Spender Address"
127
+ value={spender}
128
+ onChange={e => setSpender(e.target.value)}
129
+ helperText="The address that will be authorized to spend tokens"
130
+ />
131
+ </FormControl>
132
+ </div>
133
+
134
+ <div className="mb-4">
135
+ <FormControl fullWidth>
136
+ <TextField
137
+ label="Verifying Contract"
138
+ value={verifyingContract}
139
+ onChange={e => setVerifyingContract(e.target.value)}
140
+ helperText="The contract that will verify the signature"
141
+ />
142
+ </FormControl>
143
+ </div>
144
+
145
+ <div className="mb-4">
146
+ <FormControl fullWidth>
147
+ <InputLabel id="expiry-select-label">Expiry Time</InputLabel>
148
+ <Select
149
+ labelId="expiry-select-label"
150
+ value={selectedExpiry}
151
+ label="Expiry Time"
152
+ onChange={e => setSelectedExpiry(e.target.value as ExpiryOption)}
153
+ >
154
+ {Object.keys(EXPIRY_OPTIONS).map(option => (
155
+ <MenuItem key={option} value={option}>
156
+ {option}
157
+ </MenuItem>
158
+ ))}
159
+ </Select>
160
+ </FormControl>
161
+ </div>
162
+
163
+ <Button
164
+ onClick={refetch}
165
+ disabled={
166
+ isLoading || !connectData || connectData.chainId !== selectedChain
167
+ }
168
+ isLoading={isLoading}
169
+ >
170
+ Store Stake and Bake Signature
171
+ </Button>
172
+
173
+ <CodeBlock
174
+ text={
175
+ error ||
176
+ (data && {
177
+ ...data,
178
+ signature: data.signature,
179
+ typedData: data.typedData ? JSON.parse(data.typedData) : '',
180
+ })
181
+ }
182
+ />
183
+ </>
184
+ );
185
+ }
@@ -0,0 +1,56 @@
1
+ import axios from 'axios';
2
+ import { IEnvParam } from '../../common/types/internalTypes';
3
+ import { getErrorMessage } from '../../common/utils/getErrorMessage';
4
+ import { getApiConfig } from '../apiConfig';
5
+
6
+ export type IStoreStakeAndBakeSignatureStatus = 'success';
7
+
8
+ interface IStoreStakeAndBakeSignatureResponse {
9
+ status: IStoreStakeAndBakeSignatureStatus;
10
+ }
11
+
12
+ export interface IStoreStakeAndBakeSignatureParams extends IEnvParam {
13
+ /**
14
+ * signature
15
+ */
16
+ signature: string;
17
+ /**
18
+ * JSON typed data used for the signature
19
+ */
20
+ typedData: string;
21
+ }
22
+
23
+ /**
24
+ * Store stake and bake signature
25
+ *
26
+ * @param {IStoreStakeAndBakeSignatureParams} params - The parameters for storing stake and bake signature
27
+ *
28
+ * @returns {Promise<IStoreStakeAndBakeSignatureStatus>} Response promise with status
29
+ *
30
+ */
31
+ export async function storeStakeAndBakeSignature({
32
+ signature,
33
+ typedData,
34
+ env,
35
+ }: IStoreStakeAndBakeSignatureParams): Promise<IStoreStakeAndBakeSignatureStatus> {
36
+ const { baseApiUrl } = getApiConfig(env);
37
+
38
+ try {
39
+ const { data } = await axios.post<IStoreStakeAndBakeSignatureResponse>(
40
+ `${baseApiUrl}/api/v1/claimer/save-stake-and-bake-signature`,
41
+ null,
42
+ {
43
+ params: {
44
+ typed_data: typedData,
45
+ signature,
46
+ },
47
+ },
48
+ );
49
+
50
+ return data.status;
51
+ } catch (error) {
52
+ const errorMsg = getErrorMessage(error);
53
+
54
+ throw new Error(errorMsg);
55
+ }
56
+ }
@@ -16,9 +16,9 @@ export default meta;
16
16
 
17
17
  type Story = StoryObj<typeof meta>;
18
18
 
19
- export const Sepolia: Story = {
19
+ export const Holesky: Story = {
20
20
  args: {
21
- chainId: OChainId.sepolia,
21
+ chainId: OChainId.holesky,
22
22
  bakeGasEstimate: 77,
23
23
  },
24
24
  };
@@ -1,15 +1,6 @@
1
- export * from '../sdk/getNetworkFeeSignature';
2
- export * from '../sdk/storeNetworkFeeSignature';
3
- export * from './approveLBTC';
4
- export * from './claimLBTC';
5
- export * from './getBasculeDepositStatus';
6
1
  export * from './getLBTCMintingFee';
7
- export * from './getLBTCTotalSupply';
8
- export * from './getPermitNonce';
9
- export * from './lbtcAddressConfig';
10
- export * from './lbtcOFTAdapterAddressConfig';
11
2
  export * from './signLbtcDestionationAddr';
12
3
  export * from './signNetworkFee';
13
- export * from './types';
14
- export * from './unstakeLBTC';
4
+ export * from './signStakeAndBake';
5
+ export * from './signStakeAndBake/config';
15
6
 
@@ -0,0 +1,13 @@
1
+ import { OChainId, TChainId } from '../../common/types/types';
2
+
3
+ export const STAKE_AND_BAKE_SPENDER_ADDRESSES: Record<number, string> = {
4
+ [OChainId.holesky]: '0x52BD640617eeD47A00dA0da93351092D49208d1d',
5
+ };
6
+
7
+ export const getStakeAndBakeSpenderAddress = (chainId: TChainId): string => {
8
+ const address = STAKE_AND_BAKE_SPENDER_ADDRESSES[chainId];
9
+ if (!address) {
10
+ throw new Error(`No spender address configured for chain ID ${chainId}`);
11
+ }
12
+ return address;
13
+ };
@@ -0,0 +1,13 @@
1
+ import { OChainId } from '../../common/types/types';
2
+
3
+ interface IVaultContracts {
4
+ SPENDER: string;
5
+ VERIFYING_CONTRACT: string;
6
+ }
7
+
8
+ export const VAULT_CONTRACTS: Record<number, IVaultContracts> = {
9
+ [OChainId.holesky]: {
10
+ SPENDER: '0x52BD640617eeD47A00dA0da93351092D49208d1d',
11
+ VERIFYING_CONTRACT: '0xED7bfd5C1790576105Af4649817f6d35A75CD818',
12
+ },
13
+ } as const;
@@ -0,0 +1,78 @@
1
+ import { TChainId } from '../../common/types/types';
2
+ import { getPermitNonce } from '../getPermitNonce';
3
+
4
+ export interface IStakeAndBakeTypedData {
5
+ chainId: TChainId;
6
+ expiry: number;
7
+ owner: string;
8
+ spender: string;
9
+ value: string;
10
+ rpcUrl?: string;
11
+ verifyingContract: string;
12
+ }
13
+
14
+ /**
15
+ * Generates EIP-712 typed data for stake and bake signature
16
+ *
17
+ * @param {IStakeAndBakeTypedData} params - Parameters for generating typed data
18
+ * @returns {object} The typed data object conforming to EIP-712
19
+ */
20
+ export async function getStakeAndBakeTypedData({
21
+ chainId,
22
+ expiry,
23
+ owner,
24
+ spender,
25
+ value,
26
+ rpcUrl,
27
+ verifyingContract,
28
+ }: IStakeAndBakeTypedData) {
29
+ const nonce = await getPermitNonce({
30
+ owner,
31
+ chainId,
32
+ rpcUrl,
33
+ });
34
+
35
+ return {
36
+ domain: {
37
+ name: 'Lombard Staked Bitcoin',
38
+ version: '1',
39
+ chainId,
40
+ verifyingContract,
41
+ },
42
+ types: {
43
+ EIP712Domain: [
44
+ {
45
+ name: 'name',
46
+ type: 'string',
47
+ },
48
+ {
49
+ name: 'version',
50
+ type: 'string',
51
+ },
52
+ {
53
+ name: 'chainId',
54
+ type: 'uint256',
55
+ },
56
+ {
57
+ name: 'verifyingContract',
58
+ type: 'address',
59
+ },
60
+ ],
61
+ Permit: [
62
+ { name: 'owner', type: 'address' },
63
+ { name: 'spender', type: 'address' },
64
+ { name: 'value', type: 'uint256' },
65
+ { name: 'nonce', type: 'uint256' },
66
+ { name: 'deadline', type: 'uint256' },
67
+ ],
68
+ },
69
+ primaryType: 'Permit',
70
+ message: {
71
+ owner,
72
+ spender,
73
+ value,
74
+ nonce,
75
+ deadline: expiry.toString(),
76
+ },
77
+ };
78
+ }
@@ -0,0 +1,2 @@
1
+ export * from './getTypedData';
2
+ export * from './signStakeAndBake';
@@ -0,0 +1,197 @@
1
+ import {
2
+ FormControl,
3
+ InputLabel,
4
+ MenuItem,
5
+ Select,
6
+ TextField,
7
+ } from '@mui/material';
8
+ import type { Meta } from '@storybook/react';
9
+ import { useEffect, useState } from 'react';
10
+ import { OChainId } from '../../common/types/types';
11
+ import { Button } from '../../stories/components/Button';
12
+ import { CodeBlock } from '../../stories/components/CodeBlock';
13
+ import { useConnect } from '../../stories/hooks/useConnect';
14
+ import useQuery from '../../stories/hooks/useQuery';
15
+ import { fromCamelCase } from '../../stories/utils/fromCamelCase';
16
+ import { VAULT_CONTRACTS } from './contracts';
17
+ import { signStakeAndBake } from './signStakeAndBake';
18
+
19
+ const { name } = signStakeAndBake;
20
+ const nameWithWhitespaces = fromCamelCase(name);
21
+
22
+ const EXPIRY_OPTIONS = {
23
+ '10 seconds': 10,
24
+ '1 minute': 60,
25
+ '1 hour': 3600,
26
+ '1 day': 86400,
27
+ '1 year': 31536000,
28
+ } as const;
29
+
30
+ type ExpiryOption = keyof typeof EXPIRY_OPTIONS;
31
+
32
+ const AVAILABLE_CHAINS = Object.keys(VAULT_CONTRACTS).map(Number);
33
+
34
+ const meta = {
35
+ title: 'Web3SDK/signStakeAndBake',
36
+ component: StoryView,
37
+ tags: ['autodocs'],
38
+ } satisfies Meta<typeof StoryView>;
39
+
40
+ export default meta;
41
+
42
+ export function StoryView() {
43
+ const [selectedExpiry, setSelectedExpiry] =
44
+ useState<ExpiryOption>('10 seconds');
45
+ const [selectedChain, setSelectedChain] = useState(OChainId.holesky);
46
+ const [spender, setSpender] = useState(
47
+ VAULT_CONTRACTS[OChainId.holesky].SPENDER,
48
+ );
49
+ const [verifyingContract, setVerifyingContract] = useState(
50
+ VAULT_CONTRACTS[OChainId.holesky].VERIFYING_CONTRACT,
51
+ );
52
+
53
+ const {
54
+ data: connectData,
55
+ error: connectError,
56
+ isLoading: isConnectLoading,
57
+ connect,
58
+ } = useConnect();
59
+
60
+ useEffect(() => {
61
+ const contracts = VAULT_CONTRACTS[selectedChain];
62
+ setSpender(contracts.SPENDER);
63
+ setVerifyingContract(contracts.VERIFYING_CONTRACT);
64
+ }, [selectedChain]);
65
+
66
+ const request = async () => {
67
+ if (!connectData || !connectData.provider) {
68
+ return;
69
+ }
70
+
71
+ const expiry =
72
+ Math.floor(Date.now() / 1000) + EXPIRY_OPTIONS[selectedExpiry];
73
+
74
+ return signStakeAndBake({
75
+ provider: connectData.provider,
76
+ address: connectData.account,
77
+ chainId: selectedChain,
78
+ value: '1999',
79
+ expiry,
80
+ spender,
81
+ verifyingContract,
82
+ });
83
+ };
84
+
85
+ const { data, error, isLoading, refetch } = useQuery(
86
+ request,
87
+ [selectedExpiry, selectedChain, spender, verifyingContract],
88
+ false,
89
+ );
90
+
91
+ const formattedConnectData = connectData && {
92
+ account: connectData.account,
93
+ chainId: connectData.chainId,
94
+ };
95
+
96
+ return (
97
+ <>
98
+ <p>
99
+ This method is used to sign a permit for stake and bake operations. The
100
+ signature is used to approve spending of tokens.
101
+ </p>
102
+
103
+ <div className="mb-4">
104
+ <Button
105
+ onClick={connect}
106
+ disabled={isConnectLoading}
107
+ isLoading={isConnectLoading}
108
+ >
109
+ Connect
110
+ </Button>
111
+
112
+ <CodeBlock text={connectError || formattedConnectData} />
113
+ </div>
114
+
115
+ <div className="mb-4">
116
+ <FormControl fullWidth>
117
+ <InputLabel id="chain-select-label">Chain</InputLabel>
118
+ <Select
119
+ labelId="chain-select-label"
120
+ value={selectedChain}
121
+ label="Chain"
122
+ onChange={e =>
123
+ setSelectedChain(Number(e.target.value) as typeof selectedChain)
124
+ }
125
+ >
126
+ {AVAILABLE_CHAINS.map(chainId => (
127
+ <MenuItem key={chainId} value={chainId}>
128
+ {chainId}
129
+ </MenuItem>
130
+ ))}
131
+ </Select>
132
+ </FormControl>
133
+ </div>
134
+
135
+ <div className="mb-4">
136
+ <FormControl fullWidth>
137
+ <TextField
138
+ label="Spender Address"
139
+ value={spender}
140
+ onChange={e => setSpender(e.target.value)}
141
+ helperText="The address that will be authorized to spend tokens"
142
+ />
143
+ </FormControl>
144
+ </div>
145
+
146
+ <div className="mb-4">
147
+ <FormControl fullWidth>
148
+ <TextField
149
+ label="Verifying Contract"
150
+ value={verifyingContract}
151
+ onChange={e => setVerifyingContract(e.target.value)}
152
+ helperText="The contract that will verify the signature"
153
+ />
154
+ </FormControl>
155
+ </div>
156
+
157
+ <div className="mb-4">
158
+ <FormControl fullWidth>
159
+ <InputLabel id="expiry-select-label">Expiry Time</InputLabel>
160
+ <Select
161
+ labelId="expiry-select-label"
162
+ value={selectedExpiry}
163
+ label="Expiry Time"
164
+ onChange={e => setSelectedExpiry(e.target.value as ExpiryOption)}
165
+ >
166
+ {Object.keys(EXPIRY_OPTIONS).map(option => (
167
+ <MenuItem key={option} value={option}>
168
+ {option}
169
+ </MenuItem>
170
+ ))}
171
+ </Select>
172
+ </FormControl>
173
+ </div>
174
+
175
+ <Button
176
+ onClick={refetch}
177
+ disabled={
178
+ isLoading || !connectData || connectData.chainId !== selectedChain
179
+ }
180
+ isLoading={isLoading}
181
+ >
182
+ {nameWithWhitespaces}
183
+ </Button>
184
+
185
+ <CodeBlock
186
+ text={
187
+ error ||
188
+ (data && {
189
+ ...data,
190
+ signature: data.signature,
191
+ typedData: data.typedData ? JSON.parse(data.typedData) : '',
192
+ })
193
+ }
194
+ />
195
+ </>
196
+ );
197
+ }
@@ -0,0 +1,103 @@
1
+ import { TChainId } from '../../common/types/types';
2
+ import { Provider } from '../../provider';
3
+ import { IProviderBasedParams } from '../types';
4
+ import { getStakeAndBakeTypedData } from './getTypedData';
5
+
6
+ const NO_SIGNATURE_ERROR =
7
+ 'Failed to obtain a valid signature. The response is undefined or invalid.';
8
+
9
+ export interface ISignStakeAndBakeParams
10
+ extends Pick<IProviderBasedParams, 'provider'> {
11
+ /**
12
+ * The address to sign with (owner)
13
+ */
14
+ address: string;
15
+ /**
16
+ * Chain ID for the signature
17
+ */
18
+ chainId: TChainId;
19
+ /**
20
+ * The value to approve
21
+ */
22
+ value: string;
23
+ /**
24
+ * Expiry date as a unix timestamp
25
+ */
26
+ expiry: number;
27
+ /**
28
+ * Optional RPC URL for the network
29
+ */
30
+ rpcUrl?: string;
31
+ /**
32
+ * The spender address that will be authorized to spend tokens
33
+ */
34
+ spender: string;
35
+ /**
36
+ * The contract address that will verify the signature
37
+ */
38
+ verifyingContract: string;
39
+ }
40
+
41
+ export interface ISignStakeAndBakeResult {
42
+ /**
43
+ * The signature
44
+ */
45
+ signature: string;
46
+ /**
47
+ * The typed data used to generate the signature
48
+ */
49
+ typedData: string;
50
+ }
51
+
52
+ /**
53
+ * Signs stake and bake authorization with EIP-712
54
+ *
55
+ * @param {ISignStakeAndBakeParams} params - Parameters for signing
56
+ * @returns {Promise<ISignStakeAndBakeResult>} The signature and typed data
57
+ */
58
+ export async function signStakeAndBake({
59
+ address,
60
+ provider,
61
+ chainId,
62
+ value,
63
+ expiry,
64
+ rpcUrl,
65
+ spender,
66
+ verifyingContract,
67
+ }: ISignStakeAndBakeParams): Promise<ISignStakeAndBakeResult> {
68
+ const providerInstance = new Provider({
69
+ provider,
70
+ account: address,
71
+ chainId,
72
+ });
73
+
74
+ const typedDataObject = await getStakeAndBakeTypedData({
75
+ chainId,
76
+ expiry,
77
+ owner: address,
78
+ spender,
79
+ value,
80
+ rpcUrl,
81
+ verifyingContract,
82
+ });
83
+
84
+ const typedData = JSON.stringify(typedDataObject);
85
+
86
+ const signature = await providerInstance.web3?.currentProvider?.request<
87
+ 'eth_signTypedData_v4',
88
+ string
89
+ >({
90
+ method: 'eth_signTypedData_v4',
91
+ params: [address, typedData],
92
+ });
93
+
94
+ if (typeof signature === 'string') {
95
+ return { signature, typedData };
96
+ }
97
+
98
+ if (!signature?.result) {
99
+ throw new Error(NO_SIGNATURE_ERROR);
100
+ }
101
+
102
+ return { signature: signature.result, typedData };
103
+ }