infra-sdk-typescript 0.1.2 → 0.2.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/README.md CHANGED
@@ -1,249 +1,256 @@
1
- # infra-sdk-typescript
2
-
3
- Official TypeScript SDK for the [Convexity Infra API](https://docs.withconvexity.com) —
4
- **Wallet**, **Tokenization**, and **Blockchain Events (Indexer)**.
5
-
6
- - Fully typed — every request and response shape is modeled, including the three
7
- different pagination envelopes the API uses.
8
- - Handles auth for you — exchange your API key once; the client mints, caches, and
9
- refreshes OAuth bearer tokens automatically.
10
- - Automatic retries with backoff for rate limits (`429`, honoring `retryAfter`) and
11
- upstream outages (`503`), plus safe retries on connection failures for idempotent
12
- requests.
13
- - Auto-generated `Idempotency-Key` headers on value-moving wallet operations, with the
14
- option to supply your own.
15
- - A typed error hierarchy (`RateLimitError`, `NotFoundError`, `PaymentRequiredError`, …)
16
- instead of parsing status codes yourself.
17
- - Webhook signature verification for both the Wallet outcome webhook and Indexer event
18
- deliveries.
19
- - Zero runtime dependencies — built on native `fetch` and `node:crypto`. Ships dual
20
- ESM/CJS builds with full `.d.ts` types.
21
-
22
- ## Requirements
23
-
24
- - Node.js 18 or later.
25
- - A Convexity Infra API key. There's no self-service signup — email
26
- [infra@withconvexity.com](mailto:infra@withconvexity.com) with your business details to
27
- get a test key (`sk_test_...`) and a live key (`sk_live_...`).
28
-
29
- ## Install
30
-
31
- ```bash
32
- npm install infra-sdk-typescript
33
- ```
34
-
35
- ## Quickstart
36
-
37
- ```ts
38
- import { InfraClient } from "infra-sdk-typescript";
39
-
40
- const client = new InfraClient({ apiKey: process.env.INFRA_API_KEY! });
41
-
42
- const balance = await client.wallet.getBalance();
43
- console.log(`Business balance: $${balance.balance}`);
44
- ```
45
-
46
- The client exchanges your API key for a short-lived OAuth bearer token on first use
47
- (`POST /v1/oauth/token`), caches it, and refreshes it before it expires (and once, on an
48
- unexpected `401`) — you never handle tokens directly.
49
-
50
- More end-to-end examples live in [`examples/`](./examples):
51
-
52
- - [`basic-usage.ts`](./examples/basic-usage.ts) — derive a wallet, transfer, poll for completion.
53
- - [`pagination.ts`](./examples/pagination.ts) — the three ways to consume a paginated list.
54
- - [`tokenization-yield-lifecycle.ts`](./examples/tokenization-yield-lifecycle.ts) — issue a
55
- yield-bearing token, mint, pay a coupon, redeem principal.
56
- - [`indexer-subscription.ts`](./examples/indexer-subscription.ts) — subscribe to on-chain
57
- events and manage the subscription.
58
- - [`webhook-verification.ts`](./examples/webhook-verification.ts) — verify both webhook
59
- signature schemes in a plain `node:http` server.
60
-
61
- ## Resources
62
-
63
- The client exposes one namespace per service:
64
-
65
- | Namespace | Service | Docs |
66
- |---|---|---|
67
- | `client.wallet` | HD wallets, on-chain transfers, sponsored signing, USD gas balance | [Wallet overview](https://docs.withconvexity.com/api-reference/wallet/overview) |
68
- | `client.tokenization` | Issue, mint/burn/transfer tokenized assets; yield lifecycle | [Tokenization overview](https://docs.withconvexity.com/api-reference/tokenization/overview) |
69
- | `client.indexer` | Subscribe to decoded on-chain events, delivered to your webhook | [Blockchain Events overview](https://docs.withconvexity.com/api-reference/indexer/overview) |
70
-
71
- Every method mirrors the API 1:1 (see each resource's JSDoc, or the type definitions, for
72
- the full parameter/return shape). A few highlights:
73
-
74
- ### Async transfers
75
-
76
- [`initiateTransfer`](./src/resources/wallet/index.ts) and
77
- [`signTransaction`](./src/resources/wallet/index.ts) are asynchronous by default: they
78
- return a `PENDING` transaction immediately (HTTP `202`), and the on-chain execution
79
- continues in the background.
80
-
81
- ```ts
82
- const transfer = await client.wallet.initiateTransfer({
83
- fromAddress: 12, // HD addressIndex, or a full "0x..." address
84
- toAddress: "0x2222222222222222222222222222222222222222",
85
- amount: "1000000",
86
- chainType: "EVM",
87
- chainId: 8453,
88
- });
89
-
90
- // Poll until it settles...
91
- let final = await client.wallet.getTransaction(transfer.id);
92
- while (final.status === "PENDING" || final.status === "PROCESSING") {
93
- await new Promise((r) => setTimeout(r, 2000));
94
- final = await client.wallet.getTransaction(transfer.id);
95
- }
96
-
97
- // ...or receive the outcome at a webhook instead, by passing webhookUrl/webhookSecret
98
- // on the initiate call, and verifying deliveries with `verifyWalletWebhookSignature`.
99
- ```
100
-
101
- Pass `waitForCompletion: true` for the legacy synchronous behavior, which holds the
102
- request open for the settled result instead.
103
-
104
- ### Pagination
105
-
106
- The API uses three different pagination envelopes depending on the service. This SDK
107
- normalizes all of them into one `Page<T>` type:
108
-
109
- ```ts
110
- // Lazily iterate every item, fetching subsequent pages as needed:
111
- for await (const token of await client.tokenization.listTokens()) {
112
- console.log(token.ticker);
113
- }
114
-
115
- // Or walk page by page:
116
- let page = await client.wallet.getTransactionHistory({ status: "COMPLETED" });
117
- for (;;) {
118
- for (const tx of page.data) handle(tx);
119
- if (!page.hasNextPage()) break;
120
- page = await page.getNextPage();
121
- }
122
-
123
- // Or eagerly collect everything (fine for small collections):
124
- const holders = await (await client.tokenization.getTokenHolders({ tokenId })).toArray();
125
- ```
126
-
127
- ### Idempotency
128
-
129
- Wallet's value-moving endpoints (`initiateTransfer`, `signTransaction`, `generateWallets`)
130
- require an `Idempotency-Key` header. The SDK generates a UUIDv4 for you automatically; pass
131
- your own to control retries across separate SDK calls (e.g. after a process restart):
132
-
133
- ```ts
134
- await client.wallet.initiateTransfer(params, { idempotencyKey: "my-own-key" });
135
- ```
136
-
137
- ### Errors
138
-
139
- Every failure is a subclass of `InfraError`. HTTP failures are subclasses of
140
- `InfraAPIError`, matching the API's status code table:
141
-
142
- ```ts
143
- import { RateLimitError, PaymentRequiredError, UnprocessableEntityError, InfraAPIError } from "infra-sdk-typescript";
144
-
145
- try {
146
- await client.wallet.initiateTransfer(params);
147
- } catch (err) {
148
- if (err instanceof RateLimitError) {
149
- console.log(`Throttled — retry after ${err.retryAfterSeconds}s`);
150
- } else if (err instanceof PaymentRequiredError) {
151
- console.log("Insufficient gas balance");
152
- } else if (err instanceof UnprocessableEntityError) {
153
- // waitForCompletion: true transfers that revert on-chain still carry the failed
154
- // transaction — read it off the raw response body if you need the details.
155
- console.log((err.body as { data?: unknown }).data);
156
- } else if (err instanceof InfraAPIError) {
157
- console.log(err.status, err.message, err.errors);
158
- }
159
- }
160
- ```
161
-
162
- `429` and `503` responses are retried automatically (2 retries by default); everything
163
- else is thrown immediately since retrying without changing the request won't help.
164
-
165
- ### Retries, timeouts, and connection safety
166
-
167
- - `429` and `503` are retried automatically — the server didn't execute the request, so
168
- this is always safe. `429` retries honor the response's `retryAfter`.
169
- - A connection failure (timeout, DNS, TLS, aborted socket) is ambiguous — the request may
170
- have already run server-side. The SDK only retries these for `GET` requests or requests
171
- carrying an `Idempotency-Key`, where a retry is provably safe.
172
- - Configure globally via the constructor, or per call:
173
-
174
- ```ts
175
- const client = new InfraClient({
176
- apiKey: process.env.INFRA_API_KEY!,
177
- timeoutMs: 30_000, // default
178
- maxRetries: 2, // default
179
- });
180
-
181
- await client.wallet.getBalance({ timeoutMs: 5_000, signal: myAbortController.signal });
182
- ```
183
-
184
- ### Webhooks
185
-
186
- ```ts
187
- import { verifyWalletWebhookSignature, verifyIndexerWebhookSignature } from "infra-sdk-typescript";
188
-
189
- // Wallet outcome webhooks (wallet.transfer.completed / .failed, wallet.sign.completed / .failed):
190
- // HMAC-SHA256("<timestamp>.<rawBody>"), with a 5-minute replay-protection window by default.
191
- verifyWalletWebhookSignature({
192
- payload: rawBody, // the exact bytes received — do not JSON.parse + re-stringify first
193
- timestampHeader: req.headers["x-wallet-timestamp"],
194
- signatureHeader: req.headers["x-wallet-signature"],
195
- secret: webhookSecret, // the value you passed as `webhookSecret` on the originating call
196
- });
197
-
198
- // Indexer (Blockchain Events) deliveries: HMAC-SHA256(rawBody), no timestamp component.
199
- verifyIndexerWebhookSignature({
200
- payload: rawBody,
201
- signatureHeader: req.headers["x-indexer-signature"],
202
- secret: signingSecret, // returned once, on subscription creation or secret rotation
203
- });
204
- ```
205
-
206
- Both throw `WebhookSignatureVerificationError` on failure — see
207
- [`examples/webhook-verification.ts`](./examples/webhook-verification.ts) for a full
208
- `node:http` handler.
209
-
210
- ### Escape hatch
211
-
212
- Every resource method is a thin wrapper over `client.request()`, which carries the same
213
- auth, retry, and timeout behavior. Use it directly for any endpoint this SDK version
214
- doesn't wrap yet:
215
-
216
- ```ts
217
- const data = await client.request<{ enabled: boolean }>({
218
- method: "GET",
219
- path: "/v1/wallet/hd/enabled",
220
- });
221
- ```
222
-
223
- ## Configuration reference
224
-
225
- ```ts
226
- new InfraClient({
227
- apiKey: string; // required — sk_live_... or sk_test_...
228
- baseURL?: string; // default "https://api.withconvexity.com"
229
- scope?: string; // space-separated OAuth scopes; omit for full access
230
- timeoutMs?: number; // default 30000
231
- maxRetries?: number; // default 2
232
- fetch?: typeof fetch; // override for testing
233
- });
234
- ```
235
-
236
- ## Development
237
-
238
- ```bash
239
- npm install
240
- npm run typecheck # tsc --noEmit
241
- npm run lint # eslint
242
- npm test # vitest
243
- npm run build # tsup -> dist/ (ESM + CJS + .d.ts)
244
- npm run ci # all of the above, in order
245
- ```
246
-
247
- ## License
248
-
249
- MIT — see [LICENSE](./LICENSE).
1
+ # infra-sdk-typescript
2
+
3
+ The official TypeScript SDK for the [Convexity Infra API](https://docs.withconvexity.com).
4
+ It covers three services: **Wallet**, **Tokenization**, and **Blockchain Events** (the
5
+ Indexer).
6
+
7
+ What you get:
8
+
9
+ - Types for every request and response, including all three of the pagination envelopes
10
+ the API uses.
11
+ - Auth handled for you. Hand the client an API key and it mints, caches, and refreshes
12
+ OAuth bearer tokens in the background.
13
+ - Retries with backoff on rate limits (`429`, honoring `retryAfter`) and upstream outages
14
+ (`503`). Connection failures get retried too, but only when a retry is safe.
15
+ - `Idempotency-Key` headers generated automatically on the wallet operations that move
16
+ value. You can pass your own instead.
17
+ - Real error classes (`RateLimitError`, `NotFoundError`, `PaymentRequiredError`, and so
18
+ on) so you're not switching on status codes.
19
+ - Signature verification for both webhook schemes: Wallet outcome webhooks and Indexer
20
+ event deliveries.
21
+ - No runtime dependencies. It's built on native `fetch` and `node:crypto`, and ships dual
22
+ ESM/CJS builds with full `.d.ts` types.
23
+
24
+ ## Requirements
25
+
26
+ - Node.js 18 or later.
27
+ - A Convexity Infra API key. There's no self-service signup yet, so email
28
+ [infra@withconvexity.com](mailto:infra@withconvexity.com) with your business details and
29
+ you'll get back a test key (`sk_test_...`) and a live key (`sk_live_...`).
30
+
31
+ ## Install
32
+
33
+ ```bash
34
+ npm install infra-sdk-typescript
35
+ ```
36
+
37
+ ## Quickstart
38
+
39
+ ```ts
40
+ import { InfraClient } from "infra-sdk-typescript";
41
+
42
+ const client = new InfraClient({ apiKey: process.env.INFRA_API_KEY! });
43
+
44
+ const balance = await client.wallet.getBalance();
45
+ console.log(`Business balance: $${balance.balance}`);
46
+ ```
47
+
48
+ On the first call, the client trades your API key for a short-lived OAuth bearer token
49
+ (`POST /v1/oauth/token`) and caches it. It refreshes that token before it expires, and
50
+ retries once if it ever gets a surprise `401`. You shouldn't need to touch tokens yourself.
51
+
52
+ There are fuller examples in [`examples/`](./examples):
53
+
54
+ - [`basic-usage.ts`](./examples/basic-usage.ts) derives a wallet, transfers, and polls for
55
+ completion.
56
+ - [`pagination.ts`](./examples/pagination.ts) shows all three ways to consume a paginated
57
+ list.
58
+ - [`tokenization-yield-lifecycle.ts`](./examples/tokenization-yield-lifecycle.ts) walks a
59
+ yield-bearing token from issue through mint, coupon payment, and principal redemption.
60
+ - [`indexer-subscription.ts`](./examples/indexer-subscription.ts) subscribes to on-chain
61
+ events and manages the subscription afterward.
62
+ - [`webhook-verification.ts`](./examples/webhook-verification.ts) verifies both signature
63
+ schemes inside a plain `node:http` server.
64
+
65
+ ## Resources
66
+
67
+ There's one namespace per service:
68
+
69
+ | Namespace | Service | Docs |
70
+ |---|---|---|
71
+ | `client.wallet` | HD wallets, on-chain transfers, sponsored signing, USD gas balance | [Wallet overview](https://docs.withconvexity.com/api-reference/wallet/overview) |
72
+ | `client.tokenization` | Issue, mint/burn/transfer tokenized assets; yield lifecycle | [Tokenization overview](https://docs.withconvexity.com/api-reference/tokenization/overview) |
73
+ | `client.indexer` | Subscribe to decoded on-chain events, delivered to your webhook | [Blockchain Events overview](https://docs.withconvexity.com/api-reference/indexer/overview) |
74
+
75
+ The methods map 1:1 onto the API. Check the JSDoc on each resource, or the type
76
+ definitions, for full parameter and return shapes. A few things worth calling out:
77
+
78
+ ### Async transfers
79
+
80
+ [`initiateTransfer`](./src/resources/wallet/index.ts) and
81
+ [`signTransaction`](./src/resources/wallet/index.ts) are async by default. They hand back a
82
+ `PENDING` transaction right away (HTTP `202`) and the on-chain execution keeps going in the
83
+ background.
84
+
85
+ ```ts
86
+ const transfer = await client.wallet.initiateTransfer({
87
+ fromAddress: 12, // HD addressIndex, or a full "0x..." address
88
+ toAddress: "0x2222222222222222222222222222222222222222",
89
+ amount: "1000000",
90
+ chainType: "EVM",
91
+ chainId: 8453,
92
+ });
93
+
94
+ // Poll until it settles...
95
+ let final = await client.wallet.getTransaction(transfer.id);
96
+ while (final.status === "PENDING" || final.status === "PROCESSING") {
97
+ await new Promise((r) => setTimeout(r, 2000));
98
+ final = await client.wallet.getTransaction(transfer.id);
99
+ }
100
+
101
+ // ...or take the outcome at a webhook instead: pass webhookUrl/webhookSecret on the
102
+ // initiate call, then verify deliveries with `verifyWalletWebhookSignature`.
103
+ ```
104
+
105
+ If you want the old synchronous behavior, pass `waitForCompletion: true`. That holds the
106
+ request open until the transaction settles and returns the final result.
107
+
108
+ ### Pagination
109
+
110
+ The API hands back three different pagination envelopes depending on which service you're
111
+ hitting. The SDK flattens all of them into a single `Page<T>`:
112
+
113
+ ```ts
114
+ // Iterate every item; later pages are fetched as you go.
115
+ for await (const token of await client.tokenization.listTokens()) {
116
+ console.log(token.ticker);
117
+ }
118
+
119
+ // Or walk it a page at a time:
120
+ let page = await client.wallet.getTransactionHistory({ status: "COMPLETED" });
121
+ for (;;) {
122
+ for (const tx of page.data) handle(tx);
123
+ if (!page.hasNextPage()) break;
124
+ page = await page.getNextPage();
125
+ }
126
+
127
+ // Or just pull the whole thing into memory, which is fine for small collections:
128
+ const holders = await (await client.tokenization.getTokenHolders({ tokenId })).toArray();
129
+ ```
130
+
131
+ ### Idempotency
132
+
133
+ `initiateTransfer`, `signTransaction`, and `generateWallets` all require an
134
+ `Idempotency-Key` header, as do the tokenization write methods (`createToken`, `mintToken`,
135
+ `burnToken`, `transferToken`, `forcedTransfer`, `registerWallet`, `distributeYield`,
136
+ `payCoupon`, `claimYield`, `redeemPrincipal`). The SDK generates a UUIDv4 per call. Pass your own when you need
137
+ a retry to line up across two separate SDK calls, like after a process restart:
138
+
139
+ ```ts
140
+ await client.wallet.initiateTransfer(params, { idempotencyKey: "my-own-key" });
141
+ ```
142
+
143
+ ### Errors
144
+
145
+ Everything the SDK throws extends `InfraError`. HTTP failures extend `InfraAPIError` and
146
+ line up with the status code table in the API docs:
147
+
148
+ ```ts
149
+ import { RateLimitError, PaymentRequiredError, UnprocessableEntityError, InfraAPIError } from "infra-sdk-typescript";
150
+
151
+ try {
152
+ await client.wallet.initiateTransfer(params);
153
+ } catch (err) {
154
+ if (err instanceof RateLimitError) {
155
+ console.log(`Throttled — retry after ${err.retryAfterSeconds}s`);
156
+ } else if (err instanceof PaymentRequiredError) {
157
+ console.log("Insufficient gas balance");
158
+ } else if (err instanceof UnprocessableEntityError) {
159
+ // A waitForCompletion: true transfer that reverts on-chain still carries the failed
160
+ // transaction. Read it off the raw response body if you need the details.
161
+ console.log((err.body as { data?: unknown }).data);
162
+ } else if (err instanceof InfraAPIError) {
163
+ console.log(err.status, err.message, err.errors);
164
+ }
165
+ }
166
+ ```
167
+
168
+ `429` and `503` get retried for you (twice by default). Everything else throws right away,
169
+ because sending the identical request again won't change the answer.
170
+
171
+ ### Retries, timeouts, and connection safety
172
+
173
+ - `429` and `503` are always retried. The server never executed the request in either
174
+ case, so there's nothing to double up on. `429` retries wait for whatever `retryAfter`
175
+ says.
176
+ - Connection failures are murkier. If a request times out, or DNS fails, or TLS breaks, or
177
+ the socket aborts, you can't tell whether the server already ran it. So the SDK only
178
+ retries those on `GET` requests and on requests carrying an `Idempotency-Key`, where a
179
+ duplicate provably can't hurt.
180
+ - Set these on the constructor, or override them per call:
181
+
182
+ ```ts
183
+ const client = new InfraClient({
184
+ apiKey: process.env.INFRA_API_KEY!,
185
+ timeoutMs: 30_000, // default
186
+ maxRetries: 2, // default
187
+ });
188
+
189
+ await client.wallet.getBalance({ timeoutMs: 5_000, signal: myAbortController.signal });
190
+ ```
191
+
192
+ ### Webhooks
193
+
194
+ ```ts
195
+ import { verifyWalletWebhookSignature, verifyIndexerWebhookSignature } from "infra-sdk-typescript";
196
+
197
+ // Wallet outcome webhooks (wallet.transfer.completed / .failed, wallet.sign.completed / .failed):
198
+ // HMAC-SHA256("<timestamp>.<rawBody>"), with a 5-minute replay window by default.
199
+ verifyWalletWebhookSignature({
200
+ payload: rawBody, // exactly the bytes you received — don't JSON.parse and re-stringify
201
+ timestampHeader: req.headers["x-wallet-timestamp"],
202
+ signatureHeader: req.headers["x-wallet-signature"],
203
+ secret: webhookSecret, // whatever you passed as `webhookSecret` on the originating call
204
+ });
205
+
206
+ // Indexer (Blockchain Events) deliveries: HMAC-SHA256(rawBody), no timestamp component.
207
+ verifyIndexerWebhookSignature({
208
+ payload: rawBody,
209
+ signatureHeader: req.headers["x-indexer-signature"],
210
+ secret: signingSecret, // returned once, when you create the subscription or rotate the secret
211
+ });
212
+ ```
213
+
214
+ Both throw `WebhookSignatureVerificationError` when verification fails. There's a complete
215
+ `node:http` handler in [`examples/webhook-verification.ts`](./examples/webhook-verification.ts).
216
+
217
+ ### Escape hatch
218
+
219
+ Every resource method is a thin wrapper around `client.request()`, which carries the same
220
+ auth, retry, and timeout behavior. If this version of the SDK doesn't wrap an endpoint yet,
221
+ call it directly:
222
+
223
+ ```ts
224
+ const data = await client.request<{ enabled: boolean }>({
225
+ method: "GET",
226
+ path: "/v1/wallet/hd/enabled",
227
+ });
228
+ ```
229
+
230
+ ## Configuration reference
231
+
232
+ ```ts
233
+ new InfraClient({
234
+ apiKey: string; // required — sk_live_... or sk_test_...
235
+ baseURL?: string; // default "https://api.withconvexity.com"
236
+ scope?: string; // space-separated OAuth scopes; omit for full access
237
+ timeoutMs?: number; // default 30000
238
+ maxRetries?: number; // default 2
239
+ fetch?: typeof fetch; // override for testing
240
+ });
241
+ ```
242
+
243
+ ## Development
244
+
245
+ ```bash
246
+ npm install
247
+ npm run typecheck # tsc --noEmit
248
+ npm run lint # eslint
249
+ npm test # vitest
250
+ npm run build # tsup -> dist/ (ESM + CJS + .d.ts)
251
+ npm run ci # all of the above, in order
252
+ ```
253
+
254
+ ## License
255
+
256
+ MIT. See [LICENSE](./LICENSE).