@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.
package/src/sdk.ts ADDED
@@ -0,0 +1,140 @@
1
+ import { APIService } from './api'
2
+ import { APICredentials, CreateWalletOptions } from './types'
3
+ import { Environment } from './config'
4
+ import { StatusPoller } from './utils/statusPoller'
5
+ import {
6
+ WalletCreationStatus,
7
+ CreateWalletResponse,
8
+ WalletCreationStatusResponse,
9
+ WalletType,
10
+ WalletAsset,
11
+ AddressType,
12
+ DepositAddressResponse
13
+ } from './types'
14
+ import { validateUUID } from './utils'
15
+
16
+ export interface SDKOptions {
17
+ credentials: APICredentials
18
+ environment?: Environment
19
+ logger?: boolean
20
+ }
21
+
22
+ export class FystackSDK {
23
+ private apiService: APIService
24
+ private enableLogging: boolean
25
+
26
+ constructor(options: SDKOptions) {
27
+ const { credentials, environment = Environment.Production, logger = false } = options
28
+ this.apiService = new APIService(credentials, environment)
29
+ this.enableLogging = logger
30
+ }
31
+
32
+ private log(message: string): void {
33
+ if (this.enableLogging) {
34
+ console.log(`[FystackSDK] ${message}`)
35
+ }
36
+ }
37
+
38
+ /**
39
+ * Creates a new wallet
40
+ * @param options Wallet creation options
41
+ * @param waitForCompletion Whether to wait for the wallet creation to complete
42
+ * @returns Promise with wallet ID and status
43
+ */
44
+ async createWallet(
45
+ options: CreateWalletOptions,
46
+ waitForCompletion: boolean = true
47
+ ): Promise<CreateWalletResponse> {
48
+ const { name, walletType = WalletType.Standard } = options
49
+
50
+ const response = await this.apiService.createWallet({
51
+ name,
52
+ walletType
53
+ })
54
+
55
+ if (waitForCompletion && response.status === WalletCreationStatus.Pending) {
56
+ return this.waitForWalletCreation(response.wallet_id)
57
+ }
58
+
59
+ return response
60
+ }
61
+
62
+ /**
63
+ * Gets the current status of a wallet creation process
64
+ * @param walletId The ID of the wallet being created
65
+ * @returns Promise with wallet creation status details
66
+ */
67
+ async getWalletCreationStatus(walletId: string): Promise<WalletCreationStatusResponse> {
68
+ const response = await this.apiService.getWalletCreationStatus(walletId)
69
+ return response
70
+ }
71
+
72
+ /**
73
+ * Waits for a wallet to be created and returns the final status
74
+ * @param walletId The ID of the wallet being created
75
+ * @returns Promise with wallet ID and final status
76
+ */
77
+ private async waitForWalletCreation(walletId: string): Promise<CreateWalletResponse> {
78
+ const poller = new StatusPoller()
79
+
80
+ // Poll for wallet creation status
81
+ const result = await poller.poll<WalletCreationStatusResponse>(
82
+ // Polling function
83
+ async () => {
84
+ this.log('Polling wallet creation status...')
85
+ const response = await this.apiService.getWalletCreationStatus(walletId)
86
+ return response
87
+ },
88
+ // Success condition - when status is either success or error
89
+ (result) =>
90
+ [WalletCreationStatus.Success, WalletCreationStatus.Error].includes(result.status),
91
+ // Error condition - no specific error condition needed, as we're polling until final state
92
+ undefined
93
+ )
94
+
95
+ this.log(`Wallet creation completed with status: ${result.status}`)
96
+
97
+ return {
98
+ wallet_id: result.wallet_id,
99
+ status: result.status
100
+ }
101
+ }
102
+
103
+ /**
104
+ * Gets assets associated with a wallet
105
+ * @param walletId The ID of the wallet
106
+ * @returns Promise with wallet assets
107
+ */
108
+ async getWalletAssets(walletId: string): Promise<WalletAsset[]> {
109
+ if (!walletId || walletId.trim() === '') {
110
+ throw new Error('Invalid wallet ID provided')
111
+ }
112
+
113
+ const data = await this.apiService.getWalletAssets(walletId)
114
+ return data
115
+ }
116
+
117
+ /**
118
+ * Gets deposit address for a wallet by address type
119
+ * @param walletId The wallet ID
120
+ * @param addressType The type of address (evm, sol)
121
+ * @returns Promise with deposit address information
122
+ */
123
+ async getDepositAddress(
124
+ walletId: string,
125
+ addressType: AddressType
126
+ ): Promise<DepositAddressResponse> {
127
+ validateUUID(walletId, 'walletId')
128
+
129
+ if (!Object.values(AddressType).includes(addressType)) {
130
+ throw new Error(
131
+ `Invalid address type: ${addressType}. Must be one of: ${Object.values(AddressType).join(
132
+ ', '
133
+ )}`
134
+ )
135
+ }
136
+
137
+ const depositAddressInfo = await this.apiService.getDepositAddress(walletId, addressType)
138
+ return depositAddressInfo
139
+ }
140
+ }
package/src/signer.ts ADDED
@@ -0,0 +1,392 @@
1
+ import {
2
+ AbstractSigner,
3
+ TypedDataDomain,
4
+ TypedDataField,
5
+ resolveProperties,
6
+ resolveAddress,
7
+ assertArgument,
8
+ getAddress,
9
+ Transaction,
10
+ TransactionLike,
11
+ assert,
12
+ Provider,
13
+ TransactionResponse,
14
+ TransactionResponseParams,
15
+ Signature
16
+ } from 'ethers'
17
+ import { TransactionRequest } from 'ethers/src.ts/providers'
18
+ import { APIService, WalletDetail, WalletAddressType } from './api'
19
+ import { APICredentials, TransactionStatusResponse, TxStatus, TransactionError } from './types'
20
+ import { Environment } from './config'
21
+ import { StatusPoller, StatusPollerOptions } from './utils/statusPoller'
22
+
23
+ export class ApexSigner extends AbstractSigner {
24
+ private address!: string
25
+ private APICredentials!: APICredentials
26
+ private APIService: APIService
27
+ private walletDetail: WalletDetail
28
+ private environment: Environment
29
+ private pollerOptions?: StatusPollerOptions
30
+
31
+ constructor(
32
+ credentials: APICredentials,
33
+ environment: Environment,
34
+ provider?: null | Provider,
35
+ pollerOptions?: StatusPollerOptions
36
+ ) {
37
+ super(provider)
38
+
39
+ this.APICredentials = credentials
40
+ this.APIService = new APIService(credentials, environment)
41
+ this.environment = environment
42
+ this.pollerOptions = pollerOptions
43
+ }
44
+
45
+ setWallet(walletId: string): void {
46
+ if (!walletId || walletId.trim() === '') {
47
+ throw new Error('Invalid wallet ID provided')
48
+ }
49
+ // Set walletId in walletDetail with default values for required properties
50
+ this.walletDetail = {
51
+ WalletID: walletId,
52
+ APIKey: '',
53
+ Name: '',
54
+ AddressType: '',
55
+ Address: ''
56
+ // Other fields will be populated when getAddress is called
57
+ }
58
+ // Reset address cache since we're changing wallets
59
+ this.address = ''
60
+ }
61
+
62
+ async getAddress(): Promise<string> {
63
+ if (this.address) {
64
+ return this.address
65
+ }
66
+
67
+ if (!this.APICredentials.apiKey && !this.walletDetail.WalletID) {
68
+ throw new Error('Wallet detail not found, use setWallet(walletId) to set wallet first!')
69
+ }
70
+
71
+ const detail: WalletDetail = await this.APIService.getWalletDetail(
72
+ WalletAddressType.Evm,
73
+ this.walletDetail?.WalletID
74
+ )
75
+
76
+ this.walletDetail = detail
77
+ if (detail?.Address) {
78
+ // cache the address
79
+ this.address = detail.Address
80
+ }
81
+
82
+ if (!this.address) {
83
+ throw new Error('Address not found')
84
+ }
85
+
86
+ return this.address
87
+ }
88
+
89
+ private async getChainId(): Promise<number> {
90
+ if (!this.provider) {
91
+ throw new Error('Provider is required for signing operations')
92
+ }
93
+
94
+ try {
95
+ const network = await this.provider.getNetwork()
96
+ return Number(network.chainId)
97
+ } catch (error) {
98
+ throw new Error('Failed to get chainId from provider: ' + error)
99
+ }
100
+ }
101
+
102
+ connect(provider: null | Provider): ApexSigner {
103
+ return new ApexSigner(this.APICredentials, this.environment, provider)
104
+ }
105
+
106
+ private async waitForSignature(walletId: string, transactionId: string): Promise<string> {
107
+ const poller = new StatusPoller(this.pollerOptions)
108
+ const status = await poller.poll(
109
+ // Polling function
110
+ () => this.APIService.getSignStatus(walletId, transactionId),
111
+ // Success condition
112
+ (result) => result.status === TxStatus.Confirmed && result.signature != null,
113
+ // Error condition
114
+ (result) => [TxStatus.Failed, TxStatus.Rejected].includes(result.status)
115
+ )
116
+
117
+ if (!status.signature) {
118
+ throw new Error('Signature not found in successful response')
119
+ }
120
+
121
+ return status.signature
122
+ }
123
+
124
+ private async waitForTransactonStatus(transactionId: string): Promise<string> {
125
+ const poller = new StatusPoller(this.pollerOptions)
126
+ // Poll for transaction status using signaturePoller
127
+ const result = await poller.poll<TransactionStatusResponse>(
128
+ // Polling function
129
+ () => {
130
+ console.log('Polling status')
131
+ return this.APIService.getTransactionStatus(this.walletDetail.WalletID, transactionId)
132
+ },
133
+ // Success condition
134
+ (result) =>
135
+ (result.status === TxStatus.Confirmed || result.status === TxStatus.Completed) &&
136
+ !!result.hash,
137
+ // Error condition
138
+ (result) => {
139
+ if (result.status === TxStatus.Failed) {
140
+ throw new TransactionError(
141
+ result.failed_reason || 'Transaction failed',
142
+ 'TRANSACTION_FAILED',
143
+ result.transaction_id
144
+ )
145
+ }
146
+ return result.status === TxStatus.Rejected
147
+ }
148
+ )
149
+ console.log('reulst', result)
150
+
151
+ if (!result.hash) {
152
+ throw new TransactionError(
153
+ 'Transaction hash not found in successful response',
154
+ 'TRANSACTION_HASH_MISSING',
155
+ result.transaction_id
156
+ )
157
+ }
158
+
159
+ return result.hash
160
+ }
161
+
162
+ // Copied and editted from ethers.js -> Wallet -> BaseWallet
163
+ async signTransaction(tx: TransactionRequest): Promise<string> {
164
+ const startTime = new Date()
165
+ console.log(`[WalletSDK] Transaction started at: ${startTime.toLocaleString()}`)
166
+
167
+ // Replace any Addressable or ENS name with an address
168
+ const { to, from } = await resolveProperties({
169
+ to: tx.to ? resolveAddress(tx.to, this.provider) : undefined,
170
+ from: tx.from ? resolveAddress(tx.from, this.provider) : undefined
171
+ })
172
+
173
+ if (to != null) {
174
+ tx.to = to
175
+ }
176
+ if (from != null) {
177
+ tx.from = from
178
+ }
179
+
180
+ if (tx.from != null) {
181
+ assertArgument(
182
+ getAddress(<string>tx.from) === this.address,
183
+ 'transaction from address mismatch',
184
+ 'tx.from',
185
+ tx.from
186
+ )
187
+ delete tx.from
188
+ }
189
+
190
+ const fromAddress = this.address
191
+ // Build the transaction
192
+ const btx = Transaction.from(<TransactionLike<string>>tx)
193
+ const data = {
194
+ maxFeePerGas: btx.maxFeePerGas,
195
+ maxPriorityFeePerGas: btx.maxPriorityFeePerGas,
196
+ to: btx.to,
197
+ from: fromAddress,
198
+ nonce: btx.nonce,
199
+ gasLimit: btx.gasLimit,
200
+ data: btx.data,
201
+ value: btx.value,
202
+ chainId: btx.chainId,
203
+ accessList: btx.accessList
204
+ }
205
+ // return unseralized as API signTransaction is an asynchoronous action
206
+ const response = await this.APIService.signTransaction(this.walletDetail.WalletID, data)
207
+ const txHash = await this.waitForTransactonStatus(response.transaction_id)
208
+
209
+ const endTime = new Date()
210
+ const elapsedTimeMs = endTime.getTime() - startTime.getTime()
211
+ console.log(`[WalletSDK] Transaction completed at: ${endTime.toLocaleString()}`)
212
+ console.log(
213
+ `[WalletSDK] Transaction took ${elapsedTimeMs}ms (${(elapsedTimeMs / 1000).toFixed(2)}s)`
214
+ )
215
+ console.log('[WalletSDK] Transaction succeed!')
216
+
217
+ return txHash
218
+ }
219
+
220
+ async sendTransaction(tx: TransactionRequest): Promise<TransactionResponse> {
221
+ const startTime = new Date()
222
+ console.log(`[WalletSDK] sendTransaction started at: ${startTime.toLocaleString()}`)
223
+
224
+ if (!this.address) {
225
+ await this.getAddress()
226
+ }
227
+
228
+ checkProvider(this, 'sendTransaction')
229
+
230
+ // Only populate if gas fees are not set
231
+ const hasGasFees =
232
+ tx.gasPrice != null || (tx.maxFeePerGas != null && tx.maxPriorityFeePerGas != null)
233
+
234
+ let populatedTx = tx
235
+ if (!hasGasFees) {
236
+ const populateStartTime = new Date()
237
+ console.log(
238
+ `[WalletSDK] populateTransaction started at: ${populateStartTime.toLocaleString()}`
239
+ )
240
+ populatedTx = await this.populateTransaction(tx)
241
+ const populateEndTime = new Date()
242
+ const populateElapsedMs = populateEndTime.getTime() - populateStartTime.getTime()
243
+ console.log(
244
+ `[WalletSDK] populateTransaction completed in ${(populateElapsedMs / 1000).toFixed(2)}s`
245
+ )
246
+ } else {
247
+ console.log(`[WalletSDK] Skipping transaction population as gas fees are already set`)
248
+ }
249
+
250
+ delete populatedTx.from
251
+
252
+ // Ensure all properties are properly resolved to their string representations
253
+ const resolvedTx = (await resolveProperties(populatedTx)) as TransactionLike<string>
254
+ const txObj = Transaction.from(resolvedTx)
255
+
256
+ console.log('txObj', txObj)
257
+
258
+ const txHash = await this.signTransaction(txObj)
259
+
260
+ // Instead of creating a mock response, get the actual transaction from the provider
261
+ const endTime = new Date()
262
+ const totalElapsedMs = endTime.getTime() - startTime.getTime()
263
+ console.log(`[WalletSDK] sendTransaction completed at: ${endTime.toLocaleString()}`)
264
+ console.log(`[WalletSDK] sendTransaction took ${(totalElapsedMs / 1000).toFixed(2)}s`)
265
+ console.log('[WalletSDK] Transaction sent successfully!')
266
+
267
+ const txResponse: TransactionResponseParams = {
268
+ blockNumber: 0, // Default to 0 as this is an async transaction
269
+ blockHash: '0x0000000000000000000000000000000000000000000000000000000000000000', // not available yet
270
+ hash: txHash, // not available yet
271
+ index: 0,
272
+ type: 0,
273
+ to: tx.to as any,
274
+ from: this.address,
275
+ /**
276
+ *
277
+ */
278
+ nonce: txObj.nonce, // The nonce of the transaction, used for replay protection.
279
+ /**
280
+ * The maximum amount of gas this transaction is authorized to consume.
281
+ */
282
+ gasLimit: txObj.gasLimit,
283
+
284
+ /**
285
+ * For legacy transactions, this is the gas price per gas to pay.
286
+ */
287
+ gasPrice: txObj.gasPrice ? txObj.gasPrice : BigInt(0),
288
+
289
+ /**
290
+ * For [[link-eip-1559]] transactions, this is the maximum priority
291
+ * fee to allow a producer to claim.
292
+ */
293
+ maxPriorityFeePerGas: txObj.maxPriorityFeePerGas,
294
+
295
+ /**
296
+ * For [[link-eip-1559]] transactions, this is the maximum fee that
297
+ * will be paid.
298
+ */
299
+ maxFeePerGas: txObj.maxFeePerGas,
300
+
301
+ /**
302
+ * The transaction data.
303
+ */
304
+ data: txObj.data,
305
+
306
+ /**
307
+ * The transaction value (in wei).
308
+ */
309
+ value: txObj.value,
310
+
311
+ /**
312
+ * The chain ID this transaction is valid on.
313
+ */
314
+ chainId: txObj.chainId,
315
+
316
+ signature: Signature.from('0x' + '0'.repeat(130)), // length of signature is 65 bytes - 130 hex chars
317
+ /**
318
+ * The transaction access list.
319
+ */
320
+ accessList: txObj.accessList
321
+ }
322
+
323
+ // Let the provider create the TransactionResponse using the txHash
324
+ return new TransactionResponse(txResponse, this.provider as Provider)
325
+ }
326
+
327
+ async signMessage(message: string | Uint8Array): Promise<string> {
328
+ if (!this.provider) {
329
+ throw new Error('Provider is required for signing operations')
330
+ }
331
+
332
+ if (!this.address) {
333
+ await this.getAddress()
334
+ }
335
+
336
+ if (!this.walletDetail) {
337
+ this.walletDetail = await this.APIService.getWalletDetail()
338
+ }
339
+
340
+ const chainId = await this.getChainId()
341
+ const messageStr = typeof message === 'string' ? message : Buffer.from(message).toString('hex')
342
+
343
+ const response = await this.APIService.requestSign(this.walletDetail.WalletID, {
344
+ method: 'eth_sign',
345
+ message: messageStr,
346
+ chain_id: chainId
347
+ })
348
+
349
+ return this.waitForSignature(this.walletDetail.WalletID, response.transaction_id)
350
+ }
351
+
352
+ async signTypedData(
353
+ domain: TypedDataDomain,
354
+ types: Record<string, Array<TypedDataField>>,
355
+ value: Record<string, any>
356
+ ): Promise<string> {
357
+ if (!this.provider) {
358
+ throw new Error('Provider is required for signing operations')
359
+ }
360
+
361
+ if (!this.address) {
362
+ await this.getAddress()
363
+ }
364
+
365
+ if (!this.walletDetail) {
366
+ this.walletDetail = await this.APIService.getWalletDetail()
367
+ }
368
+
369
+ const chainId = await this.getChainId()
370
+ const typedData = JSON.stringify({
371
+ domain,
372
+ types,
373
+ message: value
374
+ })
375
+
376
+ const response = await this.APIService.requestSign(this.walletDetail.WalletID, {
377
+ method: 'eth_signTypedData_v4',
378
+ message: '',
379
+ chain_id: chainId,
380
+ typed_data: typedData
381
+ })
382
+
383
+ return this.waitForSignature(this.walletDetail.WalletID, response.transaction_id)
384
+ }
385
+ }
386
+
387
+ function checkProvider(signer: AbstractSigner, operation: string): Provider {
388
+ if (signer.provider) {
389
+ return signer.provider
390
+ }
391
+ assert(false, 'missing provider', 'UNSUPPORTED_OPERATION', { operation })
392
+ }