northstar-eva-sdk 0.1.0 → 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 +54 -6
- package/dist/northstar-eva-sdk.d.ts +88 -13
- package/dist/northstar-eva-sdk.js +107 -24
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -9,10 +9,55 @@ npm install northstar-eva-sdk @solana/web3.js @coral-xyz/anchor @solana/spl-toke
|
|
|
9
9
|
```
|
|
10
10
|
```ts
|
|
11
11
|
import { NorthstarEva } from "northstar-eva-sdk";
|
|
12
|
+
// localnet with a Keypair:
|
|
12
13
|
const sdk = NorthstarEva.create({ network: "localnet", wallet });
|
|
14
|
+
// OR keep your existing AnchorProvider + read-only wallet + custom RPC (see 0.2.0):
|
|
15
|
+
const sdk = NorthstarEva.create({ provider, erRpc: ER_RPC_URL, evaProgramId });
|
|
13
16
|
```
|
|
14
17
|
> Ships compiled JS + TypeScript types — works in any Node ≥18 project (JS or TS). The three Solana libs are **peer dependencies** (install them alongside). Prefer a single drop-in file instead? See `share/` (no npm). Publishing it yourself? See [PUBLISHING.md](PUBLISHING.md).
|
|
15
18
|
|
|
19
|
+
## What's new in 0.2.0
|
|
20
|
+
|
|
21
|
+
Integration-friendly construction — **no Keypair required, keep your provider, any RPC URL.**
|
|
22
|
+
|
|
23
|
+
- **Read-only / remote wallets are supported.** `wallet` now accepts any **wallet adapter** (`publicKey` + `signTransaction`) — e.g. a Turnkey `ReadOnlyWallet` — not just a `Keypair`. The SDK signs through `signTransaction` and never needs the secret key. (Fixes the `Type 'Wallet' is missing … 'secretKey'` error.)
|
|
24
|
+
- **Pass your `AnchorProvider` directly.** `NorthstarEva.create({ provider })` reuses the provider's `.wallet` (signer) and `.connection` (the **L1** endpoint) — keep the exact provider/program setup you already have.
|
|
25
|
+
- **Any RPC URL (not just localhost).** Pass `l1Rpc` + `erRpc` (or `provider`/`connection` + `erRpc`). `network` is now just an optional label (any string) and no longer rejects a URL. **The ER is a separate endpoint — always pass `erRpc`** (the SDK throws a clear error if a custom L1 is given without it).
|
|
26
|
+
- **Program ids are configurable + exposed.** `evaProgramId` / `portalProgramId` are options *and* readable as `sdk.evaProgramId` / `sdk.portalProgramId`.
|
|
27
|
+
- **Dynamic recipients.** Deposit credits any `recipient` (a `PublicKey`); withdraw's `recipient` accepts a `Keypair` **or** a wallet adapter (and defaults to the SDK signer).
|
|
28
|
+
|
|
29
|
+
**Migration — your old `new EvaArenaSDK(program, provider)` shape, unchanged inputs:**
|
|
30
|
+
```ts
|
|
31
|
+
const userPubkey = new PublicKey(turnkeyAddress);
|
|
32
|
+
const wallet = new ReadOnlyWallet(userPubkey); // no Keypair
|
|
33
|
+
const provider = new anchor.AnchorProvider(connection, wallet, { commitment: "confirmed" });
|
|
34
|
+
const sdk = NorthstarEva.create({
|
|
35
|
+
provider, // keeps provider + read-only wallet
|
|
36
|
+
erRpc: ER_RPC_URL, // the NorthStar ER endpoint (any URL)
|
|
37
|
+
evaProgramId: getProgramId(),// per-environment program id
|
|
38
|
+
});
|
|
39
|
+
```
|
|
40
|
+
> Backwards compatible: `NorthstarEva.create({ network, wallet: keypair })` still works (the Keypair is wrapped automatically). No method signatures changed.
|
|
41
|
+
|
|
42
|
+
## What's new in 0.1.1
|
|
43
|
+
|
|
44
|
+
The deposit ("fund fee to ER payer") API now **contains the open-session step**, plus two helper additions:
|
|
45
|
+
|
|
46
|
+
- **`fundFeeToErPayer` / `fundErFeePayer` now open the session for you.** Portal `DepositFee` requires an open session (the DepositReceipt PDA is derived from it). Before, you had to call `openSession()` yourself first; now the deposit call ensures it. Opt out with `{ ensureSession: false }` if you already opened it.
|
|
47
|
+
```ts
|
|
48
|
+
// 0.1.0 — two calls, deposit fails if you forgot openSession
|
|
49
|
+
await sdk.openSession();
|
|
50
|
+
await sdk.fundFeeToErPayer(200_000_000n);
|
|
51
|
+
// 0.1.1 — one call; the session is opened automatically (idempotent)
|
|
52
|
+
await sdk.fundFeeToErPayer(200_000_000n);
|
|
53
|
+
```
|
|
54
|
+
- **New `ensureSession()`** — the named, idempotent "open the session if it isn't already" method (opens only when the session PDA is absent). This is the step `fundFeeToErPayer` performs internally; call it directly if you want the session open up front.
|
|
55
|
+
- **New `withdrawFeeFromEr(...)`** — alias of `requestWithdraw(...)` that matches the "withdraw fee from ER" API name.
|
|
56
|
+
|
|
57
|
+
> **Backwards compatible:** existing code that calls `openSession()` before depositing still works (the extra `ensureSession` is a no-op when the session already exists). No signatures or return types changed.
|
|
58
|
+
|
|
59
|
+
The three user-fund APIs: **query balance** → `querySolBalance(account?)` · **deposit** → `fundFeeToErPayer(lamports, recipient?)` · **withdraw** → `withdrawFeeFromEr({ lamports, recipient, toL1? })`.
|
|
60
|
+
|
|
16
61
|
## What it is + the fee point
|
|
17
62
|
|
|
18
63
|
NorthStar is **one validator process with two lanes**:
|
|
@@ -24,7 +69,7 @@ The fee-reduction play: do the cheap, one-time **setup/control/custody** on L1 (
|
|
|
24
69
|
|
|
25
70
|
| Operation | Lane |
|
|
26
71
|
| --- | --- |
|
|
27
|
-
| `openSession`, `fundErFeePayer`/`ensureErFunds`, `delegateKeypairAccount`, `createPortalOwnedAccount`, `closeSession` | **L1** |
|
|
72
|
+
| `openSession`/`ensureSession`, `fundFeeToErPayer`/`fundErFeePayer`/`ensureErFunds`, `delegateKeypairAccount`, `createPortalOwnedAccount`, `closeSession` | **L1** |
|
|
28
73
|
| eva `initialize` / `createTrench` / `deposit` / `finalize` / `ensureUserAta` / `buy` / `sell` | **ER (zero fee)** |
|
|
29
74
|
| Reads (`erGetAccount`, `getGlobal`, `getTrench`, …) | **Either** — ER reads pinned to commitment `processed` so read-after-write is fresh |
|
|
30
75
|
|
|
@@ -69,10 +114,11 @@ const sdk = NorthstarEva.create({ network: "localnet", wallet });
|
|
|
69
114
|
const h = await sdk.health();
|
|
70
115
|
if (!h.l1 || !h.er) throw new Error(`validator not healthy: ${JSON.stringify(h)}`);
|
|
71
116
|
|
|
72
|
-
// 2. Open the Portal session on L1
|
|
73
|
-
await sdk.
|
|
117
|
+
// 2. (Optional since 0.1.1) Open the Portal session on L1 — the deposit below ensures it for you.
|
|
118
|
+
await sdk.ensureSession(); // idempotent; no-op if already open
|
|
74
119
|
|
|
75
|
-
// 3. Fund the ER fee payer via Portal DepositFee on L1 (ER balance = these credits)
|
|
120
|
+
// 3. Fund the ER fee payer via Portal DepositFee on L1 (ER balance = these credits).
|
|
121
|
+
// ensureErFunds/fundFeeToErPayer open the session automatically if step 2 was skipped.
|
|
76
122
|
await sdk.ensureErFunds(250_000_000n); // tops up + waits for the credit to land
|
|
77
123
|
|
|
78
124
|
// 4. Initialize the eva global state on the ER (idempotent)
|
|
@@ -198,10 +244,12 @@ The "works devnet later" claim is true only against a **self-hosted NorthStar de
|
|
|
198
244
|
| Method | Lane | Description | Returns |
|
|
199
245
|
| --- | --- | --- | --- |
|
|
200
246
|
| `openSession()` | L1 | Open the global Portal session (idempotent — checks if the session PDA exists first). | `Promise<{ signature: string \| null; sessionPda: PublicKey; alreadyOpen: boolean }>` |
|
|
247
|
+
| `ensureSession()` | L1 | **The "open session" step** that DepositFee depends on. Idempotent: opens the session only if its PDA is absent. Called for you by `fundFeeToErPayer`/`fundErFeePayer`. | `Promise<{ sessionPda: PublicKey; opened: boolean; signature: string \| null }>` |
|
|
201
248
|
| `closeSession()` | L1 | Close the session. | `Promise<string>` (signature) |
|
|
202
249
|
| `depositReceiptPda(recipient?)` | — (pure) | Derive the DepositReceipt PDA for a recipient (defaults to wallet). | `PublicKey` |
|
|
203
|
-
| `
|
|
204
|
-
| `
|
|
250
|
+
| `fundFeeToErPayer(lamports, recipient?, { ensureSession? })` | L1 | **"Fund fee to ER payer"** deposit. **Contains the open-session step** — calls `ensureSession()` first (DepositFee requires a session), then Portal `DepositFee`. Pass `{ ensureSession: false }` to skip. | `Promise<string>` (DepositFee sig) |
|
|
251
|
+
| `fundErFeePayer(lamports, recipient?, { ensureSession? })` | L1 | Same as above (the underlying impl). Portal `DepositFee` — credits an ER fee payer (its **ER balance = these credits**, not the L1 balance); ensures the session first. | `Promise<string>` |
|
|
252
|
+
| `ensureErFunds(lamports, recipient?)` | L1 + ER | Ensure the ER fee payer has `>= lamports`; tops up via `DepositFee` (which ensures the session) and polls up to 25×800ms (~20s) for the credit. | `Promise<bigint>` (resulting balance) |
|
|
205
253
|
| `delegateKeypairAccount(target: Keypair)` | L1 | Delegate a plain System-owned keypair account into the ER (the "proper" delegation path). | `Promise<string>` |
|
|
206
254
|
| `createPortalOwnedAccount(space?)` | L1 | Create a fresh Portal-owned account (delegation-target helper). | `Promise<Keypair>` |
|
|
207
255
|
|
|
@@ -136,7 +136,12 @@ export interface ResolvedConfig {
|
|
|
136
136
|
settlementIntervalSlots: bigint;
|
|
137
137
|
}
|
|
138
138
|
export interface NorthstarEvaInput {
|
|
139
|
-
|
|
139
|
+
/**
|
|
140
|
+
* A preset name (`"localnet"` | `"devnet"`) OR any custom label — presets only set
|
|
141
|
+
* default endpoints. To use ANY RPC (not localhost), pass `l1Rpc` + `erRpc` (or a
|
|
142
|
+
* `provider`/`connection`); `network` is then just a label and may be any string.
|
|
143
|
+
*/
|
|
144
|
+
network?: NetworkName | (string & {});
|
|
140
145
|
l1Rpc?: string;
|
|
141
146
|
erRpc?: string;
|
|
142
147
|
erWs?: string;
|
|
@@ -217,13 +222,37 @@ export interface ErTxResult {
|
|
|
217
222
|
err: unknown | null;
|
|
218
223
|
confirmed: boolean;
|
|
219
224
|
}
|
|
225
|
+
/**
|
|
226
|
+
* A wallet adapter the SDK can sign through — exactly what Anchor's `Wallet`,
|
|
227
|
+
* a wallet-adapter, or a Turnkey/remote `ReadOnlyWallet` already implement.
|
|
228
|
+
* The SDK never needs the secret key, so a `Keypair` is NOT required.
|
|
229
|
+
*/
|
|
230
|
+
export interface WalletLike {
|
|
231
|
+
publicKey: PublicKey;
|
|
232
|
+
signTransaction<T extends Transaction>(tx: T): Promise<T>;
|
|
233
|
+
signAllTransactions?<T extends Transaction>(txs: T[]): Promise<T[]>;
|
|
234
|
+
}
|
|
235
|
+
/** Anything the SDK can sign a tx with: a raw Keypair (it partial-signs) or a wallet adapter. */
|
|
236
|
+
export type TxSigner = Keypair | WalletLike;
|
|
220
237
|
export interface NorthstarEvaOptions extends NorthstarEvaInput {
|
|
221
|
-
/**
|
|
222
|
-
|
|
238
|
+
/**
|
|
239
|
+
* Signer / fee payer. A `Keypair` (wrapped automatically) **or** any wallet adapter
|
|
240
|
+
* (`publicKey` + `signTransaction`) — e.g. a Turnkey `ReadOnlyWallet`. Optional when
|
|
241
|
+
* `provider` is given (its `.wallet` is used).
|
|
242
|
+
*/
|
|
243
|
+
wallet?: TxSigner;
|
|
244
|
+
/**
|
|
245
|
+
* An Anchor `AnchorProvider` (as you already build for eva). Its `.wallet` becomes the
|
|
246
|
+
* signer and its `.connection` becomes the **L1** connection — so you keep your provider.
|
|
247
|
+
* NOTE: the ER is a SEPARATE endpoint, so still pass `erRpc`.
|
|
248
|
+
*/
|
|
249
|
+
provider?: anchor.AnchorProvider;
|
|
250
|
+
/** Explicit L1 connection (alternative to `provider`/`l1Rpc`). */
|
|
251
|
+
connection?: Connection;
|
|
223
252
|
/** eva IDL object. If omitted, loaded from `evaIdlPath` or the bundled IDL. */
|
|
224
253
|
evaIdl?: Idl;
|
|
225
254
|
evaIdlPath?: string;
|
|
226
|
-
/** Validator identity recorded in the session (settlement signer). Defaults to
|
|
255
|
+
/** Validator identity recorded in the session (settlement signer). Defaults to the signer. */
|
|
227
256
|
validatorIdentity?: string | PublicKey;
|
|
228
257
|
/** ms between status/read polls. */
|
|
229
258
|
pollIntervalMs?: number;
|
|
@@ -232,10 +261,17 @@ export declare class NorthstarEva {
|
|
|
232
261
|
readonly cfg: ResolvedConfig;
|
|
233
262
|
readonly l1: Connection;
|
|
234
263
|
readonly er: Connection;
|
|
235
|
-
|
|
264
|
+
/** The signer as a wallet adapter (a passed Keypair is wrapped). Use `.payer` for its pubkey. */
|
|
265
|
+
readonly wallet: WalletLike;
|
|
266
|
+
/** The fee-payer / signer public key. */
|
|
267
|
+
readonly payer: PublicKey;
|
|
236
268
|
readonly validatorIdentity: PublicKey;
|
|
237
269
|
readonly program: anchor.Program<Idl>;
|
|
238
270
|
private readonly pollMs;
|
|
271
|
+
/** The eva program id in use (configurable per environment). */
|
|
272
|
+
get evaProgramId(): PublicKey;
|
|
273
|
+
/** The Portal program id in use. */
|
|
274
|
+
get portalProgramId(): PublicKey;
|
|
239
275
|
constructor(opts: NorthstarEvaOptions);
|
|
240
276
|
static create(opts: NorthstarEvaOptions): NorthstarEva;
|
|
241
277
|
erRpc<T = any>(method: string, params?: unknown[]): Promise<T>;
|
|
@@ -255,9 +291,11 @@ export declare class NorthstarEva {
|
|
|
255
291
|
getSessionPdaFromEr(): Promise<any>;
|
|
256
292
|
getDelegatedAccounts(): Promise<string[]>;
|
|
257
293
|
sessionPda(): anchor.web3.PublicKey;
|
|
294
|
+
/** Sign `tx` with a mixed set of signers: raw Keypairs are partial-signed, wallet adapters sign via signTransaction. */
|
|
295
|
+
private signTx;
|
|
258
296
|
private sendOnL1;
|
|
259
297
|
/** Send a tx to the ER and confirm by polling (the ER returns Ok on acceptance). */
|
|
260
|
-
sendOnEr(instructions: Transaction["instructions"], signers:
|
|
298
|
+
sendOnEr(instructions: Transaction["instructions"], signers: TxSigner[]): Promise<ErTxResult>;
|
|
261
299
|
private sendEva;
|
|
262
300
|
/** Fetch eva GlobalState from the ER or throw a clear "not initialized" error. */
|
|
263
301
|
private requireGlobal;
|
|
@@ -269,9 +307,30 @@ export declare class NorthstarEva {
|
|
|
269
307
|
alreadyOpen: boolean;
|
|
270
308
|
}>;
|
|
271
309
|
closeSession(): Promise<string>;
|
|
310
|
+
/**
|
|
311
|
+
* Ensure the Portal session is open (idempotent). Opens it via {@link openSession} only if
|
|
312
|
+
* the session PDA doesn't exist yet; otherwise does nothing (no tx, no fee).
|
|
313
|
+
*
|
|
314
|
+
* THIS is the "open session" step that DepositFee depends on: the DepositReceipt PDA is
|
|
315
|
+
* derived from the session, so `fundFeeToErPayer` / `fundErFeePayer` call this for you
|
|
316
|
+
* before depositing. Call it directly if you want the session open up front.
|
|
317
|
+
*/
|
|
318
|
+
ensureSession(): Promise<{
|
|
319
|
+
sessionPda: PublicKey;
|
|
320
|
+
opened: boolean;
|
|
321
|
+
signature: string | null;
|
|
322
|
+
}>;
|
|
272
323
|
depositReceiptPda(recipient?: PublicKey): anchor.web3.PublicKey;
|
|
273
|
-
/**
|
|
274
|
-
|
|
324
|
+
/**
|
|
325
|
+
* DepositFee on L1 — funds an ER fee payer (its ER balance = these credits).
|
|
326
|
+
* DepositFee REQUIRES an open Portal session (the DepositReceipt PDA is derived from it), so
|
|
327
|
+
* this ENSURES the session is open first via {@link ensureSession} (idempotent). Pass
|
|
328
|
+
* `{ ensureSession: false }` to skip that if you already opened the session yourself.
|
|
329
|
+
* Returns the DepositFee transaction signature.
|
|
330
|
+
*/
|
|
331
|
+
fundErFeePayer(lamports: bigint, recipient?: PublicKey, opts?: {
|
|
332
|
+
ensureSession?: boolean;
|
|
333
|
+
}): Promise<string>;
|
|
275
334
|
/** Ensure the ER fee payer has >= `lamports`; tops up via DepositFee + waits for the credit. */
|
|
276
335
|
ensureErFunds(lamports: bigint, recipient?: PublicKey): Promise<bigint>;
|
|
277
336
|
/** Delegate a plain keypair account (System-owned) into the ER (the "proper" path). */
|
|
@@ -294,22 +353,38 @@ export declare class NorthstarEva {
|
|
|
294
353
|
er: bigint;
|
|
295
354
|
l1: bigint;
|
|
296
355
|
}>;
|
|
297
|
-
/**
|
|
298
|
-
|
|
356
|
+
/**
|
|
357
|
+
* "Fund fee to ER payer" (the green-box deposit API) — deposit SOL from L1 so `recipient`
|
|
358
|
+
* is credited & spendable on the ER.
|
|
359
|
+
*
|
|
360
|
+
* This NOW CONTAINS the open-session step: it calls {@link ensureSession} first (idempotent)
|
|
361
|
+
* because Portal DepositFee requires an open session. Pass `{ ensureSession: false }` to skip
|
|
362
|
+
* it if you already opened the session. Returns the DepositFee transaction signature.
|
|
363
|
+
*/
|
|
364
|
+
fundFeeToErPayer(lamports: bigint, recipient?: PublicKey, opts?: {
|
|
365
|
+
ensureSession?: boolean;
|
|
366
|
+
}): Promise<string>;
|
|
299
367
|
/** The ER WithdrawalSink PDA for a recipient (where ER withdrawal requests are sent). */
|
|
300
368
|
getWithdrawalSink(recipient?: PublicKey): PublicKey;
|
|
301
369
|
/**
|
|
302
370
|
* "Withdraw fee from ER" (path 2): an ER `system_transfer` from `recipient` → its
|
|
303
371
|
* WithdrawalSink. The validator pays the L1 SOL ASYNCHRONOUSLY at the next settlement
|
|
304
|
-
* (use {@link waitForL1Settlement} to await it). `recipient`
|
|
305
|
-
*
|
|
372
|
+
* (use {@link waitForL1Settlement} to await it). `recipient` signs the ER transfer and may
|
|
373
|
+
* be a **Keypair OR a wallet adapter** (e.g. a Turnkey `ReadOnlyWallet`); the L1 receiver never signs.
|
|
374
|
+
* Defaults to the SDK's own signer if omitted.
|
|
306
375
|
*
|
|
307
376
|
* NOTE: the L1 payout currently goes to the SAME account that deposited — an arbitrary
|
|
308
377
|
* `toL1` destination is not supported by the Portal program yet (pending protocol change).
|
|
309
378
|
*/
|
|
310
379
|
requestWithdraw(p: {
|
|
311
380
|
lamports: bigint;
|
|
312
|
-
recipient
|
|
381
|
+
recipient?: TxSigner;
|
|
382
|
+
toL1?: PublicKey;
|
|
383
|
+
}): Promise<ErTxResult>;
|
|
384
|
+
/** "Withdraw fee from ER" (the green-box withdraw API) — alias of {@link requestWithdraw}. */
|
|
385
|
+
withdrawFeeFromEr(p: {
|
|
386
|
+
lamports: bigint;
|
|
387
|
+
recipient?: TxSigner;
|
|
313
388
|
toL1?: PublicKey;
|
|
314
389
|
}): Promise<ErTxResult>;
|
|
315
390
|
/** Poll an account's L1 balance until it rises by >= `expectedDelta` (settlement is async). Returns the final L1 balance. */
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
// ============================================================================
|
|
8
8
|
import * as anchor from "@coral-xyz/anchor";
|
|
9
9
|
import { readFileSync } from "node:fs";
|
|
10
|
-
import { Connection, Keypair, PublicKey, SystemProgram, Transaction, TransactionInstruction
|
|
10
|
+
import { Connection, Keypair, PublicKey, SystemProgram, Transaction, TransactionInstruction } from "@solana/web3.js";
|
|
11
11
|
import { TOKEN_PROGRAM_ID, ASSOCIATED_TOKEN_PROGRAM_ID, getAssociatedTokenAddressSync, createAssociatedTokenAccountIdempotentInstruction } from "@solana/spl-token";
|
|
12
12
|
// --- embedded eva IDL (CQru6…) ---
|
|
13
13
|
const __EVA_IDL__ = {
|
|
@@ -1978,12 +1978,15 @@ export const NETWORKS = {
|
|
|
1978
1978
|
const pk = (v) => (typeof v === "string" ? new PublicKey(v) : v);
|
|
1979
1979
|
const bn = (v, d) => (v === undefined ? d : BigInt(v));
|
|
1980
1980
|
export function resolveConfig(input) {
|
|
1981
|
-
|
|
1981
|
+
// Only "localnet"/"devnet" are presets; any other `network` value is just a label and
|
|
1982
|
+
// falls back to the localnet preset for defaults — pass l1Rpc/erRpc to override them.
|
|
1983
|
+
const presetKey = input.network === "devnet" ? "devnet" : "localnet";
|
|
1984
|
+
const preset = NETWORKS[presetKey];
|
|
1982
1985
|
const l1Rpc = input.l1Rpc ?? preset.l1Rpc;
|
|
1983
1986
|
const erRpc = input.erRpc ?? preset.erRpc;
|
|
1984
1987
|
if (!l1Rpc || !erRpc) {
|
|
1985
|
-
throw new Error(`Missing RPC endpoints for network "${input.network}".
|
|
1986
|
-
`pointing at your NorthStar node
|
|
1988
|
+
throw new Error(`Missing RPC endpoints for network "${input.network}". Pass l1Rpc + erRpc (or a provider + erRpc) ` +
|
|
1989
|
+
`pointing at your NorthStar node — public Solana devnet has no ER/Portal, and the ER is a SEPARATE URL from L1.`);
|
|
1987
1990
|
}
|
|
1988
1991
|
return {
|
|
1989
1992
|
l1Rpc,
|
|
@@ -2136,6 +2139,7 @@ const evaPdas = { globalStatePda, trenchPda, agentPda, tokenMintPda, trenchToken
|
|
|
2136
2139
|
*/
|
|
2137
2140
|
// @coral-xyz/anchor is CommonJS — runtime values live on the default export.
|
|
2138
2141
|
const A = anchor.default ?? anchor;
|
|
2142
|
+
const isWalletLike = (s) => !!s && typeof s.signTransaction === "function";
|
|
2139
2143
|
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
2140
2144
|
function loadBundledIdl() {
|
|
2141
2145
|
return __EVA_IDL__;
|
|
@@ -2144,23 +2148,52 @@ export class NorthstarEva {
|
|
|
2144
2148
|
cfg;
|
|
2145
2149
|
l1;
|
|
2146
2150
|
er;
|
|
2151
|
+
/** The signer as a wallet adapter (a passed Keypair is wrapped). Use `.payer` for its pubkey. */
|
|
2147
2152
|
wallet;
|
|
2153
|
+
/** The fee-payer / signer public key. */
|
|
2154
|
+
payer;
|
|
2148
2155
|
validatorIdentity;
|
|
2149
2156
|
program; // eva, bound to the ER connection
|
|
2150
2157
|
pollMs;
|
|
2158
|
+
/** The eva program id in use (configurable per environment). */
|
|
2159
|
+
get evaProgramId() { return this.cfg.evaProgramId; }
|
|
2160
|
+
/** The Portal program id in use. */
|
|
2161
|
+
get portalProgramId() { return this.cfg.portalProgramId; }
|
|
2151
2162
|
constructor(opts) {
|
|
2152
|
-
|
|
2153
|
-
|
|
2163
|
+
// ── endpoints: a provider/connection can supply the L1 endpoint; ER is always separate ──
|
|
2164
|
+
const providerConn = opts.provider?.connection ?? opts.connection;
|
|
2165
|
+
this.cfg = resolveConfig({ ...opts, l1Rpc: opts.l1Rpc ?? providerConn?.rpcEndpoint });
|
|
2166
|
+
// ── signer: accept a Keypair (wrap it), a wallet adapter, or provider.wallet ──
|
|
2167
|
+
const w = opts.wallet;
|
|
2168
|
+
let signer;
|
|
2169
|
+
if (isWalletLike(w))
|
|
2170
|
+
signer = w; // already an adapter / ReadOnlyWallet
|
|
2171
|
+
else if (w && w.secretKey)
|
|
2172
|
+
signer = new A.Wallet(w); // a Keypair → wrap (NodeWallet)
|
|
2173
|
+
else if (opts.provider?.wallet)
|
|
2174
|
+
signer = opts.provider.wallet;
|
|
2175
|
+
if (!signer)
|
|
2176
|
+
throw new Error("Pass `wallet` (a Keypair or a wallet adapter with signTransaction) or a `provider` (AnchorProvider).");
|
|
2177
|
+
this.wallet = signer;
|
|
2178
|
+
this.payer = signer.publicKey;
|
|
2154
2179
|
this.validatorIdentity = opts.validatorIdentity
|
|
2155
2180
|
? (typeof opts.validatorIdentity === "string" ? new PublicKey(opts.validatorIdentity) : opts.validatorIdentity)
|
|
2156
|
-
:
|
|
2181
|
+
: this.payer;
|
|
2157
2182
|
this.pollMs = opts.pollIntervalMs ?? 500;
|
|
2158
|
-
this.l1 = new Connection(this.cfg.l1Rpc, "confirmed");
|
|
2183
|
+
this.l1 = providerConn ?? new Connection(this.cfg.l1Rpc, "confirmed");
|
|
2159
2184
|
// ER provider reads at "processed" so account resolution / reads are fresh.
|
|
2160
2185
|
this.er = new Connection(this.cfg.erRpc, "processed");
|
|
2186
|
+
// Guard: a custom (non-localhost) L1 with the default localhost ER is almost certainly a
|
|
2187
|
+
// forgotten erRpc — NorthStar's ER is a different URL than L1, fail loudly instead of hitting localhost.
|
|
2188
|
+
const customL1 = !!(opts.l1Rpc || providerConn);
|
|
2189
|
+
const erDefaulted = !opts.erRpc && opts.network !== "devnet";
|
|
2190
|
+
if (customL1 && erDefaulted && !/127\.0\.0\.1|localhost/.test(this.cfg.l1Rpc)) {
|
|
2191
|
+
throw new Error(`Custom L1 endpoint (${this.cfg.l1Rpc}) but no ER endpoint — NorthStar's ER is a SEPARATE URL. Pass erRpc (e.g. the :8910 endpoint).`);
|
|
2192
|
+
}
|
|
2161
2193
|
const idl = opts.evaIdl ?? (opts.evaIdlPath ? JSON.parse(readFileSync(opts.evaIdlPath, "utf8")) : loadBundledIdl());
|
|
2162
2194
|
idl.address = this.cfg.evaProgramId.toBase58(); // honor config / devnet override
|
|
2163
|
-
|
|
2195
|
+
// eva program is bound to the ER connection (it executes there) + signs through the same wallet.
|
|
2196
|
+
const provider = new A.AnchorProvider(this.er, this.wallet, { commitment: "processed" });
|
|
2164
2197
|
this.program = new A.Program(idl, provider);
|
|
2165
2198
|
}
|
|
2166
2199
|
static create(opts) { return new NorthstarEva(opts); }
|
|
@@ -2220,10 +2253,27 @@ export class NorthstarEva {
|
|
|
2220
2253
|
getSessionPdaFromEr() { return this.erRpc("getSessionPda"); }
|
|
2221
2254
|
getDelegatedAccounts() { return this.erRpc("getDelegatedAccounts"); }
|
|
2222
2255
|
sessionPda() { return portal.sessionPda(this.cfg.portalProgramId); }
|
|
2256
|
+
// ───────────────────────── signing (adapter-based, no Keypair required) ─────────────────────────
|
|
2257
|
+
/** Sign `tx` with a mixed set of signers: raw Keypairs are partial-signed, wallet adapters sign via signTransaction. */
|
|
2258
|
+
async signTx(tx, signers) {
|
|
2259
|
+
const keypairs = signers.filter((s) => !isWalletLike(s));
|
|
2260
|
+
const wallets = signers.filter(isWalletLike);
|
|
2261
|
+
if (keypairs.length)
|
|
2262
|
+
tx.partialSign(...keypairs);
|
|
2263
|
+
let signed = tx;
|
|
2264
|
+
for (const wlt of wallets)
|
|
2265
|
+
signed = await wlt.signTransaction(signed); // adds payer sig, preserves partials
|
|
2266
|
+
return signed;
|
|
2267
|
+
}
|
|
2223
2268
|
// ───────────────────────── L1 send + ER send ─────────────────────────
|
|
2224
2269
|
async sendOnL1(instructions, signers) {
|
|
2225
|
-
const
|
|
2226
|
-
|
|
2270
|
+
const { blockhash, lastValidBlockHeight } = await this.l1.getLatestBlockhash("confirmed");
|
|
2271
|
+
const tx = new Transaction({ feePayer: signers[0].publicKey, blockhash, lastValidBlockHeight });
|
|
2272
|
+
tx.add(...instructions);
|
|
2273
|
+
const signed = await this.signTx(tx, signers);
|
|
2274
|
+
const sig = await this.l1.sendRawTransaction(signed.serialize(), { skipPreflight: false, preflightCommitment: "confirmed" });
|
|
2275
|
+
await this.l1.confirmTransaction({ signature: sig, blockhash, lastValidBlockHeight }, "confirmed");
|
|
2276
|
+
return sig;
|
|
2227
2277
|
}
|
|
2228
2278
|
/** Send a tx to the ER and confirm by polling (the ER returns Ok on acceptance). */
|
|
2229
2279
|
async sendOnEr(instructions, signers) {
|
|
@@ -2233,8 +2283,8 @@ export class NorthstarEva {
|
|
|
2233
2283
|
const { blockhash } = await this.er.getLatestBlockhash("processed");
|
|
2234
2284
|
const tx = new Transaction({ feePayer: signers[0].publicKey, blockhash, lastValidBlockHeight: 0 });
|
|
2235
2285
|
tx.add(...instructions);
|
|
2236
|
-
|
|
2237
|
-
signature = await this.er.sendRawTransaction(
|
|
2286
|
+
const signed = await this.signTx(tx, signers);
|
|
2287
|
+
signature = await this.er.sendRawTransaction(signed.serialize(), { skipPreflight: false, preflightCommitment: "processed" });
|
|
2238
2288
|
break;
|
|
2239
2289
|
}
|
|
2240
2290
|
catch (e) {
|
|
@@ -2296,11 +2346,31 @@ export class NorthstarEva {
|
|
|
2296
2346
|
async closeSession() {
|
|
2297
2347
|
return this.sendOnL1([portal.closeSessionIx({ programId: this.cfg.portalProgramId, closer: this.wallet.publicKey })], [this.wallet]);
|
|
2298
2348
|
}
|
|
2349
|
+
/**
|
|
2350
|
+
* Ensure the Portal session is open (idempotent). Opens it via {@link openSession} only if
|
|
2351
|
+
* the session PDA doesn't exist yet; otherwise does nothing (no tx, no fee).
|
|
2352
|
+
*
|
|
2353
|
+
* THIS is the "open session" step that DepositFee depends on: the DepositReceipt PDA is
|
|
2354
|
+
* derived from the session, so `fundFeeToErPayer` / `fundErFeePayer` call this for you
|
|
2355
|
+
* before depositing. Call it directly if you want the session open up front.
|
|
2356
|
+
*/
|
|
2357
|
+
async ensureSession() {
|
|
2358
|
+
const r = await this.openSession();
|
|
2359
|
+
return { sessionPda: r.sessionPda, opened: !r.alreadyOpen, signature: r.signature };
|
|
2360
|
+
}
|
|
2299
2361
|
depositReceiptPda(recipient = this.wallet.publicKey) {
|
|
2300
2362
|
return portal.depositReceiptPda(this.cfg.portalProgramId, this.sessionPda(), recipient);
|
|
2301
2363
|
}
|
|
2302
|
-
/**
|
|
2303
|
-
|
|
2364
|
+
/**
|
|
2365
|
+
* DepositFee on L1 — funds an ER fee payer (its ER balance = these credits).
|
|
2366
|
+
* DepositFee REQUIRES an open Portal session (the DepositReceipt PDA is derived from it), so
|
|
2367
|
+
* this ENSURES the session is open first via {@link ensureSession} (idempotent). Pass
|
|
2368
|
+
* `{ ensureSession: false }` to skip that if you already opened the session yourself.
|
|
2369
|
+
* Returns the DepositFee transaction signature.
|
|
2370
|
+
*/
|
|
2371
|
+
async fundErFeePayer(lamports, recipient = this.wallet.publicKey, opts = {}) {
|
|
2372
|
+
if (opts.ensureSession !== false)
|
|
2373
|
+
await this.ensureSession();
|
|
2304
2374
|
return this.sendOnL1([portal.depositFeeIx({ programId: this.cfg.portalProgramId, depositor: this.wallet.publicKey, recipient, lamports })], [this.wallet]);
|
|
2305
2375
|
}
|
|
2306
2376
|
/** Ensure the ER fee payer has >= `lamports`; tops up via DepositFee + waits for the credit. */
|
|
@@ -2372,9 +2442,16 @@ export class NorthstarEva {
|
|
|
2372
2442
|
const l1 = (await this.l1GetAccount(account))?.lamports ?? 0n;
|
|
2373
2443
|
return { er, l1 };
|
|
2374
2444
|
}
|
|
2375
|
-
/**
|
|
2376
|
-
|
|
2377
|
-
|
|
2445
|
+
/**
|
|
2446
|
+
* "Fund fee to ER payer" (the green-box deposit API) — deposit SOL from L1 so `recipient`
|
|
2447
|
+
* is credited & spendable on the ER.
|
|
2448
|
+
*
|
|
2449
|
+
* This NOW CONTAINS the open-session step: it calls {@link ensureSession} first (idempotent)
|
|
2450
|
+
* because Portal DepositFee requires an open session. Pass `{ ensureSession: false }` to skip
|
|
2451
|
+
* it if you already opened the session. Returns the DepositFee transaction signature.
|
|
2452
|
+
*/
|
|
2453
|
+
async fundFeeToErPayer(lamports, recipient = this.wallet.publicKey, opts = {}) {
|
|
2454
|
+
return this.fundErFeePayer(lamports, recipient, opts);
|
|
2378
2455
|
}
|
|
2379
2456
|
/** The ER WithdrawalSink PDA for a recipient (where ER withdrawal requests are sent). */
|
|
2380
2457
|
getWithdrawalSink(recipient = this.wallet.publicKey) {
|
|
@@ -2383,19 +2460,25 @@ export class NorthstarEva {
|
|
|
2383
2460
|
/**
|
|
2384
2461
|
* "Withdraw fee from ER" (path 2): an ER `system_transfer` from `recipient` → its
|
|
2385
2462
|
* WithdrawalSink. The validator pays the L1 SOL ASYNCHRONOUSLY at the next settlement
|
|
2386
|
-
* (use {@link waitForL1Settlement} to await it). `recipient`
|
|
2387
|
-
*
|
|
2463
|
+
* (use {@link waitForL1Settlement} to await it). `recipient` signs the ER transfer and may
|
|
2464
|
+
* be a **Keypair OR a wallet adapter** (e.g. a Turnkey `ReadOnlyWallet`); the L1 receiver never signs.
|
|
2465
|
+
* Defaults to the SDK's own signer if omitted.
|
|
2388
2466
|
*
|
|
2389
2467
|
* NOTE: the L1 payout currently goes to the SAME account that deposited — an arbitrary
|
|
2390
2468
|
* `toL1` destination is not supported by the Portal program yet (pending protocol change).
|
|
2391
2469
|
*/
|
|
2392
2470
|
async requestWithdraw(p) {
|
|
2393
|
-
|
|
2471
|
+
const recipient = p.recipient ?? this.wallet;
|
|
2472
|
+
if (p.toL1 && !p.toL1.equals(recipient.publicKey)) {
|
|
2394
2473
|
throw new Error("Arbitrary L1 withdrawal destination is not supported yet (Portal pays the deposit recipient). Omit `toL1` or set it equal to recipient.");
|
|
2395
2474
|
}
|
|
2396
|
-
const sink = this.getWithdrawalSink(
|
|
2397
|
-
const ix = SystemProgram.transfer({ fromPubkey:
|
|
2398
|
-
return this.sendOnEr([ix], [
|
|
2475
|
+
const sink = this.getWithdrawalSink(recipient.publicKey);
|
|
2476
|
+
const ix = SystemProgram.transfer({ fromPubkey: recipient.publicKey, toPubkey: sink, lamports: Number(p.lamports) });
|
|
2477
|
+
return this.sendOnEr([ix], [recipient]);
|
|
2478
|
+
}
|
|
2479
|
+
/** "Withdraw fee from ER" (the green-box withdraw API) — alias of {@link requestWithdraw}. */
|
|
2480
|
+
async withdrawFeeFromEr(p) {
|
|
2481
|
+
return this.requestWithdraw(p);
|
|
2399
2482
|
}
|
|
2400
2483
|
/** Poll an account's L1 balance until it rises by >= `expectedDelta` (settlement is async). Returns the final L1 balance. */
|
|
2401
2484
|
async waitForL1Settlement(account, expectedDelta, timeoutMs = 60_000) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "northstar-eva-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Run the eva program on a NorthStar ephemeral rollup (zero-fee hot path). A high-level class SDK over the NorthStar Portal/ER protocol + the eva Anchor program. Works localnet & devnet.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|