@molpha/sdk 0.1.0 → 0.2.0-dev-20260911113923
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 +128 -82
- package/dist/{chunk-MFELBWSL.js → chunk-DT6IKCWB.js} +8 -4
- package/dist/chunk-DT6IKCWB.js.map +1 -0
- package/dist/index.d.ts +266 -99
- package/dist/index.js +6755 -2188
- package/dist/index.js.map +1 -1
- package/dist/utils.d.ts +1 -1
- package/dist/utils.js +1 -1
- package/dist/{wallet-CbpYQO4l.d.ts → wallet-udLZ5Iy0.d.ts} +23 -12
- package/idl/molpha.json +6385 -2009
- package/package.json +26 -17
- package/dist/chunk-MFELBWSL.js.map +0 -1
package/README.md
CHANGED
|
@@ -5,9 +5,9 @@ Browser-first TypeScript SDK for **Molpha data consumers and feed owners**.
|
|
|
5
5
|
Use it to:
|
|
6
6
|
|
|
7
7
|
- subscribe to a Molpha plan on Solana;
|
|
8
|
-
- derive a
|
|
9
|
-
- request a threshold-signed
|
|
10
|
-
- submit the signed result on-chain;
|
|
8
|
+
- derive a source id from an API config;
|
|
9
|
+
- request a threshold-signed attestation from the gateway;
|
|
10
|
+
- submit the signed result on-chain (`submit_attestation`);
|
|
11
11
|
- verify/read the latest feed value;
|
|
12
12
|
- build EVM and Starknet verifier arguments from the same signed result.
|
|
13
13
|
|
|
@@ -20,12 +20,13 @@ Molpha turns off-chain API responses into verified on-chain data.
|
|
|
20
20
|
At a high level:
|
|
21
21
|
|
|
22
22
|
```text
|
|
23
|
-
Consumer
|
|
23
|
+
Consumer
|
|
24
24
|
└─ subscribes (USDC) on Solana
|
|
25
|
-
└─ derives
|
|
25
|
+
└─ derives sourceId = keccak256(canonical apiConfig)
|
|
26
|
+
└─ signs a RequestAuth bound to (programId, gateway, sourceId, signaturesRequired, timestamp)
|
|
26
27
|
|
|
27
28
|
Gateway
|
|
28
|
-
└─ coordinates a signing round for
|
|
29
|
+
└─ coordinates a signing round for (sourceId, signaturesRequired)
|
|
29
30
|
|
|
30
31
|
Verifier nodes
|
|
31
32
|
└─ fetch/recompute the API result independently
|
|
@@ -38,7 +39,18 @@ Solana / EVM / Starknet verifiers
|
|
|
38
39
|
|
|
39
40
|
The gateway is a coordination layer, not a trusted oracle. A result is trusted only if it carries a valid threshold signature from the selected verifier nodes for the current registry version.
|
|
40
41
|
|
|
41
|
-
Molpha uses Solana as the canonical protocol chain for subscriptions, registry
|
|
42
|
+
Molpha uses Solana as the canonical protocol chain for subscriptions, registry snapshots, node accounts, and feed state. EVM and Starknet verifier contracts are stateless verification surfaces: they verify signed Molpha attestations without managing subscriptions or source configuration locally.
|
|
43
|
+
|
|
44
|
+
Every signed attestation commits to the same message across chains:
|
|
45
|
+
|
|
46
|
+
```text
|
|
47
|
+
message = keccak256(
|
|
48
|
+
keccak256("MOLPHA_MESSAGE_V1") || sourceId || u32be(registryVersion) ||
|
|
49
|
+
u32be(signaturesRequired) || signersBitmap || value || u64be(canonicalTimestamp)
|
|
50
|
+
)
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
`attestationMessageHash` / `attestationMessageHashFromResult` recompute it client-side.
|
|
42
54
|
|
|
43
55
|
## Install
|
|
44
56
|
|
|
@@ -59,7 +71,7 @@ Runtime dependencies include `@solana/kit`, `@anchor-lang/core`, and `@noble/*`.
|
|
|
59
71
|
|
|
60
72
|
| Import | Use |
|
|
61
73
|
|---|---|
|
|
62
|
-
| `@molpha/sdk` | Facade (`MolphaSDK`), `MolphaGateway`, `MolphaSolanaClient`, core
|
|
74
|
+
| `@molpha/sdk` | Facade (`MolphaSDK`), `MolphaGateway`, `MolphaSolanaClient`, core hashing (`deriveSourceId`, `attestationMessageHash`, `hashRequestAuth`), EVM/Starknet helpers. Browser-safe; no `fs` in the main entry. |
|
|
63
75
|
| `@molpha/sdk/utils` | `walletFromKeypairFile`, `loadKeypair` — load a Solana CLI keypair as an Anchor `Wallet`. Node.js only. |
|
|
64
76
|
|
|
65
77
|
The package is ESM with `"sideEffects": false`, so gateway-only or read-only apps can tree-shake unused paths.
|
|
@@ -68,18 +80,14 @@ The package is ESM with `"sideEffects": false`, so gateway-only or read-only app
|
|
|
68
80
|
|
|
69
81
|
```ts
|
|
70
82
|
import { web3 } from "@anchor-lang/core";
|
|
71
|
-
import {
|
|
72
|
-
MolphaSDK,
|
|
73
|
-
PlanType,
|
|
74
|
-
deriveApiConfigHash,
|
|
75
|
-
deriveFeedIdString,
|
|
76
|
-
} from "@molpha/sdk";
|
|
83
|
+
import { MolphaSDK, PlanType, deriveSourceIdString } from "@molpha/sdk";
|
|
77
84
|
import { walletFromKeypairFile } from "@molpha/sdk/utils";
|
|
78
85
|
|
|
79
86
|
const wallet = walletFromKeypairFile("~/.config/solana/id.json");
|
|
80
87
|
const sdk = new MolphaSDK({
|
|
81
88
|
connection: new web3.Connection("https://api.devnet.solana.com", "confirmed"),
|
|
82
89
|
wallet,
|
|
90
|
+
// endpoints: [{ url: "https://gateway.example.com", gatewayAuthority: "<base58>" }],
|
|
83
91
|
});
|
|
84
92
|
|
|
85
93
|
// Subscribe if needed (USDC on Solana)
|
|
@@ -93,19 +101,17 @@ const apiConfig = {
|
|
|
93
101
|
responseParser: "$.price",
|
|
94
102
|
};
|
|
95
103
|
const signaturesRequired = 3;
|
|
96
|
-
const feedId = deriveFeedIdString(
|
|
97
|
-
wallet.publicKey.toBytes(),
|
|
98
|
-
deriveApiConfigHash(apiConfig),
|
|
99
|
-
signaturesRequired,
|
|
100
|
-
);
|
|
101
104
|
|
|
102
|
-
const { result, signature } = await sdk.requestAndSubmit(
|
|
105
|
+
const { result, signature, feed } = await sdk.requestAndSubmit({
|
|
103
106
|
apiConfig,
|
|
104
107
|
signaturesRequired,
|
|
105
108
|
});
|
|
109
|
+
|
|
110
|
+
// The round's identity, for reads and cross-chain verification.
|
|
111
|
+
const sourceId = deriveSourceIdString(apiConfig); // === result.sourceId
|
|
106
112
|
```
|
|
107
113
|
|
|
108
|
-
`requestAndSubmit` requests a threshold-signed
|
|
114
|
+
`requestAndSubmit` requests a threshold-signed attestation from the gateway (against the current on-chain registry version) and submits it to Solana via `submit_attestation` in one call. The first successful submit creates the feed account for `(sourceId, signaturesRequired, submitter)` if it does not already exist.
|
|
109
115
|
|
|
110
116
|
## Configuration
|
|
111
117
|
|
|
@@ -120,8 +126,8 @@ const { result, signature } = await sdk.requestAndSubmit(feedId, {
|
|
|
120
126
|
|
|
121
127
|
| Option | Default |
|
|
122
128
|
|---|---|
|
|
123
|
-
| `endpoints` | `DEFAULT_GATEWAY_ENDPOINT` —
|
|
124
|
-
| `programId` | `MOLPHA_PROGRAM_ADDRESS` from the vendored IDL |
|
|
129
|
+
| `endpoints` | `DEFAULT_GATEWAY_ENDPOINT` — URL, `{ url, gatewayAuthority }`, or an array of either for failover (see [Gateway identity](#gateway-identity)) |
|
|
130
|
+
| `programId` | `MOLPHA_PROGRAM_ADDRESS` from the vendored IDL; also bound into gateway request auth |
|
|
125
131
|
| `idl` | `MOLPHA_IDL` from `idl/molpha.json` |
|
|
126
132
|
| `commitment` | `"confirmed"` |
|
|
127
133
|
|
|
@@ -135,20 +141,42 @@ import {
|
|
|
135
141
|
const sdk = new MolphaSDK({
|
|
136
142
|
connection,
|
|
137
143
|
wallet,
|
|
138
|
-
endpoints: [
|
|
144
|
+
endpoints: [
|
|
145
|
+
DEFAULT_GATEWAY_ENDPOINT,
|
|
146
|
+
{ url: "https://backup.example.com", gatewayAuthority: "<gateway base58 pubkey>" },
|
|
147
|
+
],
|
|
139
148
|
// programId: "YourProgramAddress...",
|
|
140
149
|
// idl: MOLPHA_IDL,
|
|
141
150
|
});
|
|
142
151
|
```
|
|
143
152
|
|
|
153
|
+
### Gateway identity
|
|
154
|
+
|
|
155
|
+
Gateway request authorization binds the gateway's on-chain account, so the client must know which gateway it is talking to:
|
|
156
|
+
|
|
157
|
+
```text
|
|
158
|
+
requestAuthHash = keccak256(
|
|
159
|
+
"MOLPHA_REQAUTH_V1" || programId || gatewayPda || sourceId || u8(signaturesRequired) || u64le(timestamp)
|
|
160
|
+
)
|
|
161
|
+
gatewayPda = PDA(["molpha_gateway", gatewayAuthority], programId)
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
Pass `gatewayAuthority` (the gateway's base58 signing pubkey) per endpoint to pin it. When omitted, the SDK calls `GET {url}/v1/info` once per endpoint and reads:
|
|
165
|
+
|
|
166
|
+
```json
|
|
167
|
+
{ "status": "ok", "data": { "gatewayAuthority": "<base58>", "programId": "<base58>" } }
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
A `programId` that differs from the client's is rejected. Because the hash differs per gateway, the auth signature is recomputed for every endpoint actually tried during failover — with a browser wallet that means one signing prompt per endpoint tried. `/v1/info` is never contacted when no signer is configured (dev zero-signature path).
|
|
171
|
+
|
|
144
172
|
## Wallet
|
|
145
173
|
|
|
146
174
|
`wallet` is a single `MolphaWallet` used across both protocol surfaces:
|
|
147
175
|
|
|
148
176
|
| Layer | What it signs |
|
|
149
177
|
|---|---|
|
|
150
|
-
| Solana client | Transactions such as `subscribe`, `extendSubscription`, `
|
|
151
|
-
| Gateway client | `
|
|
178
|
+
| Solana client | Transactions such as `subscribe`, `extendSubscription`, `submitAttestation` |
|
|
179
|
+
| Gateway client | `hashRequestAuth({ programId, gateway, sourceId, signaturesRequired, timestamp })` for authenticated gateway requests |
|
|
152
180
|
|
|
153
181
|
Gateway auth is resolved automatically when you use `MolphaSDK`:
|
|
154
182
|
|
|
@@ -157,7 +185,7 @@ Gateway auth is resolved automatically when you use `MolphaSDK`:
|
|
|
157
185
|
3. Else omit auth and use an all-zero `authSig`.
|
|
158
186
|
|
|
159
187
|
`MolphaSDK` passes the resolved signer to `sdk.gateway` as its default, so
|
|
160
|
-
`sdk.gateway.requestSignedData({
|
|
188
|
+
`sdk.gateway.requestSignedData({ apiConfig, signaturesRequired })` authenticates without an
|
|
161
189
|
explicit `signer`. Standalone `new MolphaGateway(...)` omits auth unless you pass
|
|
162
190
|
a `defaultSigner` (third constructor arg) or per-call `signer`.
|
|
163
191
|
|
|
@@ -223,12 +251,12 @@ const { pricePaid } = await sdk.solana.subscribe(PlanType.Basic, {
|
|
|
223
251
|
|
|
224
252
|
`maxPriceUsdc` is a safety bound. The transaction aborts if the live plan price is higher than the amount the user approved.
|
|
225
253
|
|
|
226
|
-
### 2.
|
|
254
|
+
### 2. Source id
|
|
227
255
|
|
|
228
|
-
There is no
|
|
256
|
+
There is no create-feed instruction. A data source is identified by its canonical API config:
|
|
229
257
|
|
|
230
258
|
```text
|
|
231
|
-
|
|
259
|
+
sourceId = keccak256(JSON.stringify(canonicalizeAPIConfig(apiConfig)))
|
|
232
260
|
```
|
|
233
261
|
|
|
234
262
|
```ts
|
|
@@ -237,21 +265,16 @@ const apiConfig = {
|
|
|
237
265
|
responseParser: "$.price",
|
|
238
266
|
};
|
|
239
267
|
|
|
240
|
-
const
|
|
241
|
-
const feedId = deriveFeedIdString(
|
|
242
|
-
wallet.publicKey.toBytes(),
|
|
243
|
-
deriveApiConfigHash(apiConfig),
|
|
244
|
-
signaturesRequired,
|
|
245
|
-
);
|
|
268
|
+
const sourceId = deriveSourceIdString(apiConfig); // 64 hex chars, no 0x
|
|
246
269
|
```
|
|
247
270
|
|
|
248
|
-
The
|
|
271
|
+
The gateway, the Solana program and the EVM/Starknet verifiers all recompute `sourceId` from the same canonical config, so pass the same `apiConfig` (including `{{secret.*}}` placeholders) every time. `signaturesRequired` is **not** part of `sourceId`: on Solana a feed account is keyed by `(sourceId, signaturesRequired, submitter)`, so the same source can be tracked at different quorums.
|
|
249
272
|
|
|
250
273
|
### 3. Request signed data from the gateway
|
|
251
274
|
|
|
252
275
|
```ts
|
|
276
|
+
const signaturesRequired = 3;
|
|
253
277
|
const result = await sdk.gateway.requestSignedData({
|
|
254
|
-
feedId,
|
|
255
278
|
apiConfig,
|
|
256
279
|
signaturesRequired,
|
|
257
280
|
});
|
|
@@ -259,24 +282,24 @@ const result = await sdk.gateway.requestSignedData({
|
|
|
259
282
|
|
|
260
283
|
The gateway round uses the current on-chain registry version. Selected verifier nodes independently fetch/recompute the result and sign only if the observed value matches the canonical result.
|
|
261
284
|
|
|
262
|
-
The returned `DataUpdateResult` includes the signed value, canonical timestamp, registry version, required quorum, signer bitmap, and aggregate signature.
|
|
285
|
+
The returned `DataUpdateResult` includes `sourceId`, the signed value, canonical timestamp, registry version, required quorum, signer bitmap, and aggregate signature. `signaturesRequired` must be at least the protocol's `min_signers` (currently 3) or the chain rejects the submit.
|
|
263
286
|
|
|
264
287
|
### 4. Submit on Solana
|
|
265
288
|
|
|
266
289
|
```ts
|
|
267
|
-
const { signature } = await sdk.solana.
|
|
290
|
+
const { signature, feed } = await sdk.solana.submitAttestation(result);
|
|
268
291
|
```
|
|
269
292
|
|
|
270
|
-
Then read the
|
|
293
|
+
Then read the feed this wallet wrote for that source and quorum:
|
|
271
294
|
|
|
272
295
|
```ts
|
|
273
|
-
const
|
|
296
|
+
const feedState = await sdk.solana.readFeed(result.sourceId, signaturesRequired);
|
|
274
297
|
```
|
|
275
298
|
|
|
276
299
|
### One-call request + submit
|
|
277
300
|
|
|
278
301
|
```ts
|
|
279
|
-
const { result, signature } = await sdk.requestAndSubmit(
|
|
302
|
+
const { result, signature, feed } = await sdk.requestAndSubmit({
|
|
280
303
|
apiConfig,
|
|
281
304
|
signaturesRequired,
|
|
282
305
|
});
|
|
@@ -285,23 +308,24 @@ const { result, signature } = await sdk.requestAndSubmit(feedId, {
|
|
|
285
308
|
This is equivalent to:
|
|
286
309
|
|
|
287
310
|
```ts
|
|
288
|
-
const result = await sdk.gateway.requestSignedData({
|
|
289
|
-
const { signature } = await sdk.solana.
|
|
311
|
+
const result = await sdk.gateway.requestSignedData({ apiConfig, signaturesRequired });
|
|
312
|
+
const { signature, feed } = await sdk.solana.submitAttestation(result);
|
|
290
313
|
```
|
|
291
314
|
|
|
292
315
|
### Fast requests with a cached context
|
|
293
316
|
|
|
294
|
-
By default every `requestSignedData` call
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
fetch these once and reuse them so each
|
|
317
|
+
By default every `requestSignedData` call reads the on-chain registry up front
|
|
318
|
+
(current version, redundancy buffer and node count of that snapshot) and fetches
|
|
319
|
+
the gateway node set only when the round needs it (private API encryption). When
|
|
320
|
+
you run many rounds for the same source, fetch these once and reuse them so each
|
|
321
|
+
round is a single gateway POST.
|
|
298
322
|
|
|
299
323
|
```ts
|
|
300
|
-
// Fetch registryVersion + redundancyBuffer + nodes once.
|
|
301
|
-
const context = await sdk.gateway.prepareContext(
|
|
324
|
+
// Fetch registryVersion + redundancyBuffer + nodeCount + nodes once.
|
|
325
|
+
const context = await sdk.gateway.prepareContext();
|
|
302
326
|
|
|
303
327
|
// Reuse it across rounds — no prelude fetches.
|
|
304
|
-
const result = await sdk.gateway.requestSignedData({
|
|
328
|
+
const result = await sdk.gateway.requestSignedData({ apiConfig, signaturesRequired, context });
|
|
305
329
|
```
|
|
306
330
|
|
|
307
331
|
`context` is a `Partial<RoundContext>`, so you can cache only what you have
|
|
@@ -309,25 +333,24 @@ and let `requestSignedData` fetch the rest:
|
|
|
309
333
|
|
|
310
334
|
```ts
|
|
311
335
|
const result = await sdk.gateway.requestSignedData({
|
|
312
|
-
feedId,
|
|
313
336
|
apiConfig,
|
|
314
337
|
signaturesRequired,
|
|
315
|
-
context: { nodes }, // registryVersion + redundancyBuffer still fetched fresh
|
|
338
|
+
context: { nodes }, // registryVersion + redundancyBuffer + nodeCount still fetched fresh
|
|
316
339
|
});
|
|
317
340
|
```
|
|
318
341
|
|
|
319
342
|
Caching is opt-in because these inputs can drift. A stale `registryVersion`,
|
|
320
|
-
`redundancyBuffer`, or node set yields a result the chain will reject —
|
|
321
|
-
the context when the on-chain registry changes. The
|
|
322
|
-
|
|
343
|
+
`redundancyBuffer`, `nodeCount`, or node set yields a result the chain will reject —
|
|
344
|
+
refresh the context when the on-chain registry changes. The SDK derives the
|
|
345
|
+
selection from the on-chain `nodeCount` and refuses a cached node list whose length
|
|
346
|
+
disagrees with it. The same `context` field is accepted by `requestAndSubmit`.
|
|
323
347
|
|
|
324
348
|
## Private APIs and encrypted secrets
|
|
325
349
|
|
|
326
|
-
|
|
350
|
+
Sources can use private APIs without sending plaintext secrets to the gateway.
|
|
327
351
|
|
|
328
352
|
```ts
|
|
329
353
|
const result = await sdk.gateway.requestSignedData({
|
|
330
|
-
feedId,
|
|
331
354
|
apiConfig: {
|
|
332
355
|
url: "https://api.example.com/private-price?key={{secret.apiKey}}",
|
|
333
356
|
responseParser: "$.price",
|
|
@@ -341,7 +364,7 @@ const result = await sdk.gateway.requestSignedData({
|
|
|
341
364
|
});
|
|
342
365
|
```
|
|
343
366
|
|
|
344
|
-
`MolphaSDK` wires `verifyNodeKeys` to `solana.verifyNodeKeysForPrivateApi`, which authenticates gateway node encryption keys against on-chain Node accounts before secrets are encrypted.
|
|
367
|
+
`MolphaSDK` wires `verifyNodeKeys` to `solana.verifyNodeKeysForPrivateApi`, which authenticates gateway node encryption keys against the on-chain `Node` accounts of the round's registry snapshot (`registry.nodes[index]`) before secrets are encrypted.
|
|
345
368
|
|
|
346
369
|
Secrets are encrypted into per-node envelopes. The gateway coordinates the round but should not receive plaintext API credentials.
|
|
347
370
|
|
|
@@ -371,7 +394,7 @@ Supported network ids (selection helpers only): `evm-sepolia`, `arbitrum-sepolia
|
|
|
371
394
|
```ts
|
|
372
395
|
import { buildEvmVerifierArgs } from "@molpha/sdk";
|
|
373
396
|
|
|
374
|
-
const result = await sdk.gateway.requestSignedData({
|
|
397
|
+
const result = await sdk.gateway.requestSignedData({ apiConfig, signaturesRequired });
|
|
375
398
|
|
|
376
399
|
const { dataUpdate, signature } = buildEvmVerifierArgs(result);
|
|
377
400
|
```
|
|
@@ -380,7 +403,7 @@ The generated tuples match the Molpha EVM verifier ABI:
|
|
|
380
403
|
|
|
381
404
|
```ts
|
|
382
405
|
// dataUpdate:
|
|
383
|
-
// [bytes32
|
|
406
|
+
// [bytes32 sourceId,
|
|
384
407
|
// uint32 registryVersion,
|
|
385
408
|
// uint32 signaturesRequired,
|
|
386
409
|
// bytes32 valuePacked,
|
|
@@ -392,7 +415,7 @@ The generated tuples match the Molpha EVM verifier ABI:
|
|
|
392
415
|
// uint256 signersBitmap]
|
|
393
416
|
```
|
|
394
417
|
|
|
395
|
-
Note: the
|
|
418
|
+
Note: the deployed contract's source names the first struct field `jobId`; the SDK ABI names it `sourceId` (component names do not affect encoding) and the value is the 32-byte source id.
|
|
396
419
|
|
|
397
420
|
### ethers
|
|
398
421
|
|
|
@@ -437,7 +460,7 @@ await client.readContract({
|
|
|
437
460
|
functionName: "verify",
|
|
438
461
|
args: [
|
|
439
462
|
{
|
|
440
|
-
|
|
463
|
+
sourceId: dataUpdate[0],
|
|
441
464
|
registryVersion: dataUpdate[1],
|
|
442
465
|
signaturesRequired: dataUpdate[2],
|
|
443
466
|
value: dataUpdate[3],
|
|
@@ -494,17 +517,17 @@ const sepoliaDirect = MOLPHA_VERIFIER_STARKNET_SEPOLIA;
|
|
|
494
517
|
```ts
|
|
495
518
|
import { buildStarknetVerifierArgs } from "@molpha/sdk";
|
|
496
519
|
|
|
497
|
-
const result = await sdk.gateway.requestSignedData({
|
|
520
|
+
const result = await sdk.gateway.requestSignedData({ apiConfig, signaturesRequired });
|
|
498
521
|
|
|
499
522
|
const { dataUpdate, signature } = buildStarknetVerifierArgs(result);
|
|
500
523
|
```
|
|
501
524
|
|
|
502
|
-
The generated objects match the Molpha Starknet verifier interface:
|
|
525
|
+
The generated objects match the Molpha Starknet verifier interface (the Cairo struct still names the first field `feed_id`; positional calldata is unchanged):
|
|
503
526
|
|
|
504
527
|
```ts
|
|
505
528
|
// dataUpdate:
|
|
506
529
|
// {
|
|
507
|
-
//
|
|
530
|
+
// source_id: u256,
|
|
508
531
|
// registry_version: u32,
|
|
509
532
|
// signatures_required: u32,
|
|
510
533
|
// value: u256,
|
|
@@ -530,18 +553,17 @@ import {
|
|
|
530
553
|
|
|
531
554
|
## What verification checks
|
|
532
555
|
|
|
533
|
-
A Molpha
|
|
556
|
+
A Molpha attestation is valid only if the verifier can confirm:
|
|
534
557
|
|
|
535
|
-
- the update targets the expected `
|
|
536
|
-
- the result was signed against a specific `registryVersion
|
|
558
|
+
- the update targets the expected `sourceId`;
|
|
559
|
+
- the result was signed against a specific `registryVersion` (an immutable node-set snapshot);
|
|
537
560
|
- the quorum satisfies `signaturesRequired`;
|
|
538
|
-
- the signer bitmap
|
|
539
|
-
- the aggregate Schnorr signature is valid;
|
|
540
|
-
- the signed value and canonical timestamp match the message;
|
|
561
|
+
- the signer bitmap is a subset of the deterministic selection for `(sourceId, registryVersion, canonicalTimestamp)`;
|
|
562
|
+
- the aggregate Schnorr signature over `attestationMessageHash(...)` is valid;
|
|
541
563
|
- the timestamp is within the accepted freshness bounds;
|
|
542
|
-
- on Solana,
|
|
564
|
+
- on Solana, the signer `Node` accounts passed as remaining accounts are exactly `registry.nodes[bit]` for every set bit.
|
|
543
565
|
|
|
544
|
-
Solana verification finalizes feed state via `
|
|
566
|
+
Solana verification finalizes feed state via `submit_attestation`. EVM and Starknet verification are stateless and return whether the signed Molpha attestation is valid for the deployed verifier registry.
|
|
545
567
|
|
|
546
568
|
## IDL vendoring
|
|
547
569
|
|
|
@@ -595,9 +617,9 @@ The facade wires the registry selection config resolver, gateway signer, subscri
|
|
|
595
617
|
Current scope:
|
|
596
618
|
|
|
597
619
|
- Solana subscription and extend flow;
|
|
598
|
-
- deterministic
|
|
599
|
-
- gateway signed-data requests (failover, retries, context cache);
|
|
600
|
-
- Solana
|
|
620
|
+
- deterministic source id and attestation message hashing;
|
|
621
|
+
- gateway signed-data requests (failover, retries, per-gateway request auth, context cache);
|
|
622
|
+
- Solana attestation submission and feed/registry reads;
|
|
601
623
|
- private API encryption helpers (pre-production);
|
|
602
624
|
- EVM and Starknet verifier argument building;
|
|
603
625
|
- deployed testnet verifier address helpers.
|
|
@@ -609,7 +631,25 @@ Known limitations:
|
|
|
609
631
|
- production deployments should use authenticated gateway requests;
|
|
610
632
|
- testnet verifier addresses may change between protocol releases.
|
|
611
633
|
|
|
612
|
-
Solana paths such as selection bitmap
|
|
634
|
+
Solana paths such as selection bitmap and `submit_attestation` remaining-accounts resolution are aligned with the Molpha program version vendored in this repo (`MoLFnEbuMS5gWnXNfUMLAYSqRM3eQZKWRzjeMQfqbT3`, not yet deployed).
|
|
635
|
+
|
|
636
|
+
## Migrating from 0.1.x
|
|
637
|
+
|
|
638
|
+
| Before | After |
|
|
639
|
+
|---|---|
|
|
640
|
+
| `deriveFeedId(owner, apiConfigHash, sigReq)` / `deriveFeedIdString` | removed — use `deriveSourceId(apiConfig)` / `deriveSourceIdString` |
|
|
641
|
+
| `deriveApiConfigHash(apiConfig)` | `deriveSourceId(apiConfig)` (old name kept as a deprecated alias, same bytes) |
|
|
642
|
+
| `requestSignedData({ feedId, ... })` | `requestSignedData({ apiConfig, signaturesRequired, ... })` — `sourceId` is derived from `apiConfig` |
|
|
643
|
+
| `prepareContext(feedId)` | `prepareContext()` |
|
|
644
|
+
| `requestAndSubmit(feedId, opts)` | `requestAndSubmit(opts)` |
|
|
645
|
+
| `authMessage(feedId, timestamp)` (sha256) | `hashRequestAuth({ programId, gateway, sourceId, signaturesRequired, timestamp })` (keccak) |
|
|
646
|
+
| `endpoints: string[]` | `endpoints: (string \| { url, gatewayAuthority })[]` |
|
|
647
|
+
| `submitDataUpdate(result)` | `submitAttestation(result)` (deprecated alias kept); returns `{ signature, feed }` |
|
|
648
|
+
| `readFeed(feedId)` | `readFeed(sourceId, signaturesRequired, submitter?)` |
|
|
649
|
+
| `result.feedId` / `NodeKeyVerifierArgs.feedId` | `.sourceId` |
|
|
650
|
+
| EVM tuple `feedId`, ABI `jobId` | `sourceId` |
|
|
651
|
+
| Starknet `feed_id` | `source_id` |
|
|
652
|
+
| `resolveRegistryIndexForVersion`, `VIRTUAL_INDEX`, `nodePda(index)` | removed — signer accounts are `registry.nodes[bit]`; `nodePda(owner)` |
|
|
613
653
|
|
|
614
654
|
## Develop
|
|
615
655
|
|
|
@@ -671,13 +711,19 @@ Versions follow semver and are driven by the nature of each change, not by the b
|
|
|
671
711
|
|
|
672
712
|
### One-time setup
|
|
673
713
|
|
|
674
|
-
1.
|
|
714
|
+
1. Configure npm [trusted publishing](https://docs.npmjs.com/trusted-publishers/) for `@molpha/sdk`:
|
|
675
715
|
|
|
676
716
|
```text
|
|
677
|
-
|
|
717
|
+
npmjs.com → @molpha/sdk → Settings → Trusted publishing
|
|
678
718
|
```
|
|
679
719
|
|
|
680
|
-
|
|
720
|
+
Add a trusted publisher for the release workflow:
|
|
721
|
+
|
|
722
|
+
| Workflow file | Branches | Purpose |
|
|
723
|
+
| ------------- | -------------- | ------------------------------- |
|
|
724
|
+
| `release.yml` | `main`, `dev` | Stable releases + dev snapshots |
|
|
725
|
+
|
|
726
|
+
Use organization `Molpha`, repository `sdk`, and the exact workflow filename (including `.yml`). No `NPM_TOKEN` secret is required.
|
|
681
727
|
|
|
682
728
|
2. Publish a stable release from `main` first.
|
|
683
729
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { web3 } from '@anchor-lang/core';
|
|
2
|
-
import { address, getAddressEncoder } from '@solana/kit';
|
|
2
|
+
import { address, getAddressEncoder, getAddressDecoder } from '@solana/kit';
|
|
3
3
|
import { ed25519 } from '@noble/curves/ed25519.js';
|
|
4
4
|
|
|
5
5
|
// src/solana/kit.ts
|
|
@@ -9,6 +9,7 @@ var ASSOCIATED_TOKEN_PROGRAM_ADDRESS = address(
|
|
|
9
9
|
);
|
|
10
10
|
var SYSTEM_PROGRAM_ADDRESS = address("11111111111111111111111111111111");
|
|
11
11
|
var addressEncoder = getAddressEncoder();
|
|
12
|
+
var addressDecoder = getAddressDecoder();
|
|
12
13
|
function toSolanaAddress(value) {
|
|
13
14
|
return address(typeof value === "string" ? value : value.toBase58());
|
|
14
15
|
}
|
|
@@ -18,6 +19,9 @@ function toPublicKey(value) {
|
|
|
18
19
|
function addressBytes(value) {
|
|
19
20
|
return Uint8Array.from(addressEncoder.encode(toSolanaAddress(value)));
|
|
20
21
|
}
|
|
22
|
+
function addressFromBytes(bytes) {
|
|
23
|
+
return addressDecoder.decode(bytes);
|
|
24
|
+
}
|
|
21
25
|
function findProgramAddressSync(seeds, programAddress) {
|
|
22
26
|
const [pda] = web3.PublicKey.findProgramAddressSync(seeds, toPublicKey(programAddress));
|
|
23
27
|
return toSolanaAddress(pda);
|
|
@@ -46,6 +50,6 @@ function gatewaySignerFromWallet(wallet) {
|
|
|
46
50
|
return void 0;
|
|
47
51
|
}
|
|
48
52
|
|
|
49
|
-
export { SYSTEM_PROGRAM_ADDRESS, TOKEN_PROGRAM_ADDRESS, addressBytes, findProgramAddressSync, gatewaySignerFromWallet, getAssociatedTokenAddressSync, keypairFromSecretKey, setComputeUnitLimit, signerFromKeypair, toPublicKey, toSolanaAddress };
|
|
50
|
-
//# sourceMappingURL=chunk-
|
|
51
|
-
//# sourceMappingURL=chunk-
|
|
53
|
+
export { SYSTEM_PROGRAM_ADDRESS, TOKEN_PROGRAM_ADDRESS, addressBytes, addressFromBytes, findProgramAddressSync, gatewaySignerFromWallet, getAssociatedTokenAddressSync, keypairFromSecretKey, setComputeUnitLimit, signerFromKeypair, toPublicKey, toSolanaAddress };
|
|
54
|
+
//# sourceMappingURL=chunk-DT6IKCWB.js.map
|
|
55
|
+
//# sourceMappingURL=chunk-DT6IKCWB.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/solana/kit.ts","../src/wallet.ts"],"names":[],"mappings":";;;;;AAaO,IAAM,qBAAA,GAAwB,QAAQ,6CAA6C;AACnF,IAAM,gCAAA,GAAmC,OAAA;AAAA,EAC9C;AACF,CAAA;AACO,IAAM,sBAAA,GAAyB,QAAQ,kCAAkC;AAEhF,IAAM,iBAAiB,iBAAA,EAAkB;AACzC,IAAM,iBAAiB,iBAAA,EAAkB;AAElC,SAAS,gBAAgB,KAAA,EAA+B;AAC7D,EAAA,OAAO,QAAQ,OAAO,KAAA,KAAU,WAAW,KAAA,GAAQ,KAAA,CAAM,UAAU,CAAA;AACrE;AAEO,SAAS,YAAY,KAAA,EAA2D;AACrF,EAAA,OAAO,iBAAiB,IAAA,CAAK,SAAA,GAAY,QAAQ,IAAI,IAAA,CAAK,UAAU,KAAK,CAAA;AAC3E;AAEO,SAAS,aAAa,KAAA,EAAkC;AAC7D,EAAA,OAAO,WAAW,IAAA,CAAK,cAAA,CAAe,OAAO,eAAA,CAAgB,KAAK,CAAC,CAAC,CAAA;AACtE;AAGO,SAAS,iBAAiB,KAAA,EAA4B;AAC3D,EAAA,OAAO,cAAA,CAAe,OAAO,KAAK,CAAA;AACpC;AAEO,SAAS,sBAAA,CACd,OACA,cAAA,EACS;AACT,EAAA,MAAM,CAAC,GAAG,CAAA,GAAI,IAAA,CAAK,UAAU,sBAAA,CAAuB,KAAA,EAAO,WAAA,CAAY,cAAc,CAAC,CAAA;AACtF,EAAA,OAAO,gBAAgB,GAAG,CAAA;AAC5B;AAUO,SAAS,6BAAA,CACd,IAAA,EACA,KAAA,EACA,YAAA,GAA8B,qBAAA,EACrB;AACT,EAAA,OAAO,sBAAA;AAAA,IACL,CAAC,aAAa,KAAK,CAAA,EAAG,aAAa,YAAY,CAAA,EAAG,YAAA,CAAa,IAAI,CAAC,CAAA;AAAA,IACpE;AAAA,GACF;AACF;AAEO,SAAS,oBAAoB,KAAA,EAAkC;AACpE,EAAA,OAAO,IAAA,CAAK,oBAAA,CAAqB,mBAAA,CAAoB,EAAE,OAAO,CAAA;AAChE;AAEO,SAAS,qBAAqB,SAAA,EAAsC;AACzE,EAAA,OAAO,IAAA,CAAK,OAAA,CAAQ,aAAA,CAAc,SAAS,CAAA;AAC7C;ACvDA,IAAM,SAAA,GAAY,CAAC,KAAA,KACjB,OAAO,KAAA,KAAU,QAAA,IACjB,KAAA,KAAU,IAAA,IACV,WAAA,IAAe,KAAA,IACf,KAAA,CAAM,SAAA,YAAqB,UAAA;AAGtB,SAAS,kBAAkB,OAAA,EAAgC;AAChE,EAAA,MAAM,IAAA,GAAO,OAAA,CAAQ,SAAA,CAAU,KAAA,CAAM,GAAG,EAAE,CAAA;AAC1C,EAAA,OAAO,OAAO,OAAA,KAA6C,OAAA,CAAQ,IAAA,CAAK,SAAS,IAAI,CAAA;AACvF;AAGO,SAAS,wBAAwB,MAAA,EAAoC;AAC1E,EAAA,MAAM,MAAA,GAAS,MAAA;AACf,EAAA,IAAI,MAAA,CAAO,eAAA,EAAiB,OAAO,MAAA,CAAO,eAAA;AAC1C,EAAA,IAAI,OAAA,IAAW,UAAU,SAAA,CAAU,MAAA,CAAO,KAAK,CAAA,EAAG,OAAO,iBAAA,CAAkB,MAAA,CAAO,KAAK,CAAA;AACvF,EAAA,OAAO,MAAA;AACT","file":"chunk-DT6IKCWB.js","sourcesContent":["import { web3 } from \"@anchor-lang/core\";\nimport { address, getAddressDecoder, getAddressEncoder, type Address } from \"@solana/kit\";\n\nexport type SolanaAddress = Address | string | InstanceType<typeof web3.PublicKey>;\nexport type SolanaConnection = InstanceType<typeof web3.Connection>;\nexport type SolanaKeypair = InstanceType<typeof web3.Keypair>;\nexport type SolanaInstruction = InstanceType<typeof web3.TransactionInstruction>;\nexport type SolanaAccountMeta = {\n pubkey: InstanceType<typeof web3.PublicKey>;\n isSigner: boolean;\n isWritable: boolean;\n};\n\nexport const TOKEN_PROGRAM_ADDRESS = address(\"TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA\");\nexport const ASSOCIATED_TOKEN_PROGRAM_ADDRESS = address(\n \"ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL\",\n);\nexport const SYSTEM_PROGRAM_ADDRESS = address(\"11111111111111111111111111111111\");\n\nconst addressEncoder = getAddressEncoder();\nconst addressDecoder = getAddressDecoder();\n\nexport function toSolanaAddress(value: SolanaAddress): Address {\n return address(typeof value === \"string\" ? value : value.toBase58());\n}\n\nexport function toPublicKey(value: SolanaAddress): InstanceType<typeof web3.PublicKey> {\n return value instanceof web3.PublicKey ? value : new web3.PublicKey(value);\n}\n\nexport function addressBytes(value: SolanaAddress): Uint8Array {\n return Uint8Array.from(addressEncoder.encode(toSolanaAddress(value)));\n}\n\n/** Base58 `Address` from 32 raw bytes (e.g. a `Registry.nodes[i]` entry). */\nexport function addressFromBytes(bytes: Uint8Array): Address {\n return addressDecoder.decode(bytes);\n}\n\nexport function findProgramAddressSync(\n seeds: Uint8Array[],\n programAddress: SolanaAddress,\n): Address {\n const [pda] = web3.PublicKey.findProgramAddressSync(seeds, toPublicKey(programAddress));\n return toSolanaAddress(pda);\n}\n\nexport function findProgramPublicKeySync(\n seeds: Uint8Array[],\n programAddress: SolanaAddress,\n): InstanceType<typeof web3.PublicKey> {\n const [pda] = web3.PublicKey.findProgramAddressSync(seeds, toPublicKey(programAddress));\n return pda;\n}\n\nexport function getAssociatedTokenAddressSync(\n mint: SolanaAddress,\n owner: SolanaAddress,\n tokenProgram: SolanaAddress = TOKEN_PROGRAM_ADDRESS,\n): Address {\n return findProgramAddressSync(\n [addressBytes(owner), addressBytes(tokenProgram), addressBytes(mint)],\n ASSOCIATED_TOKEN_PROGRAM_ADDRESS,\n );\n}\n\nexport function setComputeUnitLimit(units: number): SolanaInstruction {\n return web3.ComputeBudgetProgram.setComputeUnitLimit({ units });\n}\n\nexport function keypairFromSecretKey(secretKey: Uint8Array): SolanaKeypair {\n return web3.Keypair.fromSecretKey(secretKey);\n}\n\nexport function generateKeypair(): SolanaKeypair {\n return web3.Keypair.generate();\n}\n","/**\n * Unified wallet surface: Anchor txs + optional gateway authSig signing.\n */\nimport type { Wallet } from \"@anchor-lang/core\";\nimport { ed25519 } from \"@noble/curves/ed25519.js\";\nimport type { Signer } from \"./core/types.js\";\nimport type { SolanaKeypair } from \"./solana/kit.js\";\n\n/** Anchor `Wallet` plus optional gateway auth override. */\nexport type MolphaWallet = Wallet & {\n /**\n * Signs gateway request-auth hashes (`hashRequestAuth`). When omitted, derived from Anchor\n * `Wallet.payer` when the keypair secret is available (Node `Wallet`, etc.).\n */\n signAuthMessage?: Signer;\n};\n\nconst isKeypair = (value: unknown): value is SolanaKeypair =>\n typeof value === \"object\" &&\n value !== null &&\n \"secretKey\" in value &&\n value.secretKey instanceof Uint8Array;\n\n/** ed25519 gateway signer from a Solana keypair's 32-byte seed. */\nexport function signerFromKeypair(keypair: SolanaKeypair): Signer {\n const seed = keypair.secretKey.slice(0, 32);\n return async (message: Uint8Array): Promise<Uint8Array> => ed25519.sign(message, seed);\n}\n\n/** Resolve the gateway auth signer for a Molpha wallet, if available. */\nexport function gatewaySignerFromWallet(wallet: Wallet): Signer | undefined {\n const molpha = wallet as MolphaWallet;\n if (molpha.signAuthMessage) return molpha.signAuthMessage;\n if (\"payer\" in wallet && isKeypair(wallet.payer)) return signerFromKeypair(wallet.payer);\n return undefined;\n}\n"]}
|