@lifi/perps-sdk-provider-ondo 0.0.0 → 1.0.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.
Files changed (88) hide show
  1. package/dist/cjs/OndoProvider.d.ts.map +1 -1
  2. package/dist/cjs/OndoProvider.js +22 -5
  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 +12 -1
  6. package/dist/cjs/accountConfig.js.map +1 -1
  7. package/dist/cjs/auth/OndoApiKeyStore.d.ts +13 -0
  8. package/dist/cjs/auth/OndoApiKeyStore.d.ts.map +1 -0
  9. package/dist/cjs/auth/OndoApiKeyStore.js +42 -0
  10. package/dist/cjs/auth/OndoApiKeyStore.js.map +1 -0
  11. package/dist/cjs/auth/hmac.d.ts +8 -0
  12. package/dist/cjs/auth/hmac.d.ts.map +1 -0
  13. package/dist/cjs/auth/hmac.js +14 -0
  14. package/dist/cjs/auth/hmac.js.map +1 -0
  15. package/dist/cjs/auth/index.d.ts +3 -1
  16. package/dist/cjs/auth/index.d.ts.map +1 -1
  17. package/dist/cjs/auth/index.js +5 -2
  18. package/dist/cjs/auth/index.js.map +1 -1
  19. package/dist/cjs/auth/signActions.d.ts +3 -2
  20. package/dist/cjs/auth/signActions.d.ts.map +1 -1
  21. package/dist/cjs/auth/signActions.js +66 -47
  22. package/dist/cjs/auth/signActions.js.map +1 -1
  23. package/dist/cjs/constants.d.ts +4 -0
  24. package/dist/cjs/constants.d.ts.map +1 -1
  25. package/dist/cjs/constants.js +5 -1
  26. package/dist/cjs/constants.js.map +1 -1
  27. package/dist/cjs/types/auth.d.ts +7 -0
  28. package/dist/cjs/types/auth.d.ts.map +1 -1
  29. package/dist/cjs/utils/apiClient.d.ts.map +1 -1
  30. package/dist/cjs/utils/apiClient.js.map +1 -1
  31. package/dist/esm/OndoProvider.d.ts.map +1 -1
  32. package/dist/esm/OndoProvider.js +26 -6
  33. package/dist/esm/OndoProvider.js.map +1 -1
  34. package/dist/esm/accountConfig.d.ts.map +1 -1
  35. package/dist/esm/accountConfig.js +16 -3
  36. package/dist/esm/accountConfig.js.map +1 -1
  37. package/dist/esm/auth/OndoApiKeyStore.d.ts +21 -0
  38. package/dist/esm/auth/OndoApiKeyStore.d.ts.map +1 -0
  39. package/dist/esm/auth/OndoApiKeyStore.js +49 -0
  40. package/dist/esm/auth/OndoApiKeyStore.js.map +1 -0
  41. package/dist/esm/auth/hmac.d.ts +20 -0
  42. package/dist/esm/auth/hmac.d.ts.map +1 -0
  43. package/dist/esm/auth/hmac.js +19 -0
  44. package/dist/esm/auth/hmac.js.map +1 -0
  45. package/dist/esm/auth/index.d.ts +3 -1
  46. package/dist/esm/auth/index.d.ts.map +1 -1
  47. package/dist/esm/auth/index.js +3 -1
  48. package/dist/esm/auth/index.js.map +1 -1
  49. package/dist/esm/auth/signActions.d.ts +14 -15
  50. package/dist/esm/auth/signActions.d.ts.map +1 -1
  51. package/dist/esm/auth/signActions.js +85 -60
  52. package/dist/esm/auth/signActions.js.map +1 -1
  53. package/dist/esm/constants.d.ts +24 -0
  54. package/dist/esm/constants.d.ts.map +1 -1
  55. package/dist/esm/constants.js +24 -0
  56. package/dist/esm/constants.js.map +1 -1
  57. package/dist/esm/types/auth.d.ts +16 -0
  58. package/dist/esm/types/auth.d.ts.map +1 -1
  59. package/dist/esm/utils/apiClient.d.ts +2 -4
  60. package/dist/esm/utils/apiClient.d.ts.map +1 -1
  61. package/dist/esm/utils/apiClient.js +2 -4
  62. package/dist/esm/utils/apiClient.js.map +1 -1
  63. package/dist/types/OndoProvider.d.ts.map +1 -1
  64. package/dist/types/accountConfig.d.ts.map +1 -1
  65. package/dist/types/auth/OndoApiKeyStore.d.ts +21 -0
  66. package/dist/types/auth/OndoApiKeyStore.d.ts.map +1 -0
  67. package/dist/types/auth/hmac.d.ts +20 -0
  68. package/dist/types/auth/hmac.d.ts.map +1 -0
  69. package/dist/types/auth/index.d.ts +3 -1
  70. package/dist/types/auth/index.d.ts.map +1 -1
  71. package/dist/types/auth/signActions.d.ts +14 -15
  72. package/dist/types/auth/signActions.d.ts.map +1 -1
  73. package/dist/types/constants.d.ts +24 -0
  74. package/dist/types/constants.d.ts.map +1 -1
  75. package/dist/types/types/auth.d.ts +16 -0
  76. package/dist/types/types/auth.d.ts.map +1 -1
  77. package/dist/types/utils/apiClient.d.ts +2 -4
  78. package/dist/types/utils/apiClient.d.ts.map +1 -1
  79. package/package.json +3 -3
  80. package/src/OndoProvider.ts +32 -17
  81. package/src/accountConfig.ts +18 -3
  82. package/src/auth/OndoApiKeyStore.ts +64 -0
  83. package/src/auth/hmac.ts +45 -0
  84. package/src/auth/index.ts +2 -1
  85. package/src/auth/signActions.ts +126 -77
  86. package/src/constants.ts +28 -0
  87. package/src/types/auth.ts +17 -0
  88. package/src/utils/apiClient.ts +2 -4
