@fystack/sdk 0.1.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.
@@ -0,0 +1,243 @@
1
+ import { APIService, WalletAddressType, WalletDetail } from './api'
2
+ import { APICredentials, TransactionStatusResponse, TxStatus, TransactionError } from './types'
3
+ import { Environment } from './config'
4
+ import { StatusPoller, StatusPollerOptions } from './utils/statusPoller'
5
+ import { Transaction, PublicKey, VersionedTransaction } from '@solana/web3.js'
6
+ import bs58 from 'bs58'
7
+ import { Buffer } from 'buffer'
8
+
9
+ export class SolanaSigner {
10
+ private address!: string
11
+ private APIService: APIService
12
+ private APIKey: string
13
+ private walletDetail: WalletDetail
14
+ private pollerOptions?: StatusPollerOptions
15
+
16
+ constructor(
17
+ credentials: APICredentials,
18
+ environment: Environment,
19
+ pollerOptions?: StatusPollerOptions
20
+ ) {
21
+ this.APIKey = credentials.apiKey
22
+ this.APIService = new APIService(credentials, environment)
23
+ this.pollerOptions = pollerOptions
24
+ }
25
+
26
+ setWallet(walletId: string): void {
27
+ if (!walletId || walletId.trim() === '') {
28
+ throw new Error('Invalid wallet ID provided')
29
+ }
30
+ // Set walletId in walletDetail with default values for required properties
31
+ this.walletDetail = {
32
+ WalletID: walletId,
33
+ APIKey: '',
34
+ Name: '',
35
+ AddressType: '',
36
+ Address: ''
37
+ // Other fields will be populated when getAddress is called
38
+ }
39
+ // Reset address cache since we're changing wallets
40
+ this.address = ''
41
+ }
42
+
43
+ async getAddress(): Promise<string> {
44
+ if (this.address) {
45
+ return this.address
46
+ }
47
+
48
+ if (!this.APIKey && !this.walletDetail?.WalletID) {
49
+ throw new Error('Wallet detail not found, use setWallet(walletId) to set wallet first!')
50
+ }
51
+
52
+ const detail: WalletDetail = await this.APIService.getWalletDetail(
53
+ WalletAddressType.Sol,
54
+ this.walletDetail?.WalletID
55
+ )
56
+
57
+ this.walletDetail = detail
58
+ if (detail?.Address) {
59
+ // cache the address
60
+ this.address = detail.Address
61
+ }
62
+
63
+ if (!this.address) {
64
+ throw new Error('Address not found')
65
+ }
66
+
67
+ return this.address
68
+ }
69
+
70
+ private async waitForTransactionStatus(transactionId: string): Promise<string> {
71
+ const poller = new StatusPoller(this.pollerOptions)
72
+ // Poll for transaction status using signaturePoller
73
+ const result = await poller.poll<TransactionStatusResponse>(
74
+ // Polling function
75
+ () => {
76
+ console.log('Polling status')
77
+ return this.APIService.getTransactionStatus(this.walletDetail.WalletID, transactionId)
78
+ },
79
+ // Success condition
80
+ (result) =>
81
+ (result.status === TxStatus.Confirmed || result.status === TxStatus.Completed) &&
82
+ !!result.hash,
83
+ // Error condition
84
+ (result) => {
85
+ if (result.status === TxStatus.Failed) {
86
+ throw new TransactionError(
87
+ result.failed_reason || 'Transaction failed',
88
+ 'TRANSACTION_FAILED',
89
+ result.transaction_id
90
+ )
91
+ }
92
+ return result.status === TxStatus.Rejected
93
+ }
94
+ )
95
+ console.log('result', result)
96
+
97
+ if (!result.hash) {
98
+ throw new TransactionError(
99
+ 'Transaction hash not found in successful response',
100
+ 'TRANSACTION_HASH_MISSING',
101
+ result.transaction_id
102
+ )
103
+ }
104
+
105
+ return result.hash
106
+ }
107
+
108
+ /**
109
+ * Signs a Solana transaction
110
+ * @param transaction Base64 encoded serialized transaction
111
+ * @returns Signature as a base58 encoded string
112
+ */
113
+ async signTransaction(transaction: string): Promise<string> {
114
+ if (!this.address) {
115
+ await this.getAddress()
116
+ }
117
+
118
+ const data = {
119
+ data: transaction,
120
+ from: this.address
121
+ }
122
+
123
+ // Call the signRaw API similar to ApexSigner
124
+ const response = await this.APIService.signTransaction(this.walletDetail.WalletID, {
125
+ ...data,
126
+ meta: {
127
+ tx_method: 'solana_signTransaction'
128
+ },
129
+ chainId: '1399811149'
130
+ })
131
+ console.log('respnpse', response)
132
+
133
+ // Wait for the signature
134
+ return this.waitForTransactionStatus(response.transaction_id)
135
+ }
136
+
137
+ /**
138
+ * Signs a Solana message
139
+ * @param message The message to sign (string or Uint8Array)
140
+ * @returns Signature as a base58 encoded string
141
+ */
142
+ async signMessage(message: string | Uint8Array): Promise<string> {
143
+ if (!this.address) {
144
+ await this.getAddress()
145
+ }
146
+
147
+ const messageStr =
148
+ typeof message === 'string' ? message : Buffer.from(message).toString('base64')
149
+
150
+ const response = await this.APIService.requestSign(this.walletDetail.WalletID, {
151
+ method: 'solana_signMessage',
152
+ message: messageStr,
153
+ chain_id: 0 // Not used for Solana but required by API
154
+ })
155
+
156
+ console.log('Respnse', response)
157
+
158
+ return this.waitForTransactionStatus(response.transaction_id)
159
+ }
160
+
161
+ /**
162
+ * Signs and sends a Solana transaction
163
+ * @param transaction Base64 encoded serialized transaction
164
+ * @returns Transaction signature
165
+ */
166
+ async signAndSendTransaction(transaction: string): Promise<string> {
167
+ if (!this.address) {
168
+ await this.getAddress()
169
+ }
170
+
171
+ const data = {
172
+ transaction,
173
+ from: this.address,
174
+ method: 'solana_signAndSendTransaction'
175
+ }
176
+
177
+ const response = await this.APIService.signTransaction(this.walletDetail.WalletID, data)
178
+ const txHash = await this.waitForTransactionStatus(response.transaction_id)
179
+ console.log('transaction succeed!')
180
+
181
+ return txHash
182
+ }
183
+
184
+ /**
185
+ * Signs multiple Solana transactions
186
+ * @param transactions Array of base64 encoded serialized transactions
187
+ * @returns Array of signatures as base58 encoded strings
188
+ */
189
+ async signAllTransactions(transactions: string[]): Promise<{ transactions: string[] }> {
190
+ if (!this.address) {
191
+ await this.getAddress()
192
+ }
193
+
194
+ // We need to get the signatures and then incorporate them into the transactions
195
+ const signaturePromises = transactions.map(async (transaction) => {
196
+ // Get the signature
197
+ const signature = await this.signTransaction(transaction)
198
+
199
+ // Here you would need to incorporate the signature into the transaction
200
+ // This is a placeholder - you'll need to implement actual signature incorporation
201
+ // based on your Solana transaction structure
202
+ const signedTransaction = this.incorporateSignatureIntoTransaction(transaction, signature)
203
+ return signedTransaction
204
+ })
205
+
206
+ // Wait for all transactions to be signed in parallel
207
+ const signedTransactions = await Promise.all(signaturePromises)
208
+ return { transactions: signedTransactions }
209
+ }
210
+
211
+ private incorporateSignatureIntoTransaction(transaction: string, signature: string): string {
212
+ // Decode base64 transaction to buffer
213
+ const transactionBuffer = Buffer.from(transaction, 'base64')
214
+
215
+ try {
216
+ // First try with legacy transaction format
217
+ const tx = Transaction.from(transactionBuffer)
218
+
219
+ // Decode the base58 signature
220
+ const signatureBuffer = bs58.decode(signature)
221
+
222
+ // Add the signature to the transaction
223
+ tx.addSignature(new PublicKey(this.address), Buffer.from(signatureBuffer))
224
+
225
+ // Serialize and encode back to base64
226
+ return Buffer.from(tx.serialize()).toString('base64')
227
+ } catch (error) {
228
+ if (error.message.includes('Versioned messages')) {
229
+ // Deserialize as a versioned transaction
230
+ const versionedTx = VersionedTransaction.deserialize(transactionBuffer)
231
+
232
+ // Add the signature (convert from base58)
233
+ const signatureBuffer = bs58.decode(signature)
234
+ versionedTx.signatures[0] = Buffer.from(signatureBuffer)
235
+
236
+ // Serialize and encode back to base64
237
+ return Buffer.from(versionedTx.serialize()).toString('base64')
238
+ }
239
+ // If it's another type of error, rethrow it
240
+ throw error
241
+ }
242
+ }
243
+ }
package/src/types.ts ADDED
@@ -0,0 +1,161 @@
1
+ export class TransactionError extends Error {
2
+ constructor(
3
+ message: string,
4
+ public readonly code: string,
5
+ public readonly transactionId?: string,
6
+ public readonly originalError?: Error
7
+ ) {
8
+ super(message)
9
+ this.name = 'TransactionError'
10
+ }
11
+ }
12
+ export enum TxStatus {
13
+ Pending = 'pending',
14
+ Completed = 'completed',
15
+ Confirmed = 'confirmed',
16
+ Failed = 'failed',
17
+ PendingApproval = 'pending_approval',
18
+ Rejected = 'rejected'
19
+ }
20
+
21
+ export enum TxApprovalStatus {
22
+ Pending = 'pending',
23
+ Approved = 'approved',
24
+ Rejected = 'rejected'
25
+ }
26
+
27
+ export interface APICredentials {
28
+ apiKey: string
29
+ apiSecret: string
30
+ }
31
+
32
+ export interface WebhookEvent {
33
+ webhook_id: string // Equivalent to "webhook_id" in Go struct
34
+ resource_id: string // Equivalent to "resource_id" in Go struct
35
+ url: string // URL where the webhook was triggered
36
+ payload: any // Binary or string representation of the payload
37
+ event: string // Name of the event
38
+ }
39
+
40
+ export interface SignRequestParams {
41
+ method: string
42
+ message: string
43
+ chain_id: number
44
+ typed_data?: string
45
+ }
46
+
47
+ export interface SignResponse {
48
+ transaction_id: string
49
+ }
50
+
51
+ export interface SignatureStatusResponse {
52
+ status: TxStatus
53
+ signature?: string
54
+ transaction_id: string
55
+ created_at: string
56
+ updated_at: string
57
+ approvals: Array<{
58
+ user_id: string
59
+ status: TxApprovalStatus
60
+ }>
61
+ }
62
+
63
+ export interface ApprovalInfo {
64
+ user_id: string
65
+ status: TxApprovalStatus
66
+ }
67
+
68
+ export interface TransactionStatusResponse {
69
+ transaction_id: string
70
+ status: TxStatus
71
+ method: string
72
+ hash?: string
73
+ created_at: string
74
+ updated_at: string
75
+ approvals: ApprovalInfo[]
76
+ failed_reason?: string
77
+ }
78
+
79
+ // Wallets
80
+ export enum WalletType {
81
+ Standard = 'standard',
82
+ MPC = 'mpc'
83
+ }
84
+
85
+ export interface CreateWalletOptions {
86
+ name: string
87
+ walletType: WalletType
88
+ }
89
+ export enum WalletCreationStatus {
90
+ Pending = 'pending',
91
+ Success = 'success',
92
+ Error = 'error'
93
+ }
94
+
95
+ export interface CreateWalletResponse {
96
+ wallet_id: string
97
+ status: WalletCreationStatus
98
+ }
99
+
100
+ export interface WalletCreationStatusResponse {
101
+ wallet_id: string
102
+ status: WalletCreationStatus
103
+ }
104
+
105
+ export interface WalletAssetNetwork {
106
+ id: string
107
+ created_at: string
108
+ updated_at: string
109
+ name: string
110
+ description?: string
111
+ is_evm: boolean
112
+ chain_id: number
113
+ native_currency: string
114
+ is_testnet?: boolean
115
+ internal_code: string
116
+ explorer_tx: string
117
+ explorer_address: string
118
+ explorer_token: string
119
+ confirmation_blocks: number
120
+ block_interval_in_seconds: number
121
+ disabled: boolean
122
+ logo_url: string
123
+ }
124
+
125
+ export interface WalletAssetDetail {
126
+ id: string
127
+ created_at: string
128
+ updated_at: string
129
+ name: string
130
+ symbol: string
131
+ decimals: number
132
+ logo_url: string
133
+ is_native: boolean
134
+ address_type: string
135
+ is_whitelisted: boolean
136
+ address?: string
137
+ network_id: string
138
+ network?: WalletAssetNetwork
139
+ }
140
+
141
+ export interface WalletAsset {
142
+ id: string
143
+ created_at: string
144
+ updated_at: string
145
+ wallet_id: string
146
+ asset_id: string
147
+ deposit_address: string
148
+ hidden: boolean
149
+ asset: WalletAssetDetail
150
+ }
151
+
152
+ export enum AddressType {
153
+ Evm = 'evm',
154
+ Solana = 'sol'
155
+ }
156
+
157
+ export interface DepositAddressResponse {
158
+ asset_id?: string
159
+ address: string
160
+ qr_code: string
161
+ }
@@ -0,0 +1,82 @@
1
+ export interface StatusPollerOptions {
2
+ maxAttempts?: number
3
+ interval?: number
4
+ backoffFactor?: number
5
+ maxInterval?: number
6
+ timeoutMs?: number
7
+ }
8
+
9
+ export const DEFAULT_POLLER_OPTIONS: StatusPollerOptions = {
10
+ maxAttempts: 10,
11
+ interval: 1000, // Start with 1 second
12
+ backoffFactor: 1.5, // Increase interval by 50% each time
13
+ maxInterval: 10000, // Max 10 seconds between attempts
14
+ timeoutMs: 10 * 60 * 1000 // 10 minutes totla
15
+ }
16
+
17
+ export class StatusPoller {
18
+ private startTime: number
19
+ private attempts: number
20
+ private currentInterval: number
21
+ private readonly options: Required<StatusPollerOptions>
22
+
23
+ constructor(options: StatusPollerOptions = {}) {
24
+ this.options = { ...DEFAULT_POLLER_OPTIONS, ...options } as Required<StatusPollerOptions>
25
+ this.startTime = Date.now()
26
+ this.attempts = 0
27
+ this.currentInterval = this.options.interval
28
+ }
29
+
30
+ private async wait(): Promise<void> {
31
+ await new Promise((resolve) => setTimeout(resolve, this.currentInterval))
32
+
33
+ // Implement exponential backoff
34
+ this.currentInterval = Math.min(
35
+ this.currentInterval * this.options.backoffFactor,
36
+ this.options.maxInterval
37
+ )
38
+ }
39
+
40
+ private shouldContinue(): boolean {
41
+ const timeElapsed = Date.now() - this.startTime
42
+ if (timeElapsed >= this.options.timeoutMs) {
43
+ throw new Error(`Status polling timed out after ${timeElapsed}ms`)
44
+ }
45
+
46
+ if (this.attempts >= this.options.maxAttempts) {
47
+ throw new Error(`Maximum polling attempts (${this.options.maxAttempts}) exceeded`)
48
+ }
49
+
50
+ return true
51
+ }
52
+
53
+ async poll<T>(
54
+ pollingFn: () => Promise<T>,
55
+ successCondition: (result: T) => boolean,
56
+ errorCondition?: (result: T) => boolean | void
57
+ ): Promise<T> {
58
+ while (this.shouldContinue()) {
59
+ this.attempts++
60
+
61
+ const result = await pollingFn()
62
+
63
+ // Check for error condition first
64
+ if (errorCondition) {
65
+ const shouldError = errorCondition(result)
66
+ if (shouldError) {
67
+ throw new Error('Status polling failed')
68
+ }
69
+ }
70
+
71
+ // Check for success condition
72
+ if (successCondition(result)) {
73
+ return result
74
+ }
75
+
76
+ // If neither condition is met, wait and continue polling
77
+ await this.wait()
78
+ }
79
+
80
+ throw new Error('Polling ended without meeting success condition')
81
+ }
82
+ }
package/src/utils.ts ADDED
@@ -0,0 +1,101 @@
1
+ import CryptoJS from 'crypto-js'
2
+ import { WebhookEvent } from './types'
3
+
4
+ export async function computeHMAC(
5
+ apiSecret: string,
6
+ params: Record<string, string>
7
+ ): Promise<string> {
8
+ const encodedParams = Object.entries(params)
9
+ .map(([key, value]) => `${key}=${value}`)
10
+ .join('&')
11
+
12
+ const digest = CryptoJS.HmacSHA256(encodedParams, apiSecret)
13
+ return digest.toString(CryptoJS.enc.Hex)
14
+ }
15
+
16
+ export async function computeHMACForWebhook(
17
+ apiSecret: string,
18
+ event: WebhookEvent
19
+ ): Promise<string> {
20
+ const eventStr = canonicalizeJSON(event)
21
+ console.log('eventStr', eventStr)
22
+ const digest = CryptoJS.HmacSHA256(eventStr, apiSecret)
23
+ return digest.toString(CryptoJS.enc.Hex)
24
+ }
25
+
26
+ /**
27
+ * Canonicalizes a TypeScript object by sorting its keys recursively.
28
+ *
29
+ * @param inputObject - The input object to canonicalize.
30
+ * @returns A canonicalized JSON string with sorted keys.
31
+ * @throws Error if the input is not a valid object.
32
+ */
33
+ export function canonicalizeJSON(inputObject: Record<string, any>): string {
34
+ if (typeof inputObject !== 'object' || inputObject === null) {
35
+ throw new Error('Input must be a non-null object.')
36
+ }
37
+
38
+ /**
39
+ * Recursively sorts the keys of an object or processes arrays.
40
+ *
41
+ * @param value - The value to sort (can be an object, array, or primitive).
42
+ * @returns The sorted value.
43
+ */
44
+ const sortKeys = (value: any): any => {
45
+ if (Array.isArray(value)) {
46
+ // Recursively sort each element in the array
47
+ return value.map(sortKeys)
48
+ }
49
+
50
+ if (value && typeof value === 'object' && value.constructor === Object) {
51
+ // Sort object keys and recursively sort their values
52
+ return Object.keys(value)
53
+ .sort()
54
+ .reduce((sortedObj: Record<string, any>, key: string) => {
55
+ sortedObj[key] = sortKeys(value[key])
56
+ return sortedObj
57
+ }, {})
58
+ }
59
+
60
+ // Return primitive values as-is
61
+ return value
62
+ }
63
+
64
+ // Sort the keys recursively
65
+ const sortedObject = sortKeys(inputObject)
66
+
67
+ // Convert the sorted object back into a JSON string
68
+ return JSON.stringify(sortedObject)
69
+ }
70
+
71
+ /**
72
+ * Validates if a string is a valid UUID v4
73
+ * @param uuid The string to validate
74
+ * @returns boolean indicating if the string is a valid UUID
75
+ */
76
+ export function isValidUUID(uuid: string): boolean {
77
+ if (!uuid || typeof uuid !== 'string') {
78
+ return false
79
+ }
80
+
81
+ // UUID v4 pattern:
82
+ // 8-4-4-4-12 where third group starts with 4 and fourth group starts with 8, 9, a, or b
83
+ const uuidV4Regex = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i
84
+ return uuidV4Regex.test(uuid)
85
+ }
86
+
87
+ /**
88
+ * Validates if a string is a valid UUID and throws an error if not
89
+ * @param uuid The string to validate
90
+ * @param paramName The name of the parameter being validated (for error message)
91
+ * @throws Error if the UUID is invalid
92
+ */
93
+ export function validateUUID(uuid: string, paramName: string): void {
94
+ if (!uuid || typeof uuid !== 'string') {
95
+ throw new Error(`${paramName} is required and must be a string`)
96
+ }
97
+
98
+ if (!isValidUUID(uuid)) {
99
+ throw new Error(`Invalid ${paramName} format. Must be a valid UUID v4`)
100
+ }
101
+ }
package/test.js ADDED
@@ -0,0 +1,76 @@
1
+ import { TypedDataEncoder, verifyTypedData, Signature } from 'ethers'
2
+
3
+ // 0x2f71f2595162eaca40708bf6d6327d437e760b27d26f3a933389ebdb4eedf3b167008df6f1d403503c82a6d58da5659b14c578019603d6b138b2c03729c92af701
4
+ // 0xd77d10c530b25d1ea6a466eb2b3169138b5ca0d1320ff54bbad000d97f686d0a314c5233844d84da5adf3580afa38c82d9595c22a985acfea02ca722c55df48a00
5
+ // Example usage
6
+ const address = '0xFFe120Fd4D5AB5A9f25b25c30620ac8ee3E1EF21'
7
+ const jsonData = {
8
+ domain: {
9
+ name: 'Permit2',
10
+ chainId: '8453',
11
+ verifyingContract: '0x000000000022d473030f116ddee9f6b43ac78ba3'
12
+ },
13
+ types: {
14
+ PermitSingle: [
15
+ { name: 'details', type: 'PermitDetails' },
16
+ { name: 'spender', type: 'address' },
17
+ { name: 'sigDeadline', type: 'uint256' }
18
+ ],
19
+ PermitDetails: [
20
+ { name: 'token', type: 'address' },
21
+ { name: 'amount', type: 'uint160' },
22
+ { name: 'expiration', type: 'uint48' },
23
+ { name: 'nonce', type: 'uint48' }
24
+ ]
25
+ },
26
+ message: {
27
+ details: {
28
+ token: '0x0b3e328455c4059eeb9e3f84b5543f74e24e7e1b',
29
+ amount: '1461501637330902918203684832716283019655932542975',
30
+ expiration: '1743496074',
31
+ nonce: '0'
32
+ },
33
+ spender: '0x6ff5693b99212da76ad316178a184ab56d299b43',
34
+ sigDeadline: '1740905874'
35
+ }
36
+ }
37
+ const signature =
38
+ '0x032f29648f9c2d9d2524aef755c225b96121f2c9b01beee5869a70ef95eb089548dc41f71126dac83c1632738dbea12bf5c925715068e32dda0064701bfc019e00'
39
+
40
+ async function verifySignature(address, jsonData, signature) {
41
+ try {
42
+ // Step 1: Compute the EIP-712 hash
43
+ const computedHash = TypedDataEncoder.hash(jsonData.domain, jsonData.types, jsonData.message)
44
+ console.log('🔹 Computed EIP-712 Hash:', computedHash)
45
+
46
+ // Step 2: Recover the signer address
47
+ const recoveredAddress = verifyTypedData(
48
+ jsonData.domain,
49
+ jsonData.types,
50
+ jsonData.message,
51
+ signature
52
+ )
53
+
54
+ console.log('✅ Recovered Address:', recoveredAddress)
55
+ console.log('🔹 Expected Address:', address)
56
+
57
+ // Step 3: Compare the addresses
58
+ const isValid = recoveredAddress.toLowerCase() === address.toLowerCase()
59
+ console.log('🔹 Is Signature Valid?', isValid)
60
+
61
+ // Step 4: Debug signature parts (r, s, v)
62
+ const sig = Signature.from(signature)
63
+ console.log('🔹 Signature Parts:')
64
+ console.log(' r:', sig.r)
65
+ console.log(' s:', sig.s)
66
+ console.log(' v:', sig.v)
67
+
68
+ return isValid
69
+ } catch (error) {
70
+ console.error('❌ Signature Verification Failed:', error)
71
+ return false
72
+ }
73
+ }
74
+
75
+ // Run verification
76
+ verifySignature(address, jsonData, signature)
package/tsconfig.json ADDED
@@ -0,0 +1,18 @@
1
+ {
2
+ "compilerOptions": {
3
+ "declaration": true,
4
+ "declarationDir": "dist/types",
5
+ "rootDirs": ["src", "test"],
6
+ "outDir": "dist",
7
+ "module": "es2020",
8
+ "target": "es2020",
9
+ "esModuleInterop": true,
10
+ "resolveJsonModule": true,
11
+ "moduleResolution": "node",
12
+ "lib": ["es2020"],
13
+ "strictNullChecks": true,
14
+ "noImplicitAny": true,
15
+ "skipLibCheck": true
16
+ },
17
+ "exclude": ["node_modules", "**/*.test.ts"]
18
+ }
package/vite-env.d.ts ADDED
@@ -0,0 +1 @@
1
+ /// <reference types="vite/types/importMeta.d.ts" />
@@ -0,0 +1,10 @@
1
+ import { defineConfig } from 'vitest/config'
2
+ import { loadEnv } from 'vite'
3
+
4
+ // https://vitest.dev/config/
5
+ export default defineConfig({
6
+ test: {
7
+ testTimeout: 20000 * 1000,
8
+ env: loadEnv('', process.cwd(), '')
9
+ }
10
+ })