infra-sdk-typescript 0.1.3 → 0.3.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,254 +1,257 @@
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. The SDK generates a UUIDv4 per call. Pass your own when you need
135
- a retry to line up across two separate SDK calls, like after a process restart:
136
-
137
- ```ts
138
- await client.wallet.initiateTransfer(params, { idempotencyKey: "my-own-key" });
139
- ```
140
-
141
- ### Errors
142
-
143
- Everything the SDK throws extends `InfraError`. HTTP failures extend `InfraAPIError` and
144
- line up with the status code table in the API docs:
145
-
146
- ```ts
147
- import { RateLimitError, PaymentRequiredError, UnprocessableEntityError, InfraAPIError } from "infra-sdk-typescript";
148
-
149
- try {
150
- await client.wallet.initiateTransfer(params);
151
- } catch (err) {
152
- if (err instanceof RateLimitError) {
153
- console.log(`Throttled — retry after ${err.retryAfterSeconds}s`);
154
- } else if (err instanceof PaymentRequiredError) {
155
- console.log("Insufficient gas balance");
156
- } else if (err instanceof UnprocessableEntityError) {
157
- // A waitForCompletion: true transfer that reverts on-chain still carries the failed
158
- // transaction. Read it off the raw response body if you need the details.
159
- console.log((err.body as { data?: unknown }).data);
160
- } else if (err instanceof InfraAPIError) {
161
- console.log(err.status, err.message, err.errors);
162
- }
163
- }
164
- ```
165
-
166
- `429` and `503` get retried for you (twice by default). Everything else throws right away,
167
- because sending the identical request again won't change the answer.
168
-
169
- ### Retries, timeouts, and connection safety
170
-
171
- - `429` and `503` are always retried. The server never executed the request in either
172
- case, so there's nothing to double up on. `429` retries wait for whatever `retryAfter`
173
- says.
174
- - Connection failures are murkier. If a request times out, or DNS fails, or TLS breaks, or
175
- the socket aborts, you can't tell whether the server already ran it. So the SDK only
176
- retries those on `GET` requests and on requests carrying an `Idempotency-Key`, where a
177
- duplicate provably can't hurt.
178
- - Set these on the constructor, or override them per call:
179
-
180
- ```ts
181
- const client = new InfraClient({
182
- apiKey: process.env.INFRA_API_KEY!,
183
- timeoutMs: 30_000, // default
184
- maxRetries: 2, // default
185
- });
186
-
187
- await client.wallet.getBalance({ timeoutMs: 5_000, signal: myAbortController.signal });
188
- ```
189
-
190
- ### Webhooks
191
-
192
- ```ts
193
- import { verifyWalletWebhookSignature, verifyIndexerWebhookSignature } from "infra-sdk-typescript";
194
-
195
- // Wallet outcome webhooks (wallet.transfer.completed / .failed, wallet.sign.completed / .failed):
196
- // HMAC-SHA256("<timestamp>.<rawBody>"), with a 5-minute replay window by default.
197
- verifyWalletWebhookSignature({
198
- payload: rawBody, // exactly the bytes you received — don't JSON.parse and re-stringify
199
- timestampHeader: req.headers["x-wallet-timestamp"],
200
- signatureHeader: req.headers["x-wallet-signature"],
201
- secret: webhookSecret, // whatever you passed as `webhookSecret` on the originating call
202
- });
203
-
204
- // Indexer (Blockchain Events) deliveries: HMAC-SHA256(rawBody), no timestamp component.
205
- verifyIndexerWebhookSignature({
206
- payload: rawBody,
207
- signatureHeader: req.headers["x-indexer-signature"],
208
- secret: signingSecret, // returned once, when you create the subscription or rotate the secret
209
- });
210
- ```
211
-
212
- Both throw `WebhookSignatureVerificationError` when verification fails. There's a complete
213
- `node:http` handler in [`examples/webhook-verification.ts`](./examples/webhook-verification.ts).
214
-
215
- ### Escape hatch
216
-
217
- Every resource method is a thin wrapper around `client.request()`, which carries the same
218
- auth, retry, and timeout behavior. If this version of the SDK doesn't wrap an endpoint yet,
219
- call it directly:
220
-
221
- ```ts
222
- const data = await client.request<{ enabled: boolean }>({
223
- method: "GET",
224
- path: "/v1/wallet/hd/enabled",
225
- });
226
- ```
227
-
228
- ## Configuration reference
229
-
230
- ```ts
231
- new InfraClient({
232
- apiKey: string; // required — sk_live_... or sk_test_...
233
- baseURL?: string; // default "https://api.withconvexity.com"
234
- scope?: string; // space-separated OAuth scopes; omit for full access
235
- timeoutMs?: number; // default 30000
236
- maxRetries?: number; // default 2
237
- fetch?: typeof fetch; // override for testing
238
- });
239
- ```
240
-
241
- ## Development
242
-
243
- ```bash
244
- npm install
245
- npm run typecheck # tsc --noEmit
246
- npm run lint # eslint
247
- npm test # vitest
248
- npm run build # tsup -> dist/ (ESM + CJS + .d.ts)
249
- npm run ci # all of the above, in order
250
- ```
251
-
252
- ## License
253
-
254
- 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`, `reclaimUnclaimedYield`). The SDK generates a
137
+ UUIDv4 per call. Pass your own when you need a retry to line up across two separate SDK calls,
138
+ like after a process restart:
139
+
140
+ ```ts
141
+ await client.wallet.initiateTransfer(params, { idempotencyKey: "my-own-key" });
142
+ ```
143
+
144
+ ### Errors
145
+
146
+ Everything the SDK throws extends `InfraError`. HTTP failures extend `InfraAPIError` and
147
+ line up with the status code table in the API docs:
148
+
149
+ ```ts
150
+ import { RateLimitError, PaymentRequiredError, UnprocessableEntityError, InfraAPIError } from "infra-sdk-typescript";
151
+
152
+ try {
153
+ await client.wallet.initiateTransfer(params);
154
+ } catch (err) {
155
+ if (err instanceof RateLimitError) {
156
+ console.log(`Throttled — retry after ${err.retryAfterSeconds}s`);
157
+ } else if (err instanceof PaymentRequiredError) {
158
+ console.log("Insufficient gas balance");
159
+ } else if (err instanceof UnprocessableEntityError) {
160
+ // A waitForCompletion: true transfer that reverts on-chain still carries the failed
161
+ // transaction. Read it off the raw response body if you need the details.
162
+ console.log((err.body as { data?: unknown }).data);
163
+ } else if (err instanceof InfraAPIError) {
164
+ console.log(err.status, err.message, err.errors);
165
+ }
166
+ }
167
+ ```
168
+
169
+ `429` and `503` get retried for you (twice by default). Everything else throws right away,
170
+ because sending the identical request again won't change the answer.
171
+
172
+ ### Retries, timeouts, and connection safety
173
+
174
+ - `429` and `503` are always retried. The server never executed the request in either
175
+ case, so there's nothing to double up on. `429` retries wait for whatever `retryAfter`
176
+ says.
177
+ - Connection failures are murkier. If a request times out, or DNS fails, or TLS breaks, or
178
+ the socket aborts, you can't tell whether the server already ran it. So the SDK only
179
+ retries those on `GET` requests and on requests carrying an `Idempotency-Key`, where a
180
+ duplicate provably can't hurt.
181
+ - Set these on the constructor, or override them per call:
182
+
183
+ ```ts
184
+ const client = new InfraClient({
185
+ apiKey: process.env.INFRA_API_KEY!,
186
+ timeoutMs: 30_000, // default
187
+ maxRetries: 2, // default
188
+ });
189
+
190
+ await client.wallet.getBalance({ timeoutMs: 5_000, signal: myAbortController.signal });
191
+ ```
192
+
193
+ ### Webhooks
194
+
195
+ ```ts
196
+ import { verifyWalletWebhookSignature, verifyIndexerWebhookSignature } from "infra-sdk-typescript";
197
+
198
+ // Wallet outcome webhooks (wallet.transfer.completed / .failed, wallet.sign.completed / .failed):
199
+ // HMAC-SHA256("<timestamp>.<rawBody>"), with a 5-minute replay window by default.
200
+ verifyWalletWebhookSignature({
201
+ payload: rawBody, // exactly the bytes you received — don't JSON.parse and re-stringify
202
+ timestampHeader: req.headers["x-wallet-timestamp"],
203
+ signatureHeader: req.headers["x-wallet-signature"],
204
+ secret: webhookSecret, // whatever you passed as `webhookSecret` on the originating call
205
+ });
206
+
207
+ // Indexer (Blockchain Events) deliveries: HMAC-SHA256(rawBody), no timestamp component.
208
+ verifyIndexerWebhookSignature({
209
+ payload: rawBody,
210
+ signatureHeader: req.headers["x-indexer-signature"],
211
+ secret: signingSecret, // returned once, when you create the subscription or rotate the secret
212
+ });
213
+ ```
214
+
215
+ Both throw `WebhookSignatureVerificationError` when verification fails. There's a complete
216
+ `node:http` handler in [`examples/webhook-verification.ts`](./examples/webhook-verification.ts).
217
+
218
+ ### Escape hatch
219
+
220
+ Every resource method is a thin wrapper around `client.request()`, which carries the same
221
+ auth, retry, and timeout behavior. If this version of the SDK doesn't wrap an endpoint yet,
222
+ call it directly:
223
+
224
+ ```ts
225
+ const data = await client.request<{ enabled: boolean }>({
226
+ method: "GET",
227
+ path: "/v1/wallet/hd/enabled",
228
+ });
229
+ ```
230
+
231
+ ## Configuration reference
232
+
233
+ ```ts
234
+ new InfraClient({
235
+ apiKey: string; // required — sk_live_... or sk_test_...
236
+ baseURL?: string; // default "https://api.withconvexity.com"
237
+ scope?: string; // space-separated OAuth scopes; omit for full access
238
+ timeoutMs?: number; // default 30000
239
+ maxRetries?: number; // default 2
240
+ fetch?: typeof fetch; // override for testing
241
+ });
242
+ ```
243
+
244
+ ## Development
245
+
246
+ ```bash
247
+ npm install
248
+ npm run typecheck # tsc --noEmit
249
+ npm run lint # eslint
250
+ npm test # vitest
251
+ npm run build # tsup -> dist/ (ESM + CJS + .d.ts)
252
+ npm run ci # all of the above, in order
253
+ ```
254
+
255
+ ## License
256
+
257
+ MIT. See [LICENSE](./LICENSE).