@@ -0,0 +1,64 @@
1
+ import { readValidatedRecord, type StorageAdapter } from '@lifi/perps-sdk'
2
+ import type { Address } from 'viem'
3
+ import type { OndoApiKey } from '../types/auth.js'
4
+
5
+ // Keys are namespaced by environment host as well as address: a production
6
+ // API key is meaningless against the sandbox, and both stores may share one
7
+ // localStorage.
8
+
9
+ const STORAGE_PREFIX = 'lifi-perps-ondo-apikey'
10
+
11
+ const isOndoApiKey = (value: unknown): value is OndoApiKey => {
12
+ if (typeof value !== 'object' || value === null) {
13
+ return false
14
+ }
15
+ const { keyId, apiSecret, name, createdAt, scopes } = value as Record<
16
+ string,
17
+ unknown
18
+ >
19
+ return (
20
+ typeof keyId === 'string' &&
21
+ keyId.length > 0 &&
22
+ typeof apiSecret === 'string' &&
23
+ apiSecret.length > 0 &&
24
+ typeof name === 'string' &&
25
+ typeof createdAt === 'string' &&
26
+ Array.isArray(scopes) &&
27
+ scopes.every((scope) => typeof scope === 'string')
28
+ )
29
+ }
30
+
31
+ /**
32
+ * Persists the Ondo trading API key per wallet address and environment via a
33
+ * `StorageAdapter`, mirroring {@link OndoTokenStore}. The record holds the
34
+ * `apiSecret` returned only at creation; a poisoned record reads back as absent
35
+ * (and is evicted), so callers can treat `null` uniformly as "mint a key".
36
+ *
37
+ * @public
38
+ */
39
+ export class OndoApiKeyStore {
40
+ private readonly storage: StorageAdapter
41
+ private readonly host: string
42
+
43
+ constructor(storage: StorageAdapter, baseUrl: string) {
44
+ this.storage = storage
45
+ this.host = new URL(baseUrl).host
46
+ }
47
+
48
+ private storageKey(address: Address): string {
49
+ return `${STORAGE_PREFIX}:${this.host}:${address.toLowerCase()}`
50
+ }
51
+
52
+ async get(address: Address): Promise<OndoApiKey | null> {
53
+ const key = this.storageKey(address)
54
+ return (await readValidatedRecord(this.storage, key, isOndoApiKey)) ?? null
55
+ }
56
+
57
+ async set(address: Address, apiKey: OndoApiKey): Promise<void> {
58
+ await this.storage.set(this.storageKey(address), JSON.stringify(apiKey))
59
+ }
60
+
61
+ async remove(address: Address): Promise<void> {
62
+ await this.storage.remove(this.storageKey(address))
63
+ }
64
+ }
@@ -0,0 +1,45 @@
1
+ /** @internal */
2
+ export interface OndoHmacRequest {
3
+ /** Milliseconds since epoch; Ondo enforces a 30-second signing window. */
4
+ timestampMs: number
5
+ method: string
6
+ /** Venue-relative path including any query string. */
7
+ pathWithQuery: string
8
+ /** Pre-serialized body, signed verbatim; empty for bodyless requests. */
9
+ body?: string
10
+ }
11
+
12
+ const encoder = new TextEncoder()
13
+
14
+ const toHex = (buffer: ArrayBuffer): string =>
15
+ Array.from(new Uint8Array(buffer))
16
+ .map((byte) => byte.toString(16).padStart(2, '0'))
17
+ .join('')
18
+
19
+ /**
20
+ * Compute Ondo's per-request HMAC-SHA256 signature, hex-encoded. The signed
21
+ * message concatenates timestamp, upper-cased method, path-with-query, and the
22
+ * verbatim body with no separators.
23
+ *
24
+ * @param apiSecret - the API key's secret, used as the HMAC key.
25
+ * @internal
26
+ */
27
+ export async function hmacSignRequest(
28
+ apiSecret: string,
29
+ request: OndoHmacRequest
30
+ ): Promise<string> {
31
+ const message = `${request.timestampMs}${request.method.toUpperCase()}${request.pathWithQuery}${request.body ?? ''}`
32
+ const key = await crypto.subtle.importKey(
33
+ 'raw',
34
+ encoder.encode(apiSecret),
35
+ { name: 'HMAC', hash: 'SHA-256' },
36
+ false,
37
+ ['sign']
38
+ )
39
+ const signature = await crypto.subtle.sign(
40
+ 'HMAC',
41
+ key,
42
+ encoder.encode(message)
43
+ )
44
+ return toHex(signature)
45
+ }
package/src/auth/index.ts CHANGED
@@ -4,9 +4,10 @@ export {
4
4
  completeSiweLogin,
5
5
  type OndoSiweChallenge,
6
6
  } from './completeSiweLogin.js'
7
+ export { hmacSignRequest, type OndoHmacRequest } from './hmac.js'
8
+ export { OndoApiKeyStore } from './OndoApiKeyStore.js'
7
9
  export { OndoTokenStore } from './OndoTokenStore.js'
8
10
  export {
9
- executeOndoRestCallActions,
10
11
  type OndoSignActionsDeps,
11
12
  ondoSignActions,
12
13
  } from './signActions.js'
@@ -1,43 +1,90 @@
1
1
  import { PerpsError, type SignActionsContext } from '@lifi/perps-sdk'
2
2
  import type {
3
- ActionResult,
4
3
  ActionStep,
5
- RestCallSignedActionStep,
4
+ HmacSignedActionStep,
5
+ SessionActionStep,
6
6
  SignedActionStep,
7
7
  SiweActionStep,
8
8
  } from '@lifi/perps-types'
9
- import { PerpsErrorCode, SigningMethod } from '@lifi/perps-types'
9
+ import { ActionType, PerpsErrorCode, SigningMethod } from '@lifi/perps-types'
10
10
  import type { Address } from 'viem'
11
+ import {
12
+ ONDO_API_KEY_NAME,
13
+ ONDO_API_KEY_SCOPES,
14
+ ONDO_PRIVACY_VERSION,
15
+ ONDO_TERMS_VERSION,
16
+ } from '../constants.js'
17
+ import type { OndoApiKey } from '../types/auth.js'
11
18
  import {
12
19
  type OndoApiClient,
13
- type OndoHttpMethod,
14
20
  OndoSessionExpiredError,
15
21
  } from '../utils/apiClient.js'
16
22
  import { completeSiweLogin } from './completeSiweLogin.js'
23
+ import { hmacSignRequest } from './hmac.js'
24
+ import type { OndoApiKeyStore } from './OndoApiKeyStore.js'
17
25
  import type { OndoTokenStore } from './OndoTokenStore.js'
18
26
 
19
27
  /** @internal */
20
28
  export interface OndoSignActionsDeps {
21
29
  client: OndoApiClient
22
30
  tokenStore: OndoTokenStore
31
+ apiKeyStore: OndoApiKeyStore
23
32
  }
24
33
 
25
34
  const isSiweStep = (step: ActionStep): step is SiweActionStep => 'siwe' in step
26
35
 
36
+ const isSessionStep = (step: ActionStep): step is SessionActionStep =>
37
+ 'session' in step
38
+
27
39
  const hasRequest = (
28
40
  step: ActionStep
29
41
  ): step is Extract<ActionStep, { request: unknown }> => 'request' in step
30
42
 
43
+ /**
44
+ * Fetch the stored trading API key, minting one on first use. Minting is
45
+ * JWT-authorized (`POST /v1/api_keys`); the returned record — including the
46
+ * `apiSecret` the venue reveals only once — is stored immediately. An absent
47
+ * session throws {@link OndoSessionExpiredError} so callers re-run SIWE login.
48
+ */
49
+ async function ensureApiKey(
50
+ deps: OndoSignActionsDeps,
51
+ address: Address
52
+ ): Promise<OndoApiKey> {
53
+ const existing = await deps.apiKeyStore.get(address)
54
+ if (existing !== null) {
55
+ return existing
56
+ }
57
+ const token = await deps.tokenStore.get(address)
58
+ if (token === null) {
59
+ throw new OndoSessionExpiredError(
60
+ `No valid Ondo session token stored for ${address}. Run the SIWE login first.`
61
+ )
62
+ }
63
+ const apiKey = await deps.client.post<OndoApiKey>(
64
+ '/v1/api_keys',
65
+ { name: ONDO_API_KEY_NAME, scopes: ONDO_API_KEY_SCOPES },
66
+ { authToken: token.token }
67
+ )
68
+ await deps.apiKeyStore.set(address, apiKey)
69
+ return apiKey
70
+ }
71
+
31
72
  /**
32
73
  * Ondo's `signActions` arms.
33
74
  *
34
- * `SIWE` signs the backend-built ERC-4361 challenge with the user's wallet
35
- * and completes the login directly against Ondo — the returned session JWT is
75
+ * `SIWE` signs the backend-built ERC-4361 challenge with the user's wallet and
76
+ * completes the login directly against Ondo — the returned session JWT is
36
77
  * persisted in the token store and never transits the LI.FI backend.
37
78
  *
38
- * `AUTH_TOKEN` attaches the stored session JWT as an `Authorization: Bearer`
39
- * header on each REST-call step; an absent or expired token throws
40
- * {@link OndoSessionExpiredError} so callers re-run the SIWE login.
79
+ * `SESSION` executes client-only setup steps directly against the venue with
80
+ * the stored session token, keyed on the step's action — the marker steps
81
+ * carry no request material by design. Returns no signed steps, so
82
+ * `executeAction` is skipped.
83
+ *
84
+ * `HMAC` computes a per-request HMAC-SHA256 signature over each request step
85
+ * from the client-held API key (minting one on first use), attaching the
86
+ * `hmac` material. The signed step rides the normal `executeAction` path; the
87
+ * API secret itself never leaves the client.
41
88
  *
42
89
  * @public
43
90
  */
@@ -77,84 +124,86 @@ export async function ondoSignActions(
77
124
  return signed
78
125
  }
79
126
 
80
- case SigningMethod.AUTH_TOKEN: {
81
- const token = await deps.tokenStore.get(address)
82
- if (token === null) {
83
- throw new OndoSessionExpiredError(
84
- `No valid Ondo session token stored for ${address}. Run the SIWE login first.`
85
- )
86
- }
87
- return steps.map((step): RestCallSignedActionStep => {
88
- if (!hasRequest(step)) {
127
+ case SigningMethod.HMAC: {
128
+ const apiKey = await ensureApiKey(deps, address)
129
+ return Promise.all(
130
+ steps.map(async (step): Promise<HmacSignedActionStep> => {
131
+ if (!hasRequest(step)) {
132
+ throw new PerpsError(
133
+ PerpsErrorCode.SDKError,
134
+ `Ondo received a step without a request ('${step.action}') under the hmac signing method.`
135
+ )
136
+ }
137
+ // Stamped immediately before executeAction; Ondo enforces a 30s window.
138
+ const timestampMs = Date.now()
139
+ const signature = await hmacSignRequest(apiKey.apiSecret, {
140
+ timestampMs,
141
+ method: step.request.method,
142
+ pathWithQuery: step.request.path,
143
+ body: step.request.body,
144
+ })
145
+ return {
146
+ action: step.action,
147
+ request: step.request,
148
+ hmac: {
149
+ keyId: apiKey.keyId,
150
+ timestampMs,
151
+ signature,
152
+ },
153
+ }
154
+ })
155
+ )
156
+ }
157
+
158
+ case SigningMethod.SESSION: {
159
+ for (const step of steps) {
160
+ if (!isSessionStep(step)) {
89
161
  throw new PerpsError(
90
162
  PerpsErrorCode.SDKError,
91
- `Ondo received a non-REST step ('${step.action}') under the AUTH_TOKEN signing method.`
163
+ `Ondo received a non-session step ('${step.action}') under the session signing method.`
92
164
  )
93
165
  }
94
- return {
95
- action: step.action,
96
- request: step.request,
97
- headers: { Authorization: `Bearer ${token.token}` },
166
+ switch (step.action) {
167
+ case ActionType.ACCEPT_PROVIDER_TERMS: {
168
+ const token = await deps.tokenStore.get(address)
169
+ if (token === null) {
170
+ throw new OndoSessionExpiredError(
171
+ `No valid Ondo session token stored for ${address}. Run the SIWE login first.`
172
+ )
173
+ }
174
+ await deps.client.post(
175
+ '/v1/agreement',
176
+ {
177
+ termsVersion: ONDO_TERMS_VERSION,
178
+ privacyVersion: ONDO_PRIVACY_VERSION,
179
+ },
180
+ { authToken: token.token }
181
+ )
182
+ if (token.newAccount) {
183
+ await deps.tokenStore.set(address, {
184
+ ...token,
185
+ newAccount: false,
186
+ })
187
+ }
188
+ break
189
+ }
190
+ case ActionType.REGISTER_API_KEY:
191
+ await ensureApiKey(deps, address)
192
+ break
193
+ default:
194
+ throw new PerpsError(
195
+ PerpsErrorCode.SDKError,
196
+ `Ondo has no session-step executor for action '${step.action}'.`
197
+ )
98
198
  }
99
- })
199
+ }
200
+ return []
100
201
  }
101
202
 
102
203
  default:
103
204
  throw new PerpsError(
104
205
  PerpsErrorCode.SDKError,
105
- `Ondo does not sign via '${method}'. Supported methods: siwe, authToken.`
206
+ `Ondo does not sign via '${method}'. Supported methods: siwe, hmac, session.`
106
207
  )
107
208
  }
108
209
  }
109
-
110
- /**
111
- * Execute credential-bearing REST-call steps directly against Ondo,
112
- * sequentially — later steps in a batch may depend on earlier ones (e.g.
113
- * leverage update before order placement), so after the first failure the
114
- * remainder is skipped rather than executed out of order.
115
- *
116
- * @public
117
- */
118
- export async function executeOndoRestCallActions(
119
- client: OndoApiClient,
120
- steps: RestCallSignedActionStep[]
121
- ): Promise<ActionResult[]> {
122
- const results: ActionResult[] = []
123
- let failed = false
124
- for (const step of steps) {
125
- if (failed) {
126
- results.push({
127
- action: step.action,
128
- success: false,
129
- error: 'Skipped: a preceding step in the batch failed.',
130
- })
131
- continue
132
- }
133
- try {
134
- const result = await client.send<unknown>(
135
- step.request.method as OndoHttpMethod,
136
- step.request.path,
137
- { body: step.request.body, headers: step.headers }
138
- )
139
- const orderId =
140
- typeof result === 'object' &&
141
- result !== null &&
142
- typeof (result as { orderId?: unknown }).orderId === 'string'
143
- ? (result as { orderId: string }).orderId
144
- : undefined
145
- results.push({
146
- action: step.action,
147
- success: true,
148
- ...(orderId === undefined ? {} : { orderId }),
149
- })
150
- } catch (err) {
151
- failed = true
152
- results.push({
153
- action: step.action,
154
- success: false,
155
- error: err instanceof Error ? err.message : String(err),
156
- })
157
- }
158
- }
159
- return results
160
- }
package/src/constants.ts CHANGED
@@ -36,3 +36,31 @@ export const ONDO_BASE_FEE_TIER = {
36
36
  maker: '0.0002',
37
37
  taker: '0.0005',
38
38
  }
39
+
40
+ /**
41
+ * Name applied to the trading API key the SDK mints on first trading use.
42
+ *
43
+ * @public
44
+ */
45
+ export const ONDO_API_KEY_NAME = 'lifi-perps'
46
+
47
+ /**
48
+ * Scopes requested when minting the trading API key.
49
+ *
50
+ * @public
51
+ */
52
+ export const ONDO_API_KEY_SCOPES = ['trade']
53
+
54
+ /**
55
+ * Terms-of-Service version accepted at first login. Venue-owned; current value.
56
+ *
57
+ * @public
58
+ */
59
+ export const ONDO_TERMS_VERSION = 1
60
+
61
+ /**
62
+ * Privacy-policy version accepted at first login. Venue-owned; current value.
63
+ *
64
+ * @public
65
+ */
66
+ export const ONDO_PRIVACY_VERSION = 1
package/src/types/auth.ts CHANGED
@@ -30,3 +30,20 @@ export interface OndoAuthToken {
30
30
  /** True when this login created the Ondo account. */
31
31
  newAccount: boolean
32
32
  }
33
+
34
+ /**
35
+ * An Ondo trading API key, as returned by `POST /v1/api_keys` and persisted
36
+ * client-side. `apiSecret` is the HMAC key returned only at creation, so the
37
+ * whole record is stored immediately and never re-fetched.
38
+ *
39
+ * @public
40
+ */
41
+ export interface OndoApiKey {
42
+ keyId: string
43
+ /** HMAC secret; returned only at creation and kept strictly userland. */
44
+ apiSecret: string
45
+ name: string
46
+ /** ISO-8601 creation timestamp, as returned by the venue. */
47
+ createdAt: string
48
+ scopes: string[]
49
+ }
@@ -146,10 +146,8 @@ export class OndoApiClient {
146
146
  }
147
147
 
148
148
  /**
149
- * Arbitrary-method request with caller-prebuilt headers — the execution
150
- * surface for credential-bearing `RestCallSignedActionStep`s, whose
151
- * `Authorization` header is attached by `signActions` from the token store.
152
- * Only GET is ever retried; writes are not idempotent.
149
+ * Arbitrary-method request with caller-prebuilt headers. Only GET is ever
150
+ * retried; writes are not idempotent.
153
151
  */
154
152
  async send<T>(
155
153
  method: OndoHttpMethod,