@lifi/perps-sdk-provider-ondo 20.1.0 → 20.2.1

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.
Files changed (81) hide show
  1. package/dist/cjs/OndoProvider.d.ts.map +1 -1
  2. package/dist/cjs/OndoProvider.js +32 -1
  3. package/dist/cjs/OndoProvider.js.map +1 -1
  4. package/dist/cjs/accountConfig.d.ts.map +1 -1
  5. package/dist/cjs/accountConfig.js +1 -0
  6. package/dist/cjs/accountConfig.js.map +1 -1
  7. package/dist/cjs/auth/OndoApiKeyStore.d.ts +1 -0
  8. package/dist/cjs/auth/OndoApiKeyStore.d.ts.map +1 -1
  9. package/dist/cjs/auth/OndoApiKeyStore.js +4 -1
  10. package/dist/cjs/auth/OndoApiKeyStore.js.map +1 -1
  11. package/dist/cjs/auth/signActions.d.ts +1 -1
  12. package/dist/cjs/auth/signActions.d.ts.map +1 -1
  13. package/dist/cjs/auth/signActions.js +58 -9
  14. package/dist/cjs/auth/signActions.js.map +1 -1
  15. package/dist/cjs/constants.d.ts +1 -0
  16. package/dist/cjs/constants.d.ts.map +1 -1
  17. package/dist/cjs/constants.js +3 -2
  18. package/dist/cjs/constants.js.map +1 -1
  19. package/dist/cjs/types/wire.d.ts +9 -1
  20. package/dist/cjs/types/wire.d.ts.map +1 -1
  21. package/dist/cjs/utils/apiClient.js +3 -3
  22. package/dist/cjs/utils/apiClient.js.map +1 -1
  23. package/dist/cjs/utils/mapActivity.d.ts.map +1 -1
  24. package/dist/cjs/utils/mapActivity.js +2 -0
  25. package/dist/cjs/utils/mapActivity.js.map +1 -1
  26. package/dist/cjs/utils/ondoErrorCode.d.ts +5 -1
  27. package/dist/cjs/utils/ondoErrorCode.d.ts.map +1 -1
  28. package/dist/cjs/utils/ondoErrorCode.js +48 -6
  29. package/dist/cjs/utils/ondoErrorCode.js.map +1 -1
  30. package/dist/esm/OndoProvider.d.ts.map +1 -1
  31. package/dist/esm/OndoProvider.js +35 -2
  32. package/dist/esm/OndoProvider.js.map +1 -1
  33. package/dist/esm/accountConfig.d.ts.map +1 -1
  34. package/dist/esm/accountConfig.js +1 -0
  35. package/dist/esm/accountConfig.js.map +1 -1
  36. package/dist/esm/auth/OndoApiKeyStore.d.ts +2 -0
  37. package/dist/esm/auth/OndoApiKeyStore.d.ts.map +1 -1
  38. package/dist/esm/auth/OndoApiKeyStore.js +3 -0
  39. package/dist/esm/auth/OndoApiKeyStore.js.map +1 -1
  40. package/dist/esm/auth/signActions.d.ts +1 -1
  41. package/dist/esm/auth/signActions.d.ts.map +1 -1
  42. package/dist/esm/auth/signActions.js +74 -15
  43. package/dist/esm/auth/signActions.js.map +1 -1
  44. package/dist/esm/constants.d.ts +8 -1
  45. package/dist/esm/constants.d.ts.map +1 -1
  46. package/dist/esm/constants.js +9 -2
  47. package/dist/esm/constants.js.map +1 -1
  48. package/dist/esm/types/wire.d.ts +20 -2
  49. package/dist/esm/types/wire.d.ts.map +1 -1
  50. package/dist/esm/utils/apiClient.js +4 -4
  51. package/dist/esm/utils/apiClient.js.map +1 -1
  52. package/dist/esm/utils/mapActivity.d.ts.map +1 -1
  53. package/dist/esm/utils/mapActivity.js +2 -0
  54. package/dist/esm/utils/mapActivity.js.map +1 -1
  55. package/dist/esm/utils/ondoErrorCode.d.ts +11 -3
  56. package/dist/esm/utils/ondoErrorCode.d.ts.map +1 -1
  57. package/dist/esm/utils/ondoErrorCode.js +48 -6
  58. package/dist/esm/utils/ondoErrorCode.js.map +1 -1
  59. package/dist/types/OndoProvider.d.ts.map +1 -1
  60. package/dist/types/accountConfig.d.ts.map +1 -1
  61. package/dist/types/auth/OndoApiKeyStore.d.ts +2 -0
  62. package/dist/types/auth/OndoApiKeyStore.d.ts.map +1 -1
  63. package/dist/types/auth/signActions.d.ts +1 -1
  64. package/dist/types/auth/signActions.d.ts.map +1 -1
  65. package/dist/types/constants.d.ts +8 -1
  66. package/dist/types/constants.d.ts.map +1 -1
  67. package/dist/types/types/wire.d.ts +20 -2
  68. package/dist/types/types/wire.d.ts.map +1 -1
  69. package/dist/types/utils/mapActivity.d.ts.map +1 -1
  70. package/dist/types/utils/ondoErrorCode.d.ts +11 -3
  71. package/dist/types/utils/ondoErrorCode.d.ts.map +1 -1
  72. package/package.json +3 -3
  73. package/src/OndoProvider.ts +57 -3
  74. package/src/accountConfig.ts +1 -0
  75. package/src/auth/OndoApiKeyStore.ts +5 -0
  76. package/src/auth/signActions.ts +113 -18
  77. package/src/constants.ts +10 -2
  78. package/src/types/wire.ts +23 -1
  79. package/src/utils/apiClient.ts +5 -5
  80. package/src/utils/mapActivity.ts +2 -0
  81. package/src/utils/ondoErrorCode.ts +62 -8
@@ -94,6 +94,7 @@ function projectOndoDescriptor(
94
94
  case ActionType.META_ACCEPT_TERMS:
95
95
  case ActionType.META_ONBOARD:
96
96
  case ActionType.META_CREATE_REFERRAL_CODE:
97
+ case ActionType.ADD_WITHDRAWAL_ADDRESS:
97
98
  case ActionType.SYNC_FEE_ATTRIBUTION:
98
99
  throw new PerpsError(
99
100
  PerpsErrorCode.SDKError,
@@ -1,5 +1,6 @@
1
1
  import { readValidatedRecord, type StorageAdapter } from '@lifi/perps-sdk'
2
2
  import type { Address } from 'viem'
3
+ import { ONDO_API_KEY_SCOPES } from '../constants.js'
3
4
  import type { OndoApiKey } from '../types/auth.js'
4
5
 
5
6
  // Keys are namespaced by environment host as well as address: a production
@@ -29,6 +30,10 @@ export const isOndoApiKey = (value: unknown): value is OndoApiKey => {
29
30
  )
30
31
  }
31
32
 
33
+ /** @internal */
34
+ export const hasOndoApiKeyScopes = (apiKey: OndoApiKey): boolean =>
35
+ ONDO_API_KEY_SCOPES.every((scope) => apiKey.scopes.includes(scope))
36
+
32
37
  /**
33
38
  * Persists the Ondo trading API key per wallet address and environment via a
34
39
  * `StorageAdapter`, mirroring {@link OndoTokenStore}. The record holds the
@@ -8,12 +8,13 @@ import type {
8
8
  SiweActionStep,
9
9
  } from '@lifi/perps-types'
10
10
  import { ActionType, PerpsErrorCode, SigningMethod } from '@lifi/perps-types'
11
- import type { Address } from 'viem'
11
+ import { type Address, getAddress } from 'viem'
12
12
  import {
13
13
  ONDO_API_KEY_NAME,
14
14
  ONDO_API_KEY_SCOPES,
15
15
  ONDO_PRIVACY_VERSION,
16
16
  ONDO_TERMS_VERSION,
17
+ ONDO_WITHDRAWAL_ADDRESS_LABEL,
17
18
  } from '../constants.js'
18
19
  import type { OndoApiKey } from '../types/auth.js'
19
20
  import type { OndoApiKeyInfo, OndoCreatedApiKey } from '../types/wire.js'
@@ -26,9 +27,16 @@ import {
26
27
  buildOndoProvisionPayload,
27
28
  listOndoDepositAddress,
28
29
  } from '../utils/depositAddress.js'
29
- import { completeSiweLogin } from './completeSiweLogin.js'
30
+ import {
31
+ completeSiweLogin,
32
+ type OndoSiweChallenge,
33
+ } from './completeSiweLogin.js'
30
34
  import { hmacSignRequest } from './hmac.js'
31
- import { isOndoApiKey, type OndoApiKeyStore } from './OndoApiKeyStore.js'
35
+ import {
36
+ hasOndoApiKeyScopes,
37
+ isOndoApiKey,
38
+ type OndoApiKeyStore,
39
+ } from './OndoApiKeyStore.js'
32
40
  import type { OndoTokenStore } from './OndoTokenStore.js'
33
41
 
34
42
  /** @internal */
@@ -45,6 +53,18 @@ const isSiweStep = (step: ActionStep): step is SiweActionStep => 'siwe' in step
45
53
  const isSessionStep = (step: ActionStep): step is SessionActionStep =>
46
54
  'session' in step
47
55
 
56
+ const requireUserWallet = (ctx: SignActionsContext | undefined) => {
57
+ const userWallet = ctx?.userWallet
58
+ if (userWallet === undefined) {
59
+ throw new PerpsError(
60
+ PerpsErrorCode.SDKError,
61
+ 'Ondo SIWE login requires the user wallet. Pass `userWallet` when ' +
62
+ 'creating the perps client.'
63
+ )
64
+ }
65
+ return userWallet
66
+ }
67
+
48
68
  const hasRequest = (
49
69
  step: ActionStep
50
70
  ): step is Extract<ActionStep, { request: unknown }> => 'request' in step
@@ -64,7 +84,7 @@ function toStoredApiKey(created: OndoCreatedApiKey): OndoApiKey {
64
84
  createdAt: created.createdAt,
65
85
  scopes: created.scopes,
66
86
  }
67
- if (!isOndoApiKey(record)) {
87
+ if (!isOndoApiKey(record) || !hasOndoApiKeyScopes(record)) {
68
88
  throw new OndoApiError(
69
89
  `Ondo POST /v1/api_keys returned an unusable key record: ${JSON.stringify({ ...created, secretKey: '[REDACTED]' }).slice(0, 200)}`
70
90
  )
@@ -72,17 +92,45 @@ function toStoredApiKey(created: OndoCreatedApiKey): OndoApiKey {
72
92
  return record
73
93
  }
74
94
 
95
+ const pendingApiKeys = new WeakMap<
96
+ OndoApiKeyStore,
97
+ Map<string, Promise<OndoApiKey>>
98
+ >()
99
+
75
100
  /**
76
- * Fetch the stored trading API key. When no valid key exists, list the venue
77
- * registry and reclaim the oldest LI.FI-owned slot if the account is at
78
- * capacity. The replacement secret is stored immediately after creation.
101
+ * Fetch the stored trading API key, or create one. Concurrent calls for one
102
+ * address share one creation, so the venue never holds a duplicate key.
79
103
  */
80
- async function ensureApiKey(
104
+ function ensureApiKey(
105
+ deps: OndoSignActionsDeps,
106
+ address: Address
107
+ ): Promise<OndoApiKey> {
108
+ const pending = pendingApiKeys.get(deps.apiKeyStore) ?? new Map()
109
+ pendingApiKeys.set(deps.apiKeyStore, pending)
110
+ const pendingKey = address.toLowerCase()
111
+ const inFlight = pending.get(pendingKey)
112
+ if (inFlight !== undefined) {
113
+ return inFlight
114
+ }
115
+ const request = resolveApiKey(deps, address).finally(() => {
116
+ pending.delete(pendingKey)
117
+ })
118
+ pending.set(pendingKey, request)
119
+ return request
120
+ }
121
+
122
+ /**
123
+ * Return the stored key when it holds every required scope. Otherwise list
124
+ * the venue registry, revoke the stored key when the venue still lists it,
125
+ * and reclaim the oldest LI.FI-owned slot if the account is at capacity. The
126
+ * replacement secret is stored immediately after creation.
127
+ */
128
+ async function resolveApiKey(
81
129
  deps: OndoSignActionsDeps,
82
130
  address: Address
83
131
  ): Promise<OndoApiKey> {
84
132
  const existing = await deps.apiKeyStore.get(address)
85
- if (existing !== null) {
133
+ if (existing !== null && hasOndoApiKeyScopes(existing)) {
86
134
  return existing
87
135
  }
88
136
  const token = await deps.tokenStore.get(address)
@@ -91,10 +139,22 @@ async function ensureApiKey(
91
139
  `No valid Ondo session token stored for ${address}. Run the SIWE login first.`
92
140
  )
93
141
  }
94
- const configuredKeys =
142
+ const listedKeys =
95
143
  (await deps.client.get<OndoApiKeyInfo[] | null>('/v1/api_keys', {
96
144
  authToken: token.token,
97
145
  })) ?? []
146
+ const superseded =
147
+ existing === null
148
+ ? undefined
149
+ : listedKeys.find((key) => key.keyId === existing.keyId)
150
+ if (superseded !== undefined) {
151
+ await deps.client.send(
152
+ 'DELETE',
153
+ `/v1/api_keys/${encodeURIComponent(superseded.keyId)}`,
154
+ { authToken: token.token }
155
+ )
156
+ }
157
+ const configuredKeys = listedKeys.filter((key) => key !== superseded)
98
158
  if (configuredKeys.length >= ONDO_API_KEY_LIMIT) {
99
159
  let staleKey: OndoApiKeyInfo | undefined
100
160
  let staleKeyCreatedAtMs = Number.POSITIVE_INFINITY
@@ -139,6 +199,45 @@ async function ensureApiKey(
139
199
  return apiKey
140
200
  }
141
201
 
202
+ /**
203
+ * Add the login address to the Ondo address book, the only withdrawal
204
+ * destination the SDK offers. Ondo requires a wallet signature over its own
205
+ * challenge for each new entry.
206
+ */
207
+ async function addWithdrawalAddress(
208
+ deps: OndoSignActionsDeps,
209
+ address: Address,
210
+ ctx: SignActionsContext | undefined
211
+ ): Promise<void> {
212
+ const token = await deps.tokenStore.get(address)
213
+ if (token === null) {
214
+ throw new OndoSessionExpiredError(
215
+ `No valid Ondo session token stored for ${address}. Run the SIWE login first.`
216
+ )
217
+ }
218
+ const userWallet = requireUserWallet(ctx)
219
+ const withdrawalAddress = getAddress(address)
220
+ const challenge = await deps.client.post<OndoSiweChallenge>(
221
+ '/v1/auth/erc-4361/address_book/get_challenge',
222
+ { walletAddress: withdrawalAddress, chainId: '1', withdrawalAddress },
223
+ { authToken: token.token }
224
+ )
225
+ const signature = await userWallet.signMessage({
226
+ account: userWallet.account,
227
+ message: challenge.message,
228
+ })
229
+ await deps.client.post(
230
+ '/v1/auth/erc-4361/address_book/complete_challenge',
231
+ {
232
+ id: challenge.id,
233
+ signature,
234
+ withdrawalAddress,
235
+ addressLabel: ONDO_WITHDRAWAL_ADDRESS_LABEL,
236
+ },
237
+ { authToken: token.token }
238
+ )
239
+ }
240
+
142
241
  /**
143
242
  * Execute a backend-authored session request directly against the venue with
144
243
  * the stored session JWT. The pre-serialized wire body is parsed back to an
@@ -191,14 +290,7 @@ export async function ondoSignActions(
191
290
  ): Promise<SignedActionStep[]> {
192
291
  switch (method) {
193
292
  case SigningMethod.SIWE: {
194
- const userWallet = ctx?.userWallet
195
- if (userWallet === undefined) {
196
- throw new PerpsError(
197
- PerpsErrorCode.SDKError,
198
- 'Ondo SIWE login requires the user wallet. Pass `userWallet` when ' +
199
- 'creating the perps client.'
200
- )
201
- }
293
+ const userWallet = requireUserWallet(ctx)
202
294
  for (const step of steps) {
203
295
  if (!isSiweStep(step)) {
204
296
  throw new PerpsError(
@@ -303,6 +395,9 @@ export async function ondoSignActions(
303
395
  case ActionType.REGISTER_API_KEY:
304
396
  await ensureApiKey(deps, address)
305
397
  break
398
+ case ActionType.ADD_WITHDRAWAL_ADDRESS:
399
+ await addWithdrawalAddress(deps, address, ctx)
400
+ break
306
401
  default:
307
402
  throw new PerpsError(
308
403
  PerpsErrorCode.SDKError,
package/src/constants.ts CHANGED
@@ -45,11 +45,12 @@ export const ONDO_BASE_FEE_TIER = {
45
45
  export const ONDO_API_KEY_NAME = 'lifi-perps'
46
46
 
47
47
  /**
48
- * Scopes requested when minting the trading API key.
48
+ * Scopes requested when creating the trading API key. `POST /v1/withdraw`
49
+ * needs `transfer`; the SDK replaces a stored key without every scope.
49
50
  *
50
51
  * @public
51
52
  */
52
- export const ONDO_API_KEY_SCOPES = ['trade']
53
+ export const ONDO_API_KEY_SCOPES = ['trade', 'transfer']
53
54
 
54
55
  /**
55
56
  * Terms-of-Service version accepted at first login. Venue-owned; current value.
@@ -64,3 +65,10 @@ export const ONDO_TERMS_VERSION = 1
64
65
  * @public
65
66
  */
66
67
  export const ONDO_PRIVACY_VERSION = 1
68
+
69
+ /**
70
+ * Label Ondo shows on the address book entry the SDK adds for withdrawals.
71
+ *
72
+ * @public
73
+ */
74
+ export const ONDO_WITHDRAWAL_ADDRESS_LABEL = 'LI.FI'
package/src/types/wire.ts CHANGED
@@ -308,8 +308,14 @@ export interface OndoWalletDeposit {
308
308
  logIndex?: string
309
309
  }
310
310
 
311
- /** @public */
311
+ /**
312
+ * The REST spec lists the lowercase values. The live API sends the
313
+ * `WITHDRAWAL_`-prefixed values.
314
+ * @public
315
+ */
312
316
  export type OndoWithdrawalStatus =
317
+ | 'WITHDRAWAL_PENDING'
318
+ | 'WITHDRAWAL_SUCCESS'
313
319
  | 'complete'
314
320
  | 'failure'
315
321
  | 'pending'
@@ -337,6 +343,22 @@ export interface OndoWalletWithdrawal {
337
343
  from?: OndoAccountWalletKey
338
344
  }
339
345
 
346
+ /** Mirrors Ondo's `AddressBookEntry` (one `GET /v1/wallet/address_book` row). @public */
347
+ export interface OndoAddressBookEntry {
348
+ withdrawalAddress: string
349
+ label: string
350
+ lastUpdated: string
351
+ }
352
+
353
+ /**
354
+ * Mirrors Ondo's `AddressBookResult`. The venue sends `null` for an empty
355
+ * address book.
356
+ * @public
357
+ */
358
+ export interface OndoAddressBookResult {
359
+ addressBook: OndoAddressBookEntry[] | null
360
+ }
361
+
340
362
  /**
341
363
  * One side of an Ondo value movement: an account id plus which of that
342
364
  * account's two wallets held the value.
@@ -8,7 +8,7 @@ import {
8
8
  import { PerpsErrorCode } from '@lifi/perps-types'
9
9
  import type { OndoGenericResponse } from '../types/auth.js'
10
10
  import type { OndoPageInfo } from '../types/wire.js'
11
- import { ondoErrorCodeFromBody } from './ondoErrorCode.js'
11
+ import { ondoErrorFromBody } from './ondoErrorCode.js'
12
12
 
13
13
  /** @internal */
14
14
  export type ApiParams = Record<string, string | number | boolean>
@@ -246,12 +246,12 @@ export class OndoApiClient {
246
246
  )
247
247
  }
248
248
  if (isGenericResponse(data) && !data.success) {
249
- const bodyCode = ondoErrorCodeFromBody(data.error_code)
250
- if (bodyCode !== undefined) {
249
+ const bodyError = ondoErrorFromBody(data.error_code)
250
+ if (bodyError !== undefined) {
251
251
  throw new OndoApiError(
252
- `Ondo API error for ${path}: ${data.error_code} — ${data.error ?? 'no error message'}`,
252
+ `Ondo API error for ${path}: ${data.error_code} — ${bodyError.message ?? data.error ?? 'no error message'}`,
253
253
  data.error_code,
254
- bodyCode
254
+ bodyError.code
255
255
  )
256
256
  }
257
257
  }
@@ -137,6 +137,8 @@ export const mapDepositActivity = (
137
137
  }
138
138
 
139
139
  const SETTLING_WITHDRAWAL_STATUSES = new Set<string>([
140
+ 'WITHDRAWAL_PENDING',
141
+ 'WITHDRAWAL_SUCCESS',
140
142
  'complete',
141
143
  'pending',
142
144
  'unknown',
@@ -1,16 +1,70 @@
1
1
  import { PerpsErrorCode } from '@lifi/perps-types'
2
2
 
3
+ /**
4
+ * The classification of an Ondo envelope `error_code`. `message` replaces the
5
+ * venue's own text when that text does not tell the caller what to do.
6
+ */
7
+ export interface OndoBodyError {
8
+ code: PerpsErrorCode
9
+ message?: string
10
+ }
11
+
3
12
  /** Ondo envelope `error_code` values that name a rejection a `PerpsErrorCode` describes. */
4
- const ONDO_BODY_ERROR_CODES: ReadonlyMap<string, PerpsErrorCode> = new Map([
5
- ['insufficient_margin', PerpsErrorCode.InsufficientMargin],
6
- ['clientOrderID_collision', PerpsErrorCode.NonceAlreadyUsed],
13
+ const ONDO_BODY_ERRORS: ReadonlyMap<string, OndoBodyError> = new Map([
14
+ ['insufficient_margin', { code: PerpsErrorCode.InsufficientMargin }],
15
+ ['clientOrderID_collision', { code: PerpsErrorCode.NonceAlreadyUsed }],
16
+ [
17
+ 'bad_label',
18
+ {
19
+ code: PerpsErrorCode.ValidationError,
20
+ message: 'Ondo rejected the withdrawal address label.',
21
+ },
22
+ ],
23
+ [
24
+ 'empty_label',
25
+ {
26
+ code: PerpsErrorCode.ValidationError,
27
+ message: 'Ondo requires a withdrawal address label.',
28
+ },
29
+ ],
30
+ [
31
+ 'bad_withdrawal_address',
32
+ {
33
+ code: PerpsErrorCode.ValidationError,
34
+ message: 'Ondo rejected the withdrawal address as not valid.',
35
+ },
36
+ ],
37
+ [
38
+ 'internal_withdrawal_address',
39
+ {
40
+ code: PerpsErrorCode.ValidationError,
41
+ message:
42
+ 'Ondo does not accept an Ondo deposit address as a withdrawal address.',
43
+ },
44
+ ],
45
+ [
46
+ 'challenge_not_found',
47
+ {
48
+ code: PerpsErrorCode.NonceExpired,
49
+ message:
50
+ 'The Ondo address book challenge is unknown or expired. Request a new challenge.',
51
+ },
52
+ ],
53
+ [
54
+ 'withdrawal_address_not_found',
55
+ {
56
+ code: PerpsErrorCode.SetupRequired,
57
+ message:
58
+ 'The withdrawal destination is not in the Ondo address book. Add the withdrawal address first.',
59
+ },
60
+ ],
7
61
  ])
8
62
 
9
63
  /**
10
- * Maps an Ondo envelope `error_code` to the `PerpsErrorCode` it names. Returns
11
- * `undefined` for every other code so the status-driven layer decides.
64
+ * Classifies an Ondo envelope `error_code`. Returns `undefined` for every other
65
+ * code so the status-driven layer decides.
12
66
  */
13
- export const ondoErrorCodeFromBody = (
67
+ export const ondoErrorFromBody = (
14
68
  errorCode: string | undefined
15
- ): PerpsErrorCode | undefined =>
16
- errorCode === undefined ? undefined : ONDO_BODY_ERROR_CODES.get(errorCode)
69
+ ): OndoBodyError | undefined =>
70
+ errorCode === undefined ? undefined : ONDO_BODY_ERRORS.get(errorCode)