@curvhex/orm 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/LICENSE +143 -0
- package/README.md +485 -0
- package/dist/adapters/RpcAdapter.d.ts +12 -0
- package/dist/adapters/RpcAdapter.d.ts.map +1 -0
- package/dist/adapters/RpcAdapter.js +67 -0
- package/dist/adapters/RpcAdapter.js.map +1 -0
- package/dist/adapters/abstract/QueryAdapter.d.ts +61 -0
- package/dist/adapters/abstract/QueryAdapter.d.ts.map +1 -0
- package/dist/adapters/abstract/QueryAdapter.js +3 -0
- package/dist/adapters/abstract/QueryAdapter.js.map +1 -0
- package/dist/client/CurvhexClient.d.ts +19 -0
- package/dist/client/CurvhexClient.d.ts.map +1 -0
- package/dist/client/CurvhexClient.js +123 -0
- package/dist/client/CurvhexClient.js.map +1 -0
- package/dist/client/CurvhexORM.d.ts +19 -0
- package/dist/client/CurvhexORM.d.ts.map +1 -0
- package/dist/client/CurvhexORM.js +21 -0
- package/dist/client/CurvhexORM.js.map +1 -0
- package/dist/client/VertexClient.d.ts +19 -0
- package/dist/client/VertexClient.d.ts.map +1 -0
- package/dist/client/VertexClient.js +123 -0
- package/dist/client/VertexClient.js.map +1 -0
- package/dist/client/VertexORM.d.ts +19 -0
- package/dist/client/VertexORM.d.ts.map +1 -0
- package/dist/client/VertexORM.js +21 -0
- package/dist/client/VertexORM.js.map +1 -0
- package/dist/core/deserializer.d.ts +3 -0
- package/dist/core/deserializer.d.ts.map +1 -0
- package/dist/core/deserializer.js +74 -0
- package/dist/core/deserializer.js.map +1 -0
- package/dist/core/filters.d.ts +46 -0
- package/dist/core/filters.d.ts.map +1 -0
- package/dist/core/filters.js +121 -0
- package/dist/core/filters.js.map +1 -0
- package/dist/core/schema.d.ts +12 -0
- package/dist/core/schema.d.ts.map +1 -0
- package/dist/core/schema.js +34 -0
- package/dist/core/schema.js.map +1 -0
- package/dist/core/types.d.ts +16 -0
- package/dist/core/types.d.ts.map +1 -0
- package/dist/core/types.js +3 -0
- package/dist/core/types.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +12 -0
- package/dist/index.js.map +1 -0
- package/package.json +33 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
Apache License 2.0 with Commons Clause
|
|
2
|
+
|
|
3
|
+
"Commons Clause" License Condition v1.0
|
|
4
|
+
|
|
5
|
+
The Software is provided to you by the Licensor under the License, as defined
|
|
6
|
+
below, subject to the following condition.
|
|
7
|
+
|
|
8
|
+
Without limiting other conditions in the License, the grant of rights under
|
|
9
|
+
the License will not include, and the License does not grant to you, the right
|
|
10
|
+
to Sell the Software.
|
|
11
|
+
|
|
12
|
+
For purposes of the foregoing, "Sell" means practicing any or all of the
|
|
13
|
+
rights granted to you under the License to provide to third parties, for a fee
|
|
14
|
+
or other consideration (including without limitation fees for hosting or
|
|
15
|
+
consulting / support services related to the Software), a product or service
|
|
16
|
+
whose value derives, entirely or substantially, from the functionality of the
|
|
17
|
+
Software. Any license notice or attribution required by the License must also
|
|
18
|
+
include this Commons Clause License Condition notice.
|
|
19
|
+
|
|
20
|
+
Software: curvhex-orm
|
|
21
|
+
License: Apache 2.0
|
|
22
|
+
Licensor: Bugra Okumus
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
Apache License
|
|
27
|
+
Version 2.0, January 2004
|
|
28
|
+
http://www.apache.org/licenses/
|
|
29
|
+
|
|
30
|
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
31
|
+
|
|
32
|
+
1. Definitions.
|
|
33
|
+
|
|
34
|
+
"License" shall mean the terms and conditions for use, reproduction,
|
|
35
|
+
and distribution as defined by Sections 1 through 9 of this document.
|
|
36
|
+
|
|
37
|
+
"Licensor" shall mean the copyright owner or entity authorized by
|
|
38
|
+
the copyright owner that is granting the License.
|
|
39
|
+
|
|
40
|
+
"Legal Entity" shall mean the union of the acting entity and all
|
|
41
|
+
other entities that control, are controlled by, or are under common
|
|
42
|
+
control with that entity.
|
|
43
|
+
|
|
44
|
+
"You" (or "Your") shall mean an individual or Legal Entity
|
|
45
|
+
exercising permissions granted by this License.
|
|
46
|
+
|
|
47
|
+
"Source" form shall mean the preferred form for making modifications,
|
|
48
|
+
including but not limited to software source code, documentation
|
|
49
|
+
source, and configuration files.
|
|
50
|
+
|
|
51
|
+
"Object" form shall mean any form resulting from mechanical
|
|
52
|
+
transformation or translation of a Source form, including but
|
|
53
|
+
not limited to compiled object code, generated documentation,
|
|
54
|
+
and conversions to other media types.
|
|
55
|
+
|
|
56
|
+
"Work" shall mean the work of authorship made available under
|
|
57
|
+
the License, as indicated by a copyright notice that is included
|
|
58
|
+
in or attached to the work.
|
|
59
|
+
|
|
60
|
+
"Derivative Works" shall mean any work, whether in Source or Object
|
|
61
|
+
form, that is based on (or derived from) the Work and for which the
|
|
62
|
+
editorial revisions, annotations, elaborations, or other
|
|
63
|
+
transformations represent, as a whole, an original work of authorship.
|
|
64
|
+
|
|
65
|
+
"Contribution" shall mean any work of authorship, including
|
|
66
|
+
the original version of the Work and any modifications or additions
|
|
67
|
+
to that Work or Derivative Works of the Work, that is intentionally
|
|
68
|
+
submitted to the Licensor for inclusion in the Work by the copyright
|
|
69
|
+
owner or by an individual or Legal Entity authorized to submit on
|
|
70
|
+
behalf of the copyright owner.
|
|
71
|
+
|
|
72
|
+
"Contributor" shall mean Licensor and any Legal Entity on behalf of
|
|
73
|
+
whom a Contribution has been received by the Licensor and included
|
|
74
|
+
within the Work.
|
|
75
|
+
|
|
76
|
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
77
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
78
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
79
|
+
copyright license to reproduce, prepare Derivative Works of,
|
|
80
|
+
publicly display, publicly perform, sublicense, and distribute the
|
|
81
|
+
Work and such Derivative Works in Source or Object form.
|
|
82
|
+
|
|
83
|
+
3. Grant of Patent License. Subject to the terms and conditions of
|
|
84
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
85
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
86
|
+
patent license to make, use, sell, offer for sale, import, and
|
|
87
|
+
otherwise transfer the Work.
|
|
88
|
+
|
|
89
|
+
4. Redistribution. You may reproduce and distribute copies of the
|
|
90
|
+
Work or Derivative Works thereof in any medium, with or without
|
|
91
|
+
modifications, and in Source or Object form, provided that You
|
|
92
|
+
meet the following conditions:
|
|
93
|
+
|
|
94
|
+
(a) You must give any other recipients of the Work or Derivative
|
|
95
|
+
Works a copy of this License; and
|
|
96
|
+
|
|
97
|
+
(b) You must cause any modified files to carry prominent notices
|
|
98
|
+
stating that You changed the files; and
|
|
99
|
+
|
|
100
|
+
(c) You must retain, in the Source form of any Derivative Works
|
|
101
|
+
that You distribute, all copyright, patent, trademark, and
|
|
102
|
+
attribution notices from the Source form of the Work; and
|
|
103
|
+
|
|
104
|
+
(d) If the Work includes a "NOTICE" text file, You must include a
|
|
105
|
+
readable copy of the attribution notices contained within such
|
|
106
|
+
NOTICE file.
|
|
107
|
+
|
|
108
|
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
109
|
+
any Contribution intentionally submitted for inclusion in the Work
|
|
110
|
+
by You to the Licensor shall be under the terms and conditions of
|
|
111
|
+
this License, without any additional terms or conditions.
|
|
112
|
+
|
|
113
|
+
6. Trademarks. This License does not grant permission to use the trade
|
|
114
|
+
names, trademarks, service marks, or product names of the Licensor.
|
|
115
|
+
|
|
116
|
+
7. Disclaimer of Warranty. Unless required by applicable law or agreed
|
|
117
|
+
to in writing, Licensor provides the Work on an "AS IS" BASIS,
|
|
118
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
119
|
+
implied.
|
|
120
|
+
|
|
121
|
+
8. Limitation of Liability. In no event and under no legal theory,
|
|
122
|
+
whether in tort (including negligence), contract, or otherwise,
|
|
123
|
+
unless required by applicable law, shall any Contributor be liable
|
|
124
|
+
to You for damages, including any direct, indirect, special,
|
|
125
|
+
incidental, or exemplary damages of any character arising as a
|
|
126
|
+
result of this License or out of the use or inability to use the
|
|
127
|
+
Work.
|
|
128
|
+
|
|
129
|
+
9. Accepting Warranty or Additional Liability. While redistributing
|
|
130
|
+
the Work or Derivative Works thereof, You may choose to offer
|
|
131
|
+
additional warranty, liability, or other obligations consistent
|
|
132
|
+
with this License. However, in accepting such obligations, You may
|
|
133
|
+
act only on Your own behalf and on Your sole responsibility.
|
|
134
|
+
|
|
135
|
+
END OF TERMS AND CONDITIONS
|
|
136
|
+
|
|
137
|
+
Copyright 2026 Bugra Okumus
|
|
138
|
+
|
|
139
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
140
|
+
you may not use this file except in compliance with the License.
|
|
141
|
+
You may obtain a copy of the License at
|
|
142
|
+
|
|
143
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
package/README.md
ADDED
|
@@ -0,0 +1,485 @@
|
|
|
1
|
+
# Curvhex ORM
|
|
2
|
+
|
|
3
|
+
TypeScript ORM for Solana PDA accounts. Query, filter, and aggregate on-chain data with a familiar API — inspired by Prisma, built for Solana.
|
|
4
|
+
|
|
5
|
+
```typescript
|
|
6
|
+
const users = await orm.models.userAccount.findMany({
|
|
7
|
+
where: { authority: wallet.publicKey, isActive: true },
|
|
8
|
+
orderBy: { balance: 'desc' },
|
|
9
|
+
take: 10,
|
|
10
|
+
})
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## The Problem
|
|
16
|
+
|
|
17
|
+
Solana stores program state in accounts (PDAs). Querying them is painful:
|
|
18
|
+
|
|
19
|
+
- `getProgramAccounts` only supports exact byte matching (`memcmp`)
|
|
20
|
+
- No range queries, sorting, aggregation, or relations at the RPC level
|
|
21
|
+
- Every project reimplements the same deserialization + filtering boilerplate
|
|
22
|
+
- Switching from a public RPC to an indexer (Helius, your own Postgres) requires rewriting query logic
|
|
23
|
+
|
|
24
|
+
Curvhex ORM solves this with a single, adapter-agnostic query API.
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## How It Works
|
|
29
|
+
|
|
30
|
+
```
|
|
31
|
+
Your Code → CurvhexORM → QueryAdapter → Data Source
|
|
32
|
+
├── RpcAdapter (getProgramAccounts)
|
|
33
|
+
├── HeliusAdapter (DAS API) [soon]
|
|
34
|
+
└── PostgresAdapter (your indexer) [soon]
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Define your schema once. Write queries once. Swap the adapter as your needs grow — from a quick prototype on public RPC to a production indexer — without changing a single query.
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## Installation
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
npm install curvhex-orm @solana/web3.js
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## Quick Start
|
|
50
|
+
|
|
51
|
+
### 1. Define your schema
|
|
52
|
+
|
|
53
|
+
```typescript
|
|
54
|
+
import { defineModel, anchor } from 'curvhex-orm'
|
|
55
|
+
|
|
56
|
+
const UserAccount = defineModel({
|
|
57
|
+
// Anchor programs: use anchor() helper
|
|
58
|
+
discriminator: anchor('UserAccount'),
|
|
59
|
+
|
|
60
|
+
// Native programs: provide raw bytes
|
|
61
|
+
// discriminator: [1, 2, 3, 4, 5, 6, 7, 8],
|
|
62
|
+
|
|
63
|
+
fields: {
|
|
64
|
+
authority: { type: 'publicKey' },
|
|
65
|
+
balance: { type: 'u64' },
|
|
66
|
+
tier: { type: 'u8' },
|
|
67
|
+
isActive: { type: 'bool' },
|
|
68
|
+
name: { type: 'string' },
|
|
69
|
+
}
|
|
70
|
+
})
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Field offsets are calculated automatically from the discriminator length and field order — no manual byte counting.
|
|
74
|
+
|
|
75
|
+
### 2. Create the ORM
|
|
76
|
+
|
|
77
|
+
```typescript
|
|
78
|
+
import { CurvhexORM, RpcAdapter } from 'curvhex-orm'
|
|
79
|
+
import { Connection, PublicKey } from '@solana/web3.js'
|
|
80
|
+
|
|
81
|
+
const connection = new Connection('https://api.mainnet-beta.solana.com')
|
|
82
|
+
|
|
83
|
+
const orm = new CurvhexORM({
|
|
84
|
+
connection,
|
|
85
|
+
programId: 'YOUR_PROGRAM_ID',
|
|
86
|
+
models: { UserAccount },
|
|
87
|
+
})
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### 3. Query
|
|
91
|
+
|
|
92
|
+
```typescript
|
|
93
|
+
// Find by PDA seeds
|
|
94
|
+
const user = await orm.models.userAccount.findByPda([
|
|
95
|
+
Buffer.from('user'),
|
|
96
|
+
wallet.publicKey.toBuffer(),
|
|
97
|
+
])
|
|
98
|
+
|
|
99
|
+
// Find by address
|
|
100
|
+
const user = await orm.models.userAccount.findByAddress('Abc123...')
|
|
101
|
+
|
|
102
|
+
// Find many with filters
|
|
103
|
+
const users = await orm.models.userAccount.findMany({
|
|
104
|
+
where: { isActive: true, authority: wallet.publicKey.toBase58() },
|
|
105
|
+
orderBy: { balance: 'desc' },
|
|
106
|
+
take: 20,
|
|
107
|
+
skip: 0,
|
|
108
|
+
})
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
## API Reference
|
|
114
|
+
|
|
115
|
+
### `defineModel(input)`
|
|
116
|
+
|
|
117
|
+
Defines an account schema. Offsets are computed automatically.
|
|
118
|
+
|
|
119
|
+
```typescript
|
|
120
|
+
const MyAccount = defineModel({
|
|
121
|
+
discriminator: anchor('MyAccount'), // or raw number[]
|
|
122
|
+
fields: {
|
|
123
|
+
owner: { type: 'publicKey' },
|
|
124
|
+
amount: { type: 'u64' },
|
|
125
|
+
enabled: { type: 'bool' },
|
|
126
|
+
}
|
|
127
|
+
})
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
**Supported field types:**
|
|
131
|
+
|
|
132
|
+
| Type | Size | TypeScript type |
|
|
133
|
+
|------|------|-----------------|
|
|
134
|
+
| `u8` `u16` `u32` | 1–4 bytes | `number` |
|
|
135
|
+
| `i8` `i16` `i32` | 1–4 bytes | `number` |
|
|
136
|
+
| `u64` `u128` | 8–16 bytes | `bigint` |
|
|
137
|
+
| `i64` `i128` | 8–16 bytes | `bigint` |
|
|
138
|
+
| `bool` | 1 byte | `boolean` |
|
|
139
|
+
| `publicKey` | 32 bytes | `string` (base58) |
|
|
140
|
+
| `string` | 4 + N bytes | `string` |
|
|
141
|
+
| `bytes` | 4 + N bytes | `string` (hex) |
|
|
142
|
+
|
|
143
|
+
### `anchor(name)`
|
|
144
|
+
|
|
145
|
+
Computes the 8-byte Anchor discriminator for an account name.
|
|
146
|
+
|
|
147
|
+
```typescript
|
|
148
|
+
import { anchor } from 'curvhex-orm'
|
|
149
|
+
|
|
150
|
+
anchor('UserAccount') // → [149, 88, 201, ...]
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### `findMany(options?)`
|
|
154
|
+
|
|
155
|
+
```typescript
|
|
156
|
+
const results = await orm.models.userAccount.findMany({
|
|
157
|
+
where: {
|
|
158
|
+
// Equality (on-chain memcmp — fast)
|
|
159
|
+
authority: 'Abc123...',
|
|
160
|
+
isActive: true,
|
|
161
|
+
tier: 2,
|
|
162
|
+
|
|
163
|
+
// Range operators (client-side filter)
|
|
164
|
+
balance: { gte: 100n, lte: 10_000n },
|
|
165
|
+
balance: { gt: 0n },
|
|
166
|
+
tier: { in: [1, 2, 3] },
|
|
167
|
+
balance: { between: [100n, 1000n] },
|
|
168
|
+
tier: { not: 0 },
|
|
169
|
+
},
|
|
170
|
+
orderBy: { balance: 'desc' },
|
|
171
|
+
take: 10,
|
|
172
|
+
skip: 0,
|
|
173
|
+
dataSize: 165, // optional: filter by account byte size
|
|
174
|
+
})
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
> **On-chain vs client-side:** Equality filters (`authority`, `isActive`, `tier`) are sent as `memcmp` filters to the RPC node — only matching accounts are transferred. Range operators (`gt`, `gte`, `lt`, `lte`, `between`, `in`) are applied after accounts are received. Combine both for best performance.
|
|
178
|
+
|
|
179
|
+
### `findFirst(options?)`
|
|
180
|
+
|
|
181
|
+
Returns the first matching account or `null`.
|
|
182
|
+
|
|
183
|
+
```typescript
|
|
184
|
+
const user = await orm.models.userAccount.findFirst({
|
|
185
|
+
where: { authority: wallet.publicKey.toBase58() }
|
|
186
|
+
})
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
### `findByAddress(address)`
|
|
190
|
+
|
|
191
|
+
Fetches a single account by its public key string.
|
|
192
|
+
|
|
193
|
+
```typescript
|
|
194
|
+
const user = await orm.models.userAccount.findByAddress('Abc123...')
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
### `findByPda(seeds)`
|
|
198
|
+
|
|
199
|
+
Derives the PDA and fetches the account.
|
|
200
|
+
|
|
201
|
+
```typescript
|
|
202
|
+
const user = await orm.models.userAccount.findByPda([
|
|
203
|
+
Buffer.from('user'),
|
|
204
|
+
wallet.publicKey.toBuffer(),
|
|
205
|
+
])
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
### `count(where?)`
|
|
209
|
+
|
|
210
|
+
```typescript
|
|
211
|
+
const total = await orm.models.userAccount.count({ isActive: true })
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
### `aggregate(options)`
|
|
215
|
+
|
|
216
|
+
```typescript
|
|
217
|
+
const stats = await orm.models.userAccount.aggregate({
|
|
218
|
+
where: { isActive: true },
|
|
219
|
+
_count: true,
|
|
220
|
+
_sum: { balance: true },
|
|
221
|
+
_avg: { balance: true },
|
|
222
|
+
_min: { balance: true },
|
|
223
|
+
_max: { balance: true },
|
|
224
|
+
})
|
|
225
|
+
// → { _count: 142, _sum: { balance: 5_000_000n }, _avg: ..., _min: ..., _max: ... }
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
### `groupBy(options)`
|
|
229
|
+
|
|
230
|
+
```typescript
|
|
231
|
+
const byTier = await orm.models.userAccount.groupBy({
|
|
232
|
+
by: ['tier'],
|
|
233
|
+
where: { isActive: true },
|
|
234
|
+
_count: true,
|
|
235
|
+
_sum: { balance: true },
|
|
236
|
+
})
|
|
237
|
+
// → [
|
|
238
|
+
// { tier: 2, _count: 40, _sum: { balance: 2_000_000n } },
|
|
239
|
+
// { tier: 1, _count: 102, _sum: { balance: 3_000_000n } },
|
|
240
|
+
// ]
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
### `findMany` with `include` (Relations)
|
|
244
|
+
|
|
245
|
+
Fetch related accounts in a single call. Deduplicates addresses automatically — if 100 vaults share the same owner, the owner account is fetched once.
|
|
246
|
+
|
|
247
|
+
```typescript
|
|
248
|
+
const VaultAccount = defineModel({
|
|
249
|
+
discriminator: anchor('VaultAccount'),
|
|
250
|
+
fields: {
|
|
251
|
+
ownerPubkey: { type: 'publicKey' },
|
|
252
|
+
totalLocked: { type: 'u64' },
|
|
253
|
+
}
|
|
254
|
+
})
|
|
255
|
+
|
|
256
|
+
const vaults = await orm.models.vaultAccount.findMany({
|
|
257
|
+
where: { totalLocked: { gt: 1000n } },
|
|
258
|
+
include: {
|
|
259
|
+
owner: {
|
|
260
|
+
model: UserAccount,
|
|
261
|
+
foreignKey: 'ownerPubkey',
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
})
|
|
265
|
+
// → [{ address, totalLocked, owner: { authority, balance, tier } }]
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
---
|
|
269
|
+
|
|
270
|
+
## Adapters
|
|
271
|
+
|
|
272
|
+
The query API is adapter-agnostic. Swap the data source without changing your queries.
|
|
273
|
+
|
|
274
|
+
### RpcAdapter (default)
|
|
275
|
+
|
|
276
|
+
Works with any Solana RPC endpoint. No setup required.
|
|
277
|
+
|
|
278
|
+
```typescript
|
|
279
|
+
import { CurvhexORM, RpcAdapter } from 'curvhex-orm'
|
|
280
|
+
|
|
281
|
+
const orm = new CurvhexORM({
|
|
282
|
+
connection,
|
|
283
|
+
programId: 'YOUR_PROGRAM_ID',
|
|
284
|
+
models: { UserAccount },
|
|
285
|
+
// adapter defaults to RpcAdapter
|
|
286
|
+
})
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
**Limitations:** `getProgramAccounts` is rate-limited or blocked on many public endpoints for large programs. Range queries require fetching all matching accounts first.
|
|
290
|
+
|
|
291
|
+
### HeliusAdapter *(coming soon)*
|
|
292
|
+
|
|
293
|
+
Uses the Helius DAS API. Faster, higher rate limits, better support for large programs.
|
|
294
|
+
|
|
295
|
+
```typescript
|
|
296
|
+
import { HeliusAdapter } from 'curvhex-orm/adapters'
|
|
297
|
+
|
|
298
|
+
const orm = new CurvhexORM({
|
|
299
|
+
connection,
|
|
300
|
+
programId: 'YOUR_PROGRAM_ID',
|
|
301
|
+
models: { UserAccount },
|
|
302
|
+
adapter: new HeliusAdapter({ apiKey: 'your-helius-key' }),
|
|
303
|
+
})
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
### PostgresAdapter *(coming soon)*
|
|
307
|
+
|
|
308
|
+
Query your own Geyser-indexed database. Enables true range queries, sorting, and aggregation at the database level.
|
|
309
|
+
|
|
310
|
+
```typescript
|
|
311
|
+
import { PostgresAdapter } from 'curvhex-orm/adapters'
|
|
312
|
+
|
|
313
|
+
const orm = new CurvhexORM({
|
|
314
|
+
connection,
|
|
315
|
+
programId: 'YOUR_PROGRAM_ID',
|
|
316
|
+
models: { UserAccount },
|
|
317
|
+
adapter: new PostgresAdapter({
|
|
318
|
+
connectionString: 'postgresql://user:pass@localhost:5432/mydb',
|
|
319
|
+
table: 'user_accounts',
|
|
320
|
+
}),
|
|
321
|
+
})
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
---
|
|
325
|
+
|
|
326
|
+
## Architecture
|
|
327
|
+
|
|
328
|
+
### Why adapters?
|
|
329
|
+
|
|
330
|
+
Solana's native RPC (`getProgramAccounts`) only supports exact byte matching. Range queries, sorting, and aggregation require off-chain infrastructure.
|
|
331
|
+
|
|
332
|
+
Rather than picking one solution, Curvhex ORM abstracts the data source:
|
|
333
|
+
|
|
334
|
+
```
|
|
335
|
+
findMany({ where: { balance: { gt: 100n } } })
|
|
336
|
+
│
|
|
337
|
+
├── RpcAdapter → fetch all + client-side filter
|
|
338
|
+
├── HeliusAdapter → Helius API query
|
|
339
|
+
└── PostgresAdapter → SELECT * WHERE balance > 100
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
As the Solana ecosystem matures (Triton's RPC 2.0, indexers), adapters can be upgraded independently.
|
|
343
|
+
|
|
344
|
+
### On-chain vs off-chain filtering
|
|
345
|
+
|
|
346
|
+
| Filter type | RpcAdapter | HeliusAdapter | PostgresAdapter |
|
|
347
|
+
|-------------|-----------|---------------|-----------------|
|
|
348
|
+
| Equality (`authority = X`) | ✅ on-chain memcmp | ✅ | ✅ |
|
|
349
|
+
| Range (`balance > 100`) | ⚠️ client-side | ✅ | ✅ |
|
|
350
|
+
| Sorting | ⚠️ client-side | ✅ | ✅ |
|
|
351
|
+
| Aggregation | ⚠️ client-side | partial | ✅ |
|
|
352
|
+
| Relations (`include`) | ⚠️ N+1 fetches | ⚠️ N+1 fetches | ✅ JOIN |
|
|
353
|
+
|
|
354
|
+
### Data consistency
|
|
355
|
+
|
|
356
|
+
When using off-chain adapters (Helius, Postgres), there is an inherent indexer lag of 1–2 slots (~400–800ms). For critical reads (balance checks before transactions), use `findByAddress` or `findByPda` which always hit the RPC directly.
|
|
357
|
+
|
|
358
|
+
---
|
|
359
|
+
|
|
360
|
+
## Roadmap
|
|
361
|
+
|
|
362
|
+
- [x] Schema definition with automatic offset calculation
|
|
363
|
+
- [x] Borsh deserialization (all primitive types)
|
|
364
|
+
- [x] `findMany` — memcmp filters + client-side range operators
|
|
365
|
+
- [x] `findFirst`, `findByAddress`, `findByPda`
|
|
366
|
+
- [x] `count`, `aggregate`, `groupBy`
|
|
367
|
+
- [x] `include` — relation loading with deduplication
|
|
368
|
+
- [x] Anchor discriminator helper (`anchor()`)
|
|
369
|
+
- [x] Adapter pattern (`QueryAdapter` interface)
|
|
370
|
+
- [x] `RpcAdapter`
|
|
371
|
+
- [ ] `HeliusAdapter`
|
|
372
|
+
- [ ] `PostgresAdapter` + Geyser sync guide
|
|
373
|
+
- [ ] Cursor-based pagination
|
|
374
|
+
- [ ] WebSocket subscriptions (`watch`)
|
|
375
|
+
- [ ] Enum field support
|
|
376
|
+
- [ ] Option<T> field support
|
|
377
|
+
- [ ] Vec<T> field support
|
|
378
|
+
- [ ] CLI: generate schema from IDL
|
|
379
|
+
- [ ] RPC 2.0 adapter (Triton)
|
|
380
|
+
|
|
381
|
+
---
|
|
382
|
+
|
|
383
|
+
## Contributing
|
|
384
|
+
|
|
385
|
+
Contributions are welcome. Here's how to get started:
|
|
386
|
+
|
|
387
|
+
### Setup
|
|
388
|
+
|
|
389
|
+
```bash
|
|
390
|
+
git clone https://github.com/your-username/curvhex-orm
|
|
391
|
+
cd curvhex-orm
|
|
392
|
+
npm install
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
### Project structure
|
|
396
|
+
|
|
397
|
+
```
|
|
398
|
+
src/
|
|
399
|
+
├── core/
|
|
400
|
+
│ ├── types.ts — field types, ModelDefinition, InferModel
|
|
401
|
+
│ ├── schema.ts — defineModel, anchor(), offset calculation
|
|
402
|
+
│ ├── deserializer.ts — Buffer → TypeScript object
|
|
403
|
+
│ └── filters.ts — memcmp builder, client-side filter logic
|
|
404
|
+
├── adapters/
|
|
405
|
+
│ ├── abstract/
|
|
406
|
+
│ │ └── QueryAdapter.ts — adapter interface
|
|
407
|
+
│ └── RpcAdapter.ts — getProgramAccounts implementation
|
|
408
|
+
├── client/
|
|
409
|
+
│ ├── CurvhexClient.ts — findMany, findFirst, aggregate, groupBy, include
|
|
410
|
+
│ └── CurvhexORM.ts — entry point, wires models to adapter
|
|
411
|
+
└── __tests__/
|
|
412
|
+
└── integration.test.ts
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
### Where to contribute
|
|
416
|
+
|
|
417
|
+
Good first issues:
|
|
418
|
+
|
|
419
|
+
- **`HeliusAdapter`** — implement `QueryAdapter` using the Helius DAS API
|
|
420
|
+
- **`PostgresAdapter`** — implement `QueryAdapter` using `pg` or `postgres` client
|
|
421
|
+
- **`Vec<T>` field support** — extend `FieldType` and the deserializer
|
|
422
|
+
- **`Option<T>` field support** — handle Borsh option prefix byte
|
|
423
|
+
- **Enum fields** — map `u8` values to string variants via schema
|
|
424
|
+
- **Cursor-based pagination** — add `cursor` to `FindManyOptions`
|
|
425
|
+
- **`watch()`** — WebSocket subscription wrapping `onAccountChange`
|
|
426
|
+
|
|
427
|
+
### Implementing a new adapter
|
|
428
|
+
|
|
429
|
+
Create a file under `src/adapters/` and implement the `QueryAdapter` interface:
|
|
430
|
+
|
|
431
|
+
```typescript
|
|
432
|
+
import { QueryAdapter, FindManyOptions } from './abstract/QueryAdapter'
|
|
433
|
+
import { ModelDefinition, InferModel } from '../core/types'
|
|
434
|
+
|
|
435
|
+
export class MyAdapter implements QueryAdapter {
|
|
436
|
+
async findMany<M extends ModelDefinition>(
|
|
437
|
+
model: M,
|
|
438
|
+
options: FindManyOptions<M>
|
|
439
|
+
): Promise<InferModel<M>[]> {
|
|
440
|
+
// your implementation
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
async findByAddress<M extends ModelDefinition>(
|
|
444
|
+
model: M,
|
|
445
|
+
address: string
|
|
446
|
+
): Promise<InferModel<M> | null> {
|
|
447
|
+
// your implementation
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
async findByPda<M extends ModelDefinition>(
|
|
451
|
+
model: M,
|
|
452
|
+
seeds: (Buffer | Uint8Array)[]
|
|
453
|
+
): Promise<InferModel<M> | null> {
|
|
454
|
+
// your implementation
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
```
|
|
458
|
+
|
|
459
|
+
Then export it from `src/index.ts` and add a test case to `integration.test.ts`.
|
|
460
|
+
|
|
461
|
+
### Before submitting a PR
|
|
462
|
+
|
|
463
|
+
```bash
|
|
464
|
+
npx tsc --noEmit # type check
|
|
465
|
+
npx tsx src/__tests__/integration.test.ts # run tests
|
|
466
|
+
```
|
|
467
|
+
|
|
468
|
+
All 8 integration test cases must pass. If you're adding a new adapter, add at least `findByAddress` and `findMany` test cases using a mock or a real endpoint.
|
|
469
|
+
|
|
470
|
+
### Commit style
|
|
471
|
+
|
|
472
|
+
```
|
|
473
|
+
feat: add HeliusAdapter
|
|
474
|
+
fix: handle empty discriminator in buildFilters
|
|
475
|
+
docs: add Vec<T> field type to README
|
|
476
|
+
```
|
|
477
|
+
|
|
478
|
+
---
|
|
479
|
+
|
|
480
|
+
## License
|
|
481
|
+
|
|
482
|
+
Apache 2.0 with Commons Clause.
|
|
483
|
+
|
|
484
|
+
Free to use in your own projects. You may not sell this software or offer it as a hosted service without a commercial license.
|
|
485
|
+
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { Connection, PublicKey } from '@solana/web3.js';
|
|
2
|
+
import { ModelDefinition, InferModel } from '../core/types';
|
|
3
|
+
import { QueryAdapter, FindManyOptions } from './abstract/QueryAdapter';
|
|
4
|
+
export declare class RpcAdapter implements QueryAdapter {
|
|
5
|
+
private connection;
|
|
6
|
+
private programId;
|
|
7
|
+
constructor(connection: Connection, programId: PublicKey);
|
|
8
|
+
findMany<M extends ModelDefinition>(model: M, options?: FindManyOptions<M>): Promise<InferModel<M>[]>;
|
|
9
|
+
findByAddress<M extends ModelDefinition>(model: M, address: string): Promise<InferModel<M> | null>;
|
|
10
|
+
findByPda<M extends ModelDefinition>(model: M, seeds: (Buffer | Uint8Array)[]): Promise<InferModel<M> | null>;
|
|
11
|
+
}
|
|
12
|
+
//# sourceMappingURL=RpcAdapter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"RpcAdapter.d.ts","sourceRoot":"","sources":["../../src/adapters/RpcAdapter.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,SAAS,EAA4B,MAAM,iBAAiB,CAAC;AAClF,OAAO,EAAE,eAAe,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAG5D,OAAO,EAAE,YAAY,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAExE,qBAAa,UAAW,YAAW,YAAY;IAEvC,OAAO,CAAC,UAAU;IAClB,OAAO,CAAC,SAAS;gBADT,UAAU,EAAE,UAAU,EACtB,SAAS,EAAE,SAAS;IAG1B,QAAQ,CAAC,CAAC,SAAS,eAAe,EACpC,KAAK,EAAE,CAAC,EACR,OAAO,GAAE,eAAe,CAAC,CAAC,CAAM,GACjC,OAAO,CAAC,UAAU,CAAC,CAAC,CAAC,EAAE,CAAC;IA2CrB,aAAa,CAAC,CAAC,SAAS,eAAe,EACzC,KAAK,EAAE,CAAC,EACR,OAAO,EAAE,MAAM,GAChB,OAAO,CAAC,UAAU,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC;IAO1B,SAAS,CAAC,CAAC,SAAS,eAAe,EACrC,KAAK,EAAE,CAAC,EACR,KAAK,EAAE,CAAC,MAAM,GAAG,UAAU,CAAC,EAAE,GAC/B,OAAO,CAAC,UAAU,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC;CAMnC"}
|