northstar-eva-sdk 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -9,10 +9,36 @@ 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
+
16
42
  ## What's new in 0.1.1
17
43
 
18
44
  The deposit ("fund fee to ER payer") API now **contains the open-session step**, plus two helper additions:
@@ -136,7 +136,12 @@ export interface ResolvedConfig {
136
136
  settlementIntervalSlots: bigint;
137
137
  }
138
138
  export interface NorthstarEvaInput {
139
- network?: NetworkName;
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
- /** Signer / fee payer. (v1: a Keypair.) */
222
- wallet: Keypair;
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 wallet. */
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
- readonly wallet: Keypair;
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: Keypair[]): Promise<ErTxResult>;
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;
@@ -331,21 +369,22 @@ export declare class NorthstarEva {
331
369
  /**
332
370
  * "Withdraw fee from ER" (path 2): an ER `system_transfer` from `recipient` → its
333
371
  * WithdrawalSink. The validator pays the L1 SOL ASYNCHRONOUSLY at the next settlement
334
- * (use {@link waitForL1Settlement} to await it). `recipient` is a Keypair (it signs the
335
- * ER transfer; the L1 receiver never signs).
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.
336
375
  *
337
376
  * NOTE: the L1 payout currently goes to the SAME account that deposited — an arbitrary
338
377
  * `toL1` destination is not supported by the Portal program yet (pending protocol change).
339
378
  */
340
379
  requestWithdraw(p: {
341
380
  lamports: bigint;
342
- recipient: Keypair;
381
+ recipient?: TxSigner;
343
382
  toL1?: PublicKey;
344
383
  }): Promise<ErTxResult>;
345
384
  /** "Withdraw fee from ER" (the green-box withdraw API) — alias of {@link requestWithdraw}. */
346
385
  withdrawFeeFromEr(p: {
347
386
  lamports: bigint;
348
- recipient: Keypair;
387
+ recipient?: TxSigner;
349
388
  toL1?: PublicKey;
350
389
  }): Promise<ErTxResult>;
351
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, sendAndConfirmTransaction } from "@solana/web3.js";
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
- const preset = NETWORKS[input.network ?? "localnet"];
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}". For devnet, pass l1Rpc + erRpc ` +
1986
- `pointing at your NorthStar node (public Solana devnet has no ER/Portal).`);
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
- this.cfg = resolveConfig(opts);
2153
- this.wallet = opts.wallet;
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
- : opts.wallet.publicKey;
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
- const provider = new A.AnchorProvider(this.er, new A.Wallet(this.wallet), { commitment: "processed" });
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 tx = new Transaction().add(...instructions);
2226
- return sendAndConfirmTransaction(this.l1, tx, signers, { commitment: "confirmed", skipPreflight: false });
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
- tx.sign(...signers);
2237
- signature = await this.er.sendRawTransaction(tx.serialize(), { skipPreflight: false, preflightCommitment: "processed" });
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) {
@@ -2410,19 +2460,21 @@ export class NorthstarEva {
2410
2460
  /**
2411
2461
  * "Withdraw fee from ER" (path 2): an ER `system_transfer` from `recipient` → its
2412
2462
  * WithdrawalSink. The validator pays the L1 SOL ASYNCHRONOUSLY at the next settlement
2413
- * (use {@link waitForL1Settlement} to await it). `recipient` is a Keypair (it signs the
2414
- * ER transfer; the L1 receiver never signs).
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.
2415
2466
  *
2416
2467
  * NOTE: the L1 payout currently goes to the SAME account that deposited — an arbitrary
2417
2468
  * `toL1` destination is not supported by the Portal program yet (pending protocol change).
2418
2469
  */
2419
2470
  async requestWithdraw(p) {
2420
- if (p.toL1 && !p.toL1.equals(p.recipient.publicKey)) {
2471
+ const recipient = p.recipient ?? this.wallet;
2472
+ if (p.toL1 && !p.toL1.equals(recipient.publicKey)) {
2421
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.");
2422
2474
  }
2423
- const sink = this.getWithdrawalSink(p.recipient.publicKey);
2424
- const ix = SystemProgram.transfer({ fromPubkey: p.recipient.publicKey, toPubkey: sink, lamports: Number(p.lamports) });
2425
- return this.sendOnEr([ix], [p.recipient]);
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]);
2426
2478
  }
2427
2479
  /** "Withdraw fee from ER" (the green-box withdraw API) — alias of {@link requestWithdraw}. */
2428
2480
  async withdrawFeeFromEr(p) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "northstar-eva-sdk",
3
- "version": "0.1.1",
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",