@effectstream/wallets 0.100.16 → 0.100.18

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
@@ -57,7 +57,7 @@ const evmOptions = available[WalletMode.EvmInjected]; // [{ metadata, ... }, …
57
57
  ```
58
58
 
59
59
  > **Browser only.** This package depends on `window.ethereum`, the
60
- > Cardano CIP-30 API, etc. — it won't load in plain Node. Server-side
60
+ > Cardano CIP-30 API, etc. - it won't load in plain Node. Server-side
61
61
  > signature verification is `@effectstream/crypto`.
62
62
 
63
63
  ## Inside EffectStream
@@ -76,14 +76,14 @@ Login + signing:
76
76
 
77
77
  - `walletLogin(args)`: top-level helper that discovers, connects, and produces a signed batcher login.
78
78
  - `EffectstreamConfig`: runtime config (chain URLs, batcher URL, ...) the helpers consume.
79
- - `signMessage(wallet, message)` — sign an arbitrary message with the connected wallet.
79
+ - `signMessage(wallet, message)` - sign an arbitrary message with the connected wallet.
80
80
 
81
81
  Sending transactions:
82
82
 
83
83
  - `sendTransaction(wallet, conciseInput, opts?)`: submit through the batcher and (by default) wait for processing.
84
84
  - `sendBatcherTransaction(...)`: explicit batcher submission.
85
85
  - `sendSelfSequencedTransaction(...)` bypasses the batcher.
86
- - `waitForEffectstreamBlockProcessed(...)` — block-height poller used by the above.
86
+ - `waitForEffectstreamBlockProcessed(...)` - block-height poller used by the above.
87
87
 
88
88
  Wallet discovery / identification:
89
89
 
package/package.json CHANGED
@@ -1,22 +1,63 @@
1
1
  {
2
2
  "name": "@effectstream/wallets",
3
- "version": "0.100.16",
3
+ "version": "0.100.18",
4
4
  "license": "MIT",
5
5
  "homepage": "https://effectstream.github.io/docs/",
6
6
  "repository": {
7
7
  "type": "git",
8
- "url": "https://github.com/PaimaStudios/paima-engine",
8
+ "url": "https://github.com/effectstream/effectstream",
9
9
  "directory": "packages/effectstream-sdk/wallets"
10
10
  },
11
11
  "bugs": {
12
- "url": "https://github.com/PaimaStudios/paima-engine/issues"
12
+ "url": "https://github.com/effectstream/effectstream/issues"
13
+ },
14
+ "exports": {
15
+ ".": "./src/mod.ts",
16
+ "./evm-viem": "./src/evm/viem.ts",
17
+ "./cardano-local": "./src/cardano/local.ts",
18
+ "./midnight-local": "./src/midnight/local.ts"
19
+ },
20
+ "peerDependencies": {
21
+ "@lucid-evolution/lucid": "^0.4.29",
22
+ "@lucid-evolution/provider": "^0.1.90",
23
+ "@lucid-evolution/utils": "^0.1.66",
24
+ "@midnight-ntwrk/ledger-v8": "^8.0.3",
25
+ "@midnight-ntwrk/wallet-sdk-hd": "^3.0.1",
26
+ "@midnight-ntwrk/wallet-sdk-unshielded-wallet": "^2.1.0"
27
+ },
28
+ "peerDependenciesMeta": {
29
+ "@lucid-evolution/lucid": {
30
+ "optional": true
31
+ },
32
+ "@lucid-evolution/provider": {
33
+ "optional": true
34
+ },
35
+ "@lucid-evolution/utils": {
36
+ "optional": true
37
+ },
38
+ "@midnight-ntwrk/ledger-v8": {
39
+ "optional": true
40
+ },
41
+ "@midnight-ntwrk/wallet-sdk-hd": {
42
+ "optional": true
43
+ },
44
+ "@midnight-ntwrk/wallet-sdk-unshielded-wallet": {
45
+ "optional": true
46
+ }
47
+ },
48
+ "devDependencies": {
49
+ "@lucid-evolution/lucid": "^0.4.30",
50
+ "@lucid-evolution/provider": "^0.1.91",
51
+ "@lucid-evolution/utils": "^0.1.66",
52
+ "@midnight-ntwrk/ledger-v8": "8.0.3",
53
+ "@midnight-ntwrk/wallet-sdk-hd": "3.0.1",
54
+ "@midnight-ntwrk/wallet-sdk-unshielded-wallet": "2.1.0"
13
55
  },
14
- "exports": "./src/mod.ts",
15
56
  "dependencies": {
16
- "@effectstream/concise": "0.100.16",
17
- "@effectstream/crypto": "0.100.16",
18
- "@effectstream/event-client": "0.100.16",
19
- "@effectstream/utils": "0.100.16",
57
+ "@effectstream/concise": "0.100.18",
58
+ "@effectstream/crypto": "0.100.18",
59
+ "@effectstream/event-client": "0.100.18",
60
+ "@effectstream/utils": "0.100.18",
20
61
  "@aurowallet/mina-provider": "^1.0.12",
21
62
  "@metamask/providers": "^22.1.0",
22
63
  "@midnight-ntwrk/dapp-connector-api": "4.0.1",
@@ -3,6 +3,7 @@ import type { IProvider } from "../IProvider.ts";
3
3
  import type { WalletMode, ApiForMode } from "../utils.ts";
4
4
  import type { LoginInfoMap } from "../wallet-modes.ts";
5
5
  import { type AvailJsApi, AvailConnector } from "./avail.ts";
6
+ import { formatError } from "../helpers/format-error.ts";
6
7
 
7
8
  // Problematic dependency for deno-fresh
8
9
  import { Keyring } from "avail-js-sdk";
@@ -31,7 +32,7 @@ async function connectWallet(
31
32
  console.log(`availJsLoginWrapper: Error while logging into wallet name}`);
32
33
  return {
33
34
  success: false,
34
- errorMessage: err.message ?? String(err),
35
+ errorMessage: formatError(err),
35
36
  }
36
37
  }
37
38
  }
@@ -0,0 +1,31 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { CryptoManager } from "@effectstream/crypto";
3
+ import { AddressType } from "@effectstream/utils";
4
+ import { CardanoLocalConnector } from "./local.ts";
5
+
6
+ describe("CardanoLocalProvider", () => {
7
+ test("connect from seed produces a bech32 address", async () => {
8
+ const provider = await CardanoLocalConnector.instance().connectFromSeed({
9
+ network: "Preview",
10
+ });
11
+ const { type, address } = provider.getAddress();
12
+ expect(type).toBe(AddressType.CARDANO);
13
+ expect(address.startsWith("addr_test1")).toBe(true);
14
+ });
15
+
16
+ test("signMessage produces a signature that @effectstream/crypto verifies", async () => {
17
+ const provider = await CardanoLocalConnector.instance().connectFromSeed({
18
+ network: "Preview",
19
+ });
20
+ const message = "hello effectstream";
21
+ const signature = await provider.signMessage(message);
22
+
23
+ const cardanoCrypto = CryptoManager.getCryptoManager(AddressType.CARDANO);
24
+ const ok = await cardanoCrypto.verifySignature(
25
+ provider.getAddress().address,
26
+ message,
27
+ signature,
28
+ );
29
+ expect(ok).toBe(true);
30
+ });
31
+ });
@@ -0,0 +1,159 @@
1
+ import { AddressType } from "@effectstream/utils";
2
+ import { utf8ToHex } from "web3-utils";
3
+ import type {
4
+ ActiveConnection,
5
+ AddressAndType,
6
+ IProvider,
7
+ UserSignature,
8
+ } from "../IProvider.ts";
9
+ import type { CardanoApi, CardanoAddress } from "./cardano.ts";
10
+
11
+ export type CardanoLocalNetwork = "Mainnet" | "Preprod" | "Preview" | "Custom";
12
+
13
+ export type CardanoLocalConnectArgs = {
14
+ /** BIP-39 mnemonic. Generated via `@lucid-evolution/utils#generateSeedPhrase` when omitted. */
15
+ seedPhrase?: string;
16
+ /** Cardano network — must match the chain address the signature is verified against. */
17
+ network: CardanoLocalNetwork;
18
+ /**
19
+ * Optional Lucid `Provider`. Pass when the caller wants Lucid to fetch UTxOs
20
+ * or submit transactions through this wallet. Pure signing does not require it.
21
+ */
22
+ provider?: unknown;
23
+ };
24
+
25
+ /**
26
+ * `getConnection().api` shape. Mirrors the CIP-30 subset that the rest of
27
+ * Effectstream's Cardano code consumes from a real injected wallet (signData,
28
+ * getUsedAddresses) so existing consumers work unchanged.
29
+ */
30
+ export type CardanoLocalApi = {
31
+ /** CIP-30 signData. Returns COSE_Sign1 (signature) + COSE_Key (key) as hex. */
32
+ signData: (
33
+ address: string,
34
+ payloadHex: string,
35
+ ) => Promise<{ signature: string; key: string }>;
36
+ /** Hex-encoded address list, matching CIP-30 `getUsedAddresses`. */
37
+ getUsedAddresses: () => Promise<string[]>;
38
+ /** Underlying Lucid handle for callers that need tx-building. */
39
+ lucid: unknown;
40
+ /** Generated/passed seed phrase. Exposed so tests can persist/reuse the wallet. */
41
+ seedPhrase: string;
42
+ };
43
+
44
+ export class CardanoLocalConnector {
45
+ private provider: CardanoLocalProvider | undefined;
46
+ private static INSTANCE: undefined | CardanoLocalConnector = undefined;
47
+
48
+ static instance(): CardanoLocalConnector {
49
+ if (CardanoLocalConnector.INSTANCE == null) {
50
+ CardanoLocalConnector.INSTANCE = new CardanoLocalConnector();
51
+ }
52
+ return CardanoLocalConnector.INSTANCE;
53
+ }
54
+
55
+ connectFromSeed = async (
56
+ args: CardanoLocalConnectArgs,
57
+ ): Promise<CardanoLocalProvider> => {
58
+ const lucidMod = await import("@lucid-evolution/lucid").catch(() => {
59
+ throw new Error(
60
+ "@lucid-evolution/lucid is required for WalletMode.CardanoLocal. Install it as a peer dependency.",
61
+ );
62
+ });
63
+ const utilsMod = await import("@lucid-evolution/utils").catch(() => {
64
+ throw new Error(
65
+ "@lucid-evolution/utils is required for WalletMode.CardanoLocal. Install it as a peer dependency.",
66
+ );
67
+ });
68
+
69
+ const seedPhrase = args.seedPhrase ?? utilsMod.generateSeedPhrase();
70
+ // Lucid's wallet().address() requires a Provider to be set even for pure
71
+ // signing. When the caller doesn't supply one, fall back to an in-memory
72
+ // Emulator so signing-only flows (e2e tests, wallet-connect dry runs) work
73
+ // without standing up Blockfrost / YACI.
74
+ let provider = args.provider;
75
+ if (provider == null) {
76
+ const providerMod = await import("@lucid-evolution/provider").catch(() => {
77
+ throw new Error(
78
+ "@lucid-evolution/provider is required when no provider is supplied to CardanoLocal.",
79
+ );
80
+ });
81
+ provider = new providerMod.Emulator([]);
82
+ }
83
+ const lucid = await lucidMod.Lucid(provider as never, args.network);
84
+ lucid.selectWallet.fromSeed(seedPhrase);
85
+
86
+ const bech32Address = await lucid.wallet().address();
87
+ const hexAddress = await addressToHex(lucid, bech32Address);
88
+
89
+ const api: CardanoLocalApi = {
90
+ signData: async (address, payloadHex) => {
91
+ return await lucid.wallet().signMessage(address, payloadHex);
92
+ },
93
+ getUsedAddresses: async () => [hexAddress],
94
+ lucid,
95
+ seedPhrase,
96
+ };
97
+
98
+ const conn: ActiveConnection<CardanoApi> = {
99
+ api,
100
+ metadata: {
101
+ name: "cardano-local",
102
+ displayName: "Cardano (local seed)",
103
+ },
104
+ };
105
+
106
+ this.provider = new CardanoLocalProvider(conn, {
107
+ bech32: bech32Address,
108
+ hex: hexAddress,
109
+ });
110
+ return this.provider;
111
+ };
112
+
113
+ getProvider = (): undefined | CardanoLocalProvider => {
114
+ return this.provider;
115
+ };
116
+
117
+ getOrThrowProvider = (): CardanoLocalProvider => {
118
+ if (this.provider == null) {
119
+ throw new Error("CardanoLocalConnector not initialized yet");
120
+ }
121
+ return this.provider;
122
+ };
123
+
124
+ isConnected = (): boolean => {
125
+ return this.provider != null;
126
+ };
127
+ }
128
+
129
+ export class CardanoLocalProvider implements IProvider<CardanoApi> {
130
+ constructor(
131
+ private readonly conn: ActiveConnection<CardanoApi>,
132
+ readonly address: CardanoAddress,
133
+ ) {}
134
+
135
+ getConnection = (): ActiveConnection<CardanoApi> => {
136
+ return this.conn;
137
+ };
138
+
139
+ getAddress = (): AddressAndType => {
140
+ return {
141
+ type: AddressType.CARDANO,
142
+ address: this.address.bech32,
143
+ };
144
+ };
145
+
146
+ signMessage = async (message: string): Promise<UserSignature> => {
147
+ const hexMessage = utf8ToHex(message).slice(2);
148
+ const { signature, key } = await (
149
+ this.conn.api as CardanoLocalApi
150
+ ).signData(this.address.bech32, hexMessage);
151
+ return `${signature}+${key}`;
152
+ };
153
+ }
154
+
155
+ async function addressToHex(lucid: any, bech32Address: string): Promise<string> {
156
+ const utilsMod = await import("@lucid-evolution/utils");
157
+ const details = utilsMod.getAddressDetails(bech32Address);
158
+ return details.address.hex;
159
+ }
@@ -0,0 +1,31 @@
1
+ import type { Result } from "@effectstream/utils";
2
+ import type { IProvider } from "../IProvider.ts";
3
+ import type { WalletMode, ApiForMode } from "../utils.ts";
4
+ import type { LoginInfoMap } from "../wallet-modes.ts";
5
+ import { CardanoLocalConnector } from "./local.ts";
6
+ import { formatError } from "../helpers/format-error.ts";
7
+
8
+ export async function cardanoLocalLoginWrapper(
9
+ loginInfo: LoginInfoMap[WalletMode.CardanoLocal],
10
+ ): Promise<Result<IProvider<ApiForMode<WalletMode.CardanoLocal>>>> {
11
+ try {
12
+ console.log("cardanoLocalLoginWrapper: building Lucid wallet from seed");
13
+ const provider = await CardanoLocalConnector.instance().connectFromSeed({
14
+ seedPhrase: loginInfo.seedPhrase,
15
+ network: loginInfo.network,
16
+ provider: loginInfo.provider,
17
+ });
18
+ return {
19
+ success: true,
20
+ result: provider as unknown as IProvider<
21
+ ApiForMode<WalletMode.CardanoLocal>
22
+ >,
23
+ };
24
+ } catch (err) {
25
+ console.log("cardanoLocalLoginWrapper: error while building local Cardano wallet");
26
+ return {
27
+ success: false,
28
+ errorMessage: formatError(err),
29
+ };
30
+ }
31
+ }
@@ -0,0 +1,37 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { CryptoManager } from "@effectstream/crypto";
3
+ import { AddressType } from "@effectstream/utils";
4
+ import { ViemConnector } from "./viem.ts";
5
+
6
+ const TEST_KEY =
7
+ "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80" as const;
8
+ const EXPECTED_ADDR = "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266";
9
+
10
+ describe("ViemEvmProvider", () => {
11
+ test("derives the hardhat default address from its private key", async () => {
12
+ const provider = await ViemConnector.instance().connectFromPrivateKey({
13
+ privateKey: TEST_KEY,
14
+ rpcUrl: "http://localhost:8545",
15
+ });
16
+ const { type, address } = provider.getAddress();
17
+ expect(type).toBe(AddressType.EVM);
18
+ expect(address).toBe(EXPECTED_ADDR);
19
+ });
20
+
21
+ test("signMessage produces a signature that @effectstream/crypto verifies", async () => {
22
+ const provider = await ViemConnector.instance().connectFromPrivateKey({
23
+ privateKey: TEST_KEY,
24
+ rpcUrl: "http://localhost:8545",
25
+ });
26
+ const message = "hello effectstream";
27
+ const signature = await provider.signMessage(message);
28
+
29
+ const evmCrypto = CryptoManager.getCryptoManager(AddressType.EVM);
30
+ const ok = await evmCrypto.verifySignature(
31
+ provider.getAddress().address,
32
+ message,
33
+ signature,
34
+ );
35
+ expect(ok).toBe(true);
36
+ });
37
+ });
@@ -0,0 +1,122 @@
1
+ import {
2
+ createWalletClient,
3
+ http,
4
+ type Chain,
5
+ type Hex,
6
+ type PrivateKeyAccount,
7
+ type WalletClient,
8
+ } from "viem";
9
+ import { privateKeyToAccount } from "viem/accounts";
10
+ import { AddressType } from "@effectstream/utils";
11
+ import type {
12
+ ActiveConnection,
13
+ AddressAndType,
14
+ IProvider,
15
+ UserSignature,
16
+ } from "../IProvider.ts";
17
+ import type { EvmAddress } from "./types.ts";
18
+
19
+ export type ViemApi = WalletClient;
20
+
21
+ export type ViemConnectArgs = {
22
+ privateKey: Hex;
23
+ rpcUrl: string;
24
+ chain?: Chain;
25
+ };
26
+
27
+ export class ViemConnector {
28
+ private provider: ViemEvmProvider | undefined;
29
+ private static INSTANCE: undefined | ViemConnector = undefined;
30
+
31
+ static instance(): ViemConnector {
32
+ if (ViemConnector.INSTANCE == null) {
33
+ ViemConnector.INSTANCE = new ViemConnector();
34
+ }
35
+ return ViemConnector.INSTANCE;
36
+ }
37
+
38
+ connectFromPrivateKey = async (
39
+ args: ViemConnectArgs,
40
+ ): Promise<ViemEvmProvider> => {
41
+ const account = privateKeyToAccount(args.privateKey);
42
+ const client = createWalletClient({
43
+ account,
44
+ chain: args.chain,
45
+ transport: http(args.rpcUrl),
46
+ });
47
+ this.provider = await ViemEvmProvider.init(client, account);
48
+ return this.provider;
49
+ };
50
+
51
+ connectExternal = async (
52
+ conn: ActiveConnection<ViemApi>,
53
+ ): Promise<ViemEvmProvider> => {
54
+ const account = conn.api.account;
55
+ if (account == null) {
56
+ throw new Error(
57
+ "ViemConnector.connectExternal: WalletClient.account is null. Build the client with createWalletClient({ account, ... }).",
58
+ );
59
+ }
60
+ this.provider = await ViemEvmProvider.init(conn.api, account, conn.metadata);
61
+ return this.provider;
62
+ };
63
+
64
+ getProvider = (): undefined | ViemEvmProvider => {
65
+ return this.provider;
66
+ };
67
+
68
+ getOrThrowProvider = (): ViemEvmProvider => {
69
+ if (this.provider == null) {
70
+ throw new Error("ViemConnector not initialized yet");
71
+ }
72
+ return this.provider;
73
+ };
74
+
75
+ isConnected = (): boolean => {
76
+ return this.provider != null;
77
+ };
78
+ }
79
+
80
+ export class ViemEvmProvider implements IProvider<ViemApi> {
81
+ constructor(
82
+ private readonly conn: ActiveConnection<ViemApi>,
83
+ private readonly account: PrivateKeyAccount | WalletClient["account"],
84
+ readonly address: EvmAddress,
85
+ ) {}
86
+
87
+ static init = async (
88
+ client: ViemApi,
89
+ account: NonNullable<WalletClient["account"]>,
90
+ metadata?: { name: string; displayName: string },
91
+ ): Promise<ViemEvmProvider> => {
92
+ const conn: ActiveConnection<ViemApi> = {
93
+ api: client,
94
+ metadata: metadata ?? {
95
+ name: "viem-local",
96
+ displayName: "Viem (local)",
97
+ },
98
+ };
99
+ return new ViemEvmProvider(conn, account, account.address as EvmAddress);
100
+ };
101
+
102
+ getConnection = (): ActiveConnection<ViemApi> => {
103
+ return this.conn;
104
+ };
105
+
106
+ getAddress = (): AddressAndType => {
107
+ return {
108
+ type: AddressType.EVM,
109
+ address: this.address.toLowerCase(),
110
+ };
111
+ };
112
+
113
+ signMessage = async (message: string): Promise<UserSignature> => {
114
+ if (this.account == null) {
115
+ throw new Error("ViemEvmProvider.signMessage: account is missing");
116
+ }
117
+ return await this.conn.api.signMessage({
118
+ account: this.account,
119
+ message,
120
+ });
121
+ };
122
+ }
@@ -3,6 +3,7 @@ import type { IProvider } from "../IProvider.ts";
3
3
  import type { WalletMode, ApiForMode } from "../utils.ts";
4
4
  import type { LoginInfoMap } from "../wallet-modes.ts";
5
5
  import { type EthersApi, EthersConnector } from "./ethers.ts";
6
+ import { formatError } from "../helpers/format-error.ts";
6
7
 
7
8
  async function connectWallet(
8
9
  loginInfo: LoginInfoMap[WalletMode.EvmEthers]
@@ -22,7 +23,7 @@ async function connectWallet(
22
23
 
23
24
  return {
24
25
  success: false,
25
- errorMessage: err instanceof Error ? err.message : String(err),
26
+ errorMessage: formatError(err),
26
27
  };
27
28
  }
28
29
  }
@@ -0,0 +1,31 @@
1
+ import type { Result } from "@effectstream/utils";
2
+ import type { IProvider } from "../IProvider.ts";
3
+ import type { WalletMode, ApiForMode } from "../utils.ts";
4
+ import type { LoginInfoMap } from "../wallet-modes.ts";
5
+ import { type ViemApi, ViemConnector } from "./viem.ts";
6
+ import { formatError } from "../helpers/format-error.ts";
7
+
8
+ export async function viemLoginWrapper(
9
+ loginInfo: LoginInfoMap[WalletMode.EvmViem],
10
+ ): Promise<Result<IProvider<ApiForMode<WalletMode.EvmViem>>>> {
11
+ try {
12
+ console.log("viemLoginWrapper: building viem WalletClient from private key");
13
+ const provider = await ViemConnector.instance().connectFromPrivateKey({
14
+ privateKey: loginInfo.privateKey,
15
+ rpcUrl: loginInfo.rpcUrl,
16
+ chain: loginInfo.chain,
17
+ });
18
+ return {
19
+ success: true,
20
+ result: provider as unknown as IProvider<ApiForMode<WalletMode.EvmViem>>,
21
+ };
22
+ } catch (err) {
23
+ console.log("viemLoginWrapper: error while building local viem wallet");
24
+ return {
25
+ success: false,
26
+ errorMessage: formatError(err),
27
+ };
28
+ }
29
+ }
30
+
31
+ export type { ViemApi };
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Coerce anything a wallet/SDK can throw or reject with into a readable
3
+ * string.
4
+ *
5
+ * Wallet bridges return errors in three common shapes:
6
+ * 1. `Error` instances (most JS SDKs)
7
+ * 2. CIP-30-style `{ code: number, info: string }` (Midnight Lace, some
8
+ * Cardano wallets, Mina/Auro)
9
+ * 3. Plain strings or numbers
10
+ *
11
+ * `String(obj)` on shape #2 yields the useless `"[object Object]"`, which
12
+ * leaks into UIs as "Error: [object Object]". This helper unpacks all three.
13
+ */
14
+ export function formatError(err: unknown): string {
15
+ if (err == null) return "";
16
+ if (err instanceof Error) return err.message;
17
+ if (typeof err === "string") return err;
18
+ if (typeof err === "object") {
19
+ const o = err as { info?: string; message?: string; code?: number };
20
+ if (typeof o.info === "string") {
21
+ return o.code != null ? `${o.info} (code ${o.code})` : o.info;
22
+ }
23
+ if (typeof o.message === "string") return o.message;
24
+ try {
25
+ return JSON.stringify(err);
26
+ } catch {
27
+ // Cyclic objects land here.
28
+ }
29
+ }
30
+ return String(err);
31
+ }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Inline SVG icons for the three local-JS wallet modes.
3
+ *
4
+ * Each is a 32x32 circle filled with the chain's brand colour and the letters
5
+ * "JS" centered in white, so the wallets-ui (and templates) can distinguish
6
+ * the seed-driven local wallets from injected browser-extension wallets at a
7
+ * glance.
8
+ *
9
+ * Returned as `data:image/svg+xml` data URIs so they slot directly into
10
+ * `WalletOption.icon`, which the UI renders inside `<img>` tags.
11
+ */
12
+
13
+ function encodeSvg(svg: string): string {
14
+ return `data:image/svg+xml;utf8,${encodeURIComponent(svg)}`;
15
+ }
16
+
17
+ function jsCircle(fill: string, textFill = "#ffffff"): string {
18
+ return (
19
+ `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">` +
20
+ `<circle cx="16" cy="16" r="16" fill="${fill}"/>` +
21
+ `<text x="16" y="21" text-anchor="middle" ` +
22
+ `font-family="Inter,-apple-system,BlinkMacSystemFont,Segoe UI,sans-serif" ` +
23
+ `font-weight="700" font-size="13" fill="${textFill}">JS</text>` +
24
+ `</svg>`
25
+ );
26
+ }
27
+
28
+ /** Orange circle — EVM Viem local wallet. */
29
+ export const evmViemIcon = encodeSvg(jsCircle("#F97316"));
30
+
31
+ /** Cardano brand blue circle — Cardano Lucid local wallet. */
32
+ export const cardanoLocalIcon = encodeSvg(jsCircle("#0033AD"));
33
+
34
+ /** Near-black circle — Midnight local-seed wallet. */
35
+ export const midnightLocalIcon = encodeSvg(jsCircle("#0A0A0A"));
@@ -0,0 +1,89 @@
1
+ import { WalletMode } from "../utils.ts";
2
+ import type { LoginInfo, LoginInfoMap } from "../wallet-modes.ts";
3
+
4
+ export type ChainKey = "evm" | "cardano" | "midnight";
5
+
6
+ type StripMode<T> = T extends { mode: WalletMode } ? Omit<T, "mode"> : T;
7
+
8
+ type LocalModeForChain<C extends ChainKey> = C extends "evm"
9
+ ? WalletMode.EvmViem
10
+ : C extends "cardano"
11
+ ? WalletMode.CardanoLocal
12
+ : C extends "midnight"
13
+ ? WalletMode.MidnightLocal
14
+ : never;
15
+
16
+ type InjectedModeForChain<C extends ChainKey> = C extends "evm"
17
+ ? WalletMode.EvmInjected
18
+ : C extends "cardano"
19
+ ? WalletMode.Cardano
20
+ : C extends "midnight"
21
+ ? WalletMode.Midnight
22
+ : never;
23
+
24
+ export type LocalOptionsForChain<C extends ChainKey> = StripMode<
25
+ LoginInfoMap[LocalModeForChain<C>]
26
+ >;
27
+
28
+ export type InjectedOptionsForChain<C extends ChainKey> = StripMode<
29
+ LoginInfoMap[InjectedModeForChain<C>]
30
+ >;
31
+
32
+ export type PickWalletModeArgs<C extends ChainKey> = {
33
+ chain: C;
34
+ /** True → return a local-wallet LoginInfo; false → injected. */
35
+ preferLocal: boolean;
36
+ /** Options for the local-wallet branch. Required because callers can't no-op a local wallet (it needs a seed/key). */
37
+ localOptions: LocalOptionsForChain<C>;
38
+ /** Options for the injected branch. Required so users always see the same wallet picker UX. */
39
+ injectedOptions: InjectedOptionsForChain<C>;
40
+ };
41
+
42
+ /**
43
+ * Pick between an injected (browser-extension) and a local (seed/key) wallet
44
+ * based on `preferLocal`, and return a ready-to-pass `LoginInfo` for
45
+ * `walletLogin()`. Both branches return the same `Wallet` shape from
46
+ * `walletLogin`, so the rest of the app does not need to branch on mode.
47
+ *
48
+ * Typical use (Vite frontend, headless test, etc.):
49
+ * ```ts
50
+ * const wallet = await walletLogin(pickWalletMode({
51
+ * chain: "midnight",
52
+ * preferLocal: import.meta.env.VITE_WALLET_MODE === "local",
53
+ * localOptions: { seed, networkId },
54
+ * injectedOptions: { preference: { name: "lace" }, networkId },
55
+ * }));
56
+ * ```
57
+ */
58
+ export function pickWalletMode<C extends ChainKey>(
59
+ args: PickWalletModeArgs<C>,
60
+ ): LoginInfo {
61
+ if (args.preferLocal) {
62
+ const mode = localModeFor(args.chain);
63
+ return { mode, ...(args.localOptions as object) } as LoginInfo;
64
+ }
65
+ const mode = injectedModeFor(args.chain);
66
+ return { mode, ...(args.injectedOptions as object) } as LoginInfo;
67
+ }
68
+
69
+ function localModeFor(chain: ChainKey): WalletMode {
70
+ switch (chain) {
71
+ case "evm":
72
+ return WalletMode.EvmViem;
73
+ case "cardano":
74
+ return WalletMode.CardanoLocal;
75
+ case "midnight":
76
+ return WalletMode.MidnightLocal;
77
+ }
78
+ }
79
+
80
+ function injectedModeFor(chain: ChainKey): WalletMode {
81
+ switch (chain) {
82
+ case "evm":
83
+ return WalletMode.EvmInjected;
84
+ case "cardano":
85
+ return WalletMode.Cardano;
86
+ case "midnight":
87
+ return WalletMode.Midnight;
88
+ }
89
+ }
@@ -0,0 +1,82 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { Buffer } from "node:buffer";
3
+ import { AddressType } from "@effectstream/utils";
4
+ import { verifySignature } from "@midnight-ntwrk/ledger-v8";
5
+ import { CryptoManager } from "@effectstream/crypto";
6
+ import { MidnightLocalConnector, type MidnightLocalApi } from "./local.ts";
7
+
8
+ const DETERMINISTIC_SEED =
9
+ "0000000000000000000000000000000000000000000000000000000000000001";
10
+
11
+ describe("MidnightLocalProvider", () => {
12
+ test("derives an unshielded bech32 address from a deterministic seed", async () => {
13
+ const provider = await MidnightLocalConnector.instance().connectFromSeed({
14
+ seed: DETERMINISTIC_SEED,
15
+ networkId: "undeployed",
16
+ });
17
+ const { type, address } = provider.getAddress();
18
+ expect(type).toBe(AddressType.MIDNIGHT);
19
+ expect(typeof address).toBe("string");
20
+ expect(address.length).toBeGreaterThan(0);
21
+ });
22
+
23
+ test("signMessage produces a signature that verifies via ledger-v8", async () => {
24
+ const provider = await MidnightLocalConnector.instance().connectFromSeed({
25
+ seed: DETERMINISTIC_SEED,
26
+ networkId: "undeployed",
27
+ });
28
+ const api = provider.getConnection().api as unknown as MidnightLocalApi;
29
+ const message = "hello effectstream";
30
+ const signed = await api.signData(message, {
31
+ encoding: "text",
32
+ keyType: "unshielded",
33
+ });
34
+
35
+ const messageBytes = Buffer.from(message, "utf-8");
36
+ const ok = verifySignature(
37
+ signed.verifyingKey as never,
38
+ messageBytes,
39
+ signed.signature as never,
40
+ );
41
+ expect(ok).toBe(true);
42
+
43
+ // signMessage on the IProvider surface returns "signature|verifyingKey"
44
+ // (Midnight signing is non-deterministic, so the inner signature differs
45
+ // each call). The combined form is what CryptoManager.Midnight() consumes.
46
+ const sig = await provider.signMessage(message);
47
+ const [innerSig, innerVk, ...rest] = sig.split("|");
48
+ expect(rest).toEqual([]);
49
+ expect(innerVk).toBe(signed.verifyingKey);
50
+ const okAgain = verifySignature(
51
+ innerVk as never,
52
+ messageBytes,
53
+ innerSig as never,
54
+ );
55
+ expect(okAgain).toBe(true);
56
+ });
57
+
58
+ test("signMessage round-trips through CryptoManager.Midnight().verifySignature", async () => {
59
+ const provider = await MidnightLocalConnector.instance().connectFromSeed({
60
+ seed: DETERMINISTIC_SEED,
61
+ networkId: "undeployed",
62
+ });
63
+ const message = "hello effectstream from @effectstream/crypto";
64
+ const sig = await provider.signMessage(message);
65
+
66
+ const midnightCrypto = CryptoManager.getCryptoManager(AddressType.MIDNIGHT);
67
+ const ok = await midnightCrypto.verifySignature(
68
+ provider.getAddress().address,
69
+ message,
70
+ sig,
71
+ );
72
+ expect(ok).toBe(true);
73
+
74
+ // Tampering with the message must fail verification.
75
+ const tampered = await midnightCrypto.verifySignature(
76
+ provider.getAddress().address,
77
+ `${message} tampered`,
78
+ sig,
79
+ );
80
+ expect(tampered).toBe(false);
81
+ });
82
+ });
@@ -0,0 +1,205 @@
1
+ import { Buffer } from "node:buffer";
2
+ import { AddressType, type MidnightAddress } from "@effectstream/utils";
3
+ import type {
4
+ ActiveConnection,
5
+ AddressAndType,
6
+ IProvider,
7
+ UserSignature,
8
+ } from "../IProvider.ts";
9
+ import type { MidnightApi } from "./midnight.ts";
10
+
11
+ export type MidnightLocalConnectArgs = {
12
+ /** 64-character hex seed. Generated when omitted. */
13
+ seed?: string;
14
+ /** Midnight network id — sets the bech32 prefix on the unshielded address. */
15
+ networkId: string;
16
+ };
17
+
18
+ /**
19
+ * Shape exposed on `getConnection().api`. A minimal subset of `ConnectedAPI`
20
+ * that the rest of Effectstream's Midnight code consumes:
21
+ * - `signData` mirrors the dapp-connector-api shape.
22
+ * - `getShieldedAddresses` is stubbed (shielded sync is out of scope for the
23
+ * lite/seed-only path — callers that need shielded/dust addresses should
24
+ * build the full WalletFacade via `@effectstream/midnight-contracts`).
25
+ */
26
+ export type MidnightLocalApi = {
27
+ signData: (
28
+ data: string,
29
+ options: { encoding: "text" | "hex"; keyType: "unshielded" },
30
+ ) => Promise<{ data: string; signature: string; verifyingKey: string }>;
31
+ getShieldedAddresses: () => Promise<{ shieldedAddress: string }>;
32
+ /** The derived seed, exposed so tests can persist/reuse the wallet. */
33
+ seed: string;
34
+ /** Public verifying key for the unshielded signing role (hex). */
35
+ verifyingKey: string;
36
+ /** Bech32-encoded unshielded address. */
37
+ unshieldedAddress: string;
38
+ };
39
+
40
+ export class MidnightLocalConnector {
41
+ private provider: MidnightLocalProvider | undefined;
42
+ private static INSTANCE: undefined | MidnightLocalConnector = undefined;
43
+
44
+ static instance(): MidnightLocalConnector {
45
+ if (MidnightLocalConnector.INSTANCE == null) {
46
+ MidnightLocalConnector.INSTANCE = new MidnightLocalConnector();
47
+ }
48
+ return MidnightLocalConnector.INSTANCE;
49
+ }
50
+
51
+ /**
52
+ * Build a Midnight wallet identity from a hex seed.
53
+ *
54
+ * This is the "lite" / signing-only path: it derives the unshielded
55
+ * keystore from the seed and exposes a CIP-30-shaped `signData` backed by
56
+ * `unshieldedKeystore.signData(...)`. It does NOT bring up the shielded /
57
+ * dust wallets — that requires a live indexer + proof server and lives in
58
+ * `@effectstream/midnight-contracts`'s `buildWalletFacade`.
59
+ */
60
+ connectFromSeed = async (
61
+ args: MidnightLocalConnectArgs,
62
+ ): Promise<MidnightLocalProvider> => {
63
+ const hdMod = await import("@midnight-ntwrk/wallet-sdk-hd").catch(() => {
64
+ throw new Error(
65
+ "@midnight-ntwrk/wallet-sdk-hd is required for WalletMode.MidnightLocal.",
66
+ );
67
+ });
68
+ const keystoreMod = await import(
69
+ "@midnight-ntwrk/wallet-sdk-unshielded-wallet"
70
+ ).catch(() => {
71
+ throw new Error(
72
+ "@midnight-ntwrk/wallet-sdk-unshielded-wallet is required for WalletMode.MidnightLocal.",
73
+ );
74
+ });
75
+
76
+ const seed = args.seed ?? generateRandomHexSeed(32);
77
+ const unshieldedSeed = deriveUnshieldedSeed(
78
+ hdMod,
79
+ seed,
80
+ hdMod.Roles.NightExternal,
81
+ );
82
+ const unshieldedKeystore = keystoreMod.createKeystore(
83
+ unshieldedSeed,
84
+ args.networkId as never,
85
+ );
86
+
87
+ const unshieldedAddress = unshieldedKeystore.getBech32Address().asString();
88
+ const verifyingKey = String(unshieldedKeystore.getPublicKey());
89
+
90
+ const api: MidnightLocalApi = {
91
+ signData: async (data, options) => {
92
+ const bytes =
93
+ options.encoding === "hex"
94
+ ? Buffer.from(data, "hex")
95
+ : Buffer.from(data, "utf-8");
96
+ const signature = unshieldedKeystore.signData(bytes);
97
+ return {
98
+ data,
99
+ signature: String(signature),
100
+ verifyingKey,
101
+ };
102
+ },
103
+ getShieldedAddresses: async () => {
104
+ throw new Error(
105
+ "MidnightLocal lite mode does not expose a shielded address. Build a WalletFacade via @effectstream/midnight-contracts for shielded/dust support.",
106
+ );
107
+ },
108
+ seed,
109
+ verifyingKey,
110
+ unshieldedAddress,
111
+ };
112
+
113
+ const conn: ActiveConnection<MidnightApi> = {
114
+ api: api as unknown as MidnightApi,
115
+ metadata: {
116
+ name: "midnight-local",
117
+ displayName: "Midnight (local seed)",
118
+ },
119
+ };
120
+
121
+ this.provider = new MidnightLocalProvider(
122
+ conn,
123
+ unshieldedAddress as MidnightAddress,
124
+ unshieldedKeystore,
125
+ );
126
+ return this.provider;
127
+ };
128
+
129
+ getProvider = (): undefined | MidnightLocalProvider => {
130
+ return this.provider;
131
+ };
132
+
133
+ getOrThrowProvider = (): MidnightLocalProvider => {
134
+ if (this.provider == null) {
135
+ throw new Error("MidnightLocalConnector not initialized yet");
136
+ }
137
+ return this.provider;
138
+ };
139
+
140
+ isConnected = (): boolean => {
141
+ return this.provider != null;
142
+ };
143
+ }
144
+
145
+ export class MidnightLocalProvider implements IProvider<MidnightApi> {
146
+ constructor(
147
+ private readonly conn: ActiveConnection<MidnightApi>,
148
+ readonly address: MidnightAddress,
149
+ private readonly unshieldedKeystore: unknown,
150
+ ) {}
151
+
152
+ getConnection = (): ActiveConnection<MidnightApi> => {
153
+ return this.conn;
154
+ };
155
+
156
+ getAddress = (): AddressAndType => {
157
+ return {
158
+ type: AddressType.MIDNIGHT,
159
+ address: this.address,
160
+ };
161
+ };
162
+
163
+ signMessage = async (message: string): Promise<UserSignature> => {
164
+ const signed = await (this.conn.api as unknown as MidnightLocalApi).signData(
165
+ message,
166
+ { encoding: "text", keyType: "unshielded" },
167
+ );
168
+ // Format: "signature|verifyingKey". Midnight signatures are not
169
+ // self-recovering (unlike EVM ECDSA), so the verifier needs the public
170
+ // key to call ledger-v8.verifySignature. Mirrors Cardano's "sig+key"
171
+ // convention, just with `|` as the separator to keep the formats distinct.
172
+ return `${signed.signature}|${signed.verifyingKey}`;
173
+ };
174
+
175
+ /** Expose the keystore for callers that need the raw signing primitive. */
176
+ getUnshieldedKeystore = (): unknown => this.unshieldedKeystore;
177
+ }
178
+
179
+ function deriveUnshieldedSeed(
180
+ hdMod: typeof import("@midnight-ntwrk/wallet-sdk-hd"),
181
+ seedHex: string,
182
+ role: import("@midnight-ntwrk/wallet-sdk-hd").Role,
183
+ ): Uint8Array {
184
+ const seedBuffer = Buffer.from(seedHex, "hex");
185
+ const hdResult = hdMod.HDWallet.fromSeed(seedBuffer);
186
+ if (hdResult.type !== "seedOk") {
187
+ throw new Error(`HDWallet.fromSeed failed: ${hdResult.type}`);
188
+ }
189
+ const derivation = hdResult.hdWallet
190
+ .selectAccount(0)
191
+ .selectRole(role)
192
+ .deriveKeyAt(0);
193
+ if (derivation.type !== "keyDerived") {
194
+ throw new Error(`HDWallet.deriveKeyAt failed: ${derivation.type}`);
195
+ }
196
+ return Buffer.from(derivation.key);
197
+ }
198
+
199
+ function generateRandomHexSeed(byteLength: number): string {
200
+ const bytes = new Uint8Array(byteLength);
201
+ crypto.getRandomValues(bytes);
202
+ return Array.from(bytes)
203
+ .map((b) => b.toString(16).padStart(2, "0"))
204
+ .join("");
205
+ }
@@ -132,11 +132,15 @@ export class MidnightProvider implements IProvider<MidnightApi> {
132
132
  static init = async (
133
133
  conn: ActiveConnection<MidnightApi>
134
134
  ): Promise<MidnightProvider> => {
135
- const hexAddress = await MidnightProvider.fetchAddress(conn);
136
- // const prefix = pickBech32Prefix(hexAddress);
137
- // const words = bech32.toWords(hexStringToUint8Array(hexAddress));
138
- // const userAddress = bech32.encode(prefix, words, 200);
139
-
135
+ // The IProvider's primary address must be the UNSHIELDED one — that's
136
+ // the address `addressFromKey(verifyingKey)` resolves to for the
137
+ // `signData({keyType: "unshielded"})` signatures we produce in
138
+ // `signMessage`, which is what `CryptoManager.Midnight().verifySignature`
139
+ // (and therefore the batcher) cross-checks. The shielded and dust
140
+ // addresses remain reachable via `fetchAddress(conn, { type })` or
141
+ // directly through `conn.api.getShieldedAddresses()` /
142
+ // `getDustAddress()`.
143
+ const hexAddress = await MidnightProvider.fetchAddress(conn, { type: "unshielded" });
140
144
  return new MidnightProvider(conn, hexAddress as MidnightAddress);
141
145
  };
142
146
 
@@ -152,7 +156,12 @@ export class MidnightProvider implements IProvider<MidnightApi> {
152
156
  encoding: "text",
153
157
  keyType,
154
158
  })) as Signature;
155
- return signature.signature;
159
+ // Format: "signature|verifyingKey". Midnight unshielded signatures are
160
+ // not self-recovering, so `@effectstream/crypto`'s verifier needs the
161
+ // public key alongside the signature (matches the local-seed wallet
162
+ // and mirrors Cardano's "sig+key" convention with a different
163
+ // separator).
164
+ return `${signature.signature}|${signature.verifyingKey}`;
156
165
  };
157
166
 
158
167
  getAddress(): AddressAndType {
@@ -162,12 +171,49 @@ export class MidnightProvider implements IProvider<MidnightApi> {
162
171
  }
163
172
  }
164
173
 
165
- static fetchAddress = async (conn: ActiveConnection<MidnightApi>): Promise<string> => {
166
- const addresses = await conn.api.getShieldedAddresses();
167
- if (!addresses?.shieldedAddress) {
168
- throw new Error("Midnight wallet did not return a shielded address");
174
+ /**
175
+ * Fetch a specific address type from a connected Midnight wallet. Defaults
176
+ * to the **shielded** address to keep existing callers backwards-compatible
177
+ * with the pre-multi-address behaviour. The IProvider primary address
178
+ * (`getAddress()`) is the **unshielded** one — that's the address
179
+ * `addressFromKey(verifyingKey)` resolves to and what
180
+ * `CryptoManager.Midnight().verifySignature` needs to cross-check.
181
+ *
182
+ * @example
183
+ * ```ts
184
+ * const shielded = await MidnightProvider.fetchAddress(conn);
185
+ * const unshielded = await MidnightProvider.fetchAddress(conn, { type: "unshielded" });
186
+ * const dust = await MidnightProvider.fetchAddress(conn, { type: "dust" });
187
+ * ```
188
+ */
189
+ static fetchAddress = async (
190
+ conn: ActiveConnection<MidnightApi>,
191
+ options: { type?: "shielded" | "unshielded" | "dust" } = {},
192
+ ): Promise<string> => {
193
+ const type = options.type ?? "shielded";
194
+ switch (type) {
195
+ case "shielded": {
196
+ const res = await conn.api.getShieldedAddresses();
197
+ if (!res?.shieldedAddress) {
198
+ throw new Error("Midnight wallet did not return a shielded address");
199
+ }
200
+ return res.shieldedAddress;
201
+ }
202
+ case "unshielded": {
203
+ const res = await conn.api.getUnshieldedAddress();
204
+ if (!res?.unshieldedAddress) {
205
+ throw new Error("Midnight wallet did not return an unshielded address");
206
+ }
207
+ return res.unshieldedAddress;
208
+ }
209
+ case "dust": {
210
+ const res = await conn.api.getDustAddress();
211
+ if (!res?.dustAddress) {
212
+ throw new Error("Midnight wallet did not return a dust address");
213
+ }
214
+ return res.dustAddress;
215
+ }
169
216
  }
170
- return addresses.shieldedAddress;
171
217
  };
172
218
  }
173
219
 
@@ -0,0 +1,30 @@
1
+ import type { Result } from "@effectstream/utils";
2
+ import type { IProvider } from "../IProvider.ts";
3
+ import type { WalletMode, ApiForMode } from "../utils.ts";
4
+ import type { LoginInfoMap } from "../wallet-modes.ts";
5
+ import { MidnightLocalConnector } from "./local.ts";
6
+ import { formatError } from "../helpers/format-error.ts";
7
+
8
+ export async function midnightLocalLoginWrapper(
9
+ loginInfo: LoginInfoMap[WalletMode.MidnightLocal],
10
+ ): Promise<Result<IProvider<ApiForMode<WalletMode.MidnightLocal>>>> {
11
+ try {
12
+ console.log("midnightLocalLoginWrapper: deriving unshielded keystore from seed");
13
+ const provider = await MidnightLocalConnector.instance().connectFromSeed({
14
+ seed: loginInfo.seed,
15
+ networkId: loginInfo.networkId,
16
+ });
17
+ return {
18
+ success: true,
19
+ result: provider as unknown as IProvider<
20
+ ApiForMode<WalletMode.MidnightLocal>
21
+ >,
22
+ };
23
+ } catch (err) {
24
+ console.log("midnightLocalLoginWrapper: error building local Midnight wallet");
25
+ return {
26
+ success: false,
27
+ errorMessage: formatError(err),
28
+ };
29
+ }
30
+ }
package/src/mod.ts CHANGED
@@ -7,4 +7,23 @@ export { WalletMode, WalletNameMap } from "./utils.ts";
7
7
  export { walletLogin } from "./wallets.ts";
8
8
  export { allInjectedWallets } from "./utils.ts";
9
9
  export { getAddressType } from "./wallet-modes.ts";
10
+ export type { LoginInfo, LoginInfoMap } from "./wallet-modes.ts";
11
+ // Local-wallet mode helper (env-driven injected↔local swap).
12
+ export { pickWalletMode } from "./helpers/pick-mode.ts";
13
+ export type {
14
+ ChainKey,
15
+ PickWalletModeArgs,
16
+ LocalOptionsForChain,
17
+ InjectedOptionsForChain,
18
+ } from "./helpers/pick-mode.ts";
19
+ // Inline data-URI icons for the three local-JS wallet modes (orange/blue/black
20
+ // JS circles), suitable for use as `WalletOption.icon`.
21
+ export {
22
+ evmViemIcon,
23
+ cardanoLocalIcon,
24
+ midnightLocalIcon,
25
+ } from "./helpers/local-icons.ts";
26
+ // Coerces wallet/SDK rejects (including CIP-30-style `{code, info}` objects)
27
+ // to a readable string.
28
+ export { formatError } from "./helpers/format-error.ts";
10
29
  export * from "./effectstream.ts";
package/src/utils.ts CHANGED
@@ -7,18 +7,24 @@ import type {
7
7
 
8
8
  import { AlgorandConnector } from "./algorand/algorand.ts";
9
9
  import { EthersConnector } from "./evm/ethers.ts";
10
+ import { ViemConnector } from "./evm/viem.ts";
10
11
  import { EvmInjectedConnector } from "./evm/injected.ts";
11
12
  import { CardanoConnector } from "./cardano/cardano.ts";
13
+ import { CardanoLocalConnector } from "./cardano/local.ts";
12
14
  import { PolkadotConnector } from "./polkadot/polkadot.ts";
13
15
  import { MinaConnector } from "./mina/mina.ts";
14
16
  import { AvailConnector } from "./avail/avail.ts";
15
17
  import { MidnightConnector } from "./midnight/midnight.ts";
18
+ import { MidnightLocalConnector } from "./midnight/local.ts";
16
19
 
17
20
  export const enum WalletMode {
18
21
  EvmInjected,
19
22
  EvmEthers,
23
+ EvmViem,
20
24
  Midnight,
25
+ MidnightLocal,
21
26
  Cardano,
27
+ CardanoLocal,
22
28
  Polkadot,
23
29
  Algorand,
24
30
  Mina,
@@ -28,8 +34,11 @@ export const enum WalletMode {
28
34
  export const WalletNameMap: Record<WalletMode, string> = {
29
35
  [WalletMode.EvmInjected]: "EVM",
30
36
  [WalletMode.EvmEthers]: "EVM",
37
+ [WalletMode.EvmViem]: "EVM",
31
38
  [WalletMode.Midnight]: "Midnight",
39
+ [WalletMode.MidnightLocal]: "Midnight",
32
40
  [WalletMode.Cardano]: "Cardano",
41
+ [WalletMode.CardanoLocal]: "Cardano",
33
42
  [WalletMode.Polkadot]: 'Polkadot',
34
43
  [WalletMode.Algorand]: 'Algorand',
35
44
  [WalletMode.Mina]: "Mina",
@@ -39,8 +48,11 @@ export const WalletNameMap: Record<WalletMode, string> = {
39
48
  export const WalletModeMap = {
40
49
  [WalletMode.EvmInjected]: EvmInjectedConnector.instance(),
41
50
  [WalletMode.EvmEthers]: EthersConnector.instance(),
51
+ [WalletMode.EvmViem]: ViemConnector.instance(),
42
52
  [WalletMode.Midnight]: MidnightConnector.instance(),
53
+ [WalletMode.MidnightLocal]: MidnightLocalConnector.instance(),
43
54
  [WalletMode.Cardano]: CardanoConnector.instance(),
55
+ [WalletMode.CardanoLocal]: CardanoLocalConnector.instance(),
44
56
  [WalletMode.Polkadot]: PolkadotConnector.instance(),
45
57
  [WalletMode.Algorand]: AlgorandConnector.instance(),
46
58
  [WalletMode.Mina]: MinaConnector.instance(),
@@ -87,9 +99,12 @@ export async function allInjectedWallets(config: {
87
99
  [WalletMode.Polkadot]: await PolkadotConnector.getWalletOptions(),
88
100
  [WalletMode.Mina]: MinaConnector.getWalletOptions(),
89
101
  // TODO: Algorand signature support is not implemented yet.
90
- // NOTE: Midnight Do not support signature yet.
91
102
  [WalletMode.Algorand]: signatureSupport? [] : AlgorandConnector.getWalletOptions(),
92
- [WalletMode.Midnight]: signatureSupport? [] : MidnightConnector.getWalletOptions(),
103
+ // Midnight signing via ConnectedAPI.signData(message, {keyType: "unshielded"})
104
+ // works as of @effectstream/crypto's MidnightCrypto.verifySignature
105
+ // implementation, so it now surfaces in the injected-wallet list even when
106
+ // signature support is requested.
107
+ [WalletMode.Midnight]: MidnightConnector.getWalletOptions(),
93
108
  };
94
109
 
95
110
  return wallets;
@@ -2,8 +2,11 @@ import type { ApiPromise } from "avail-js-sdk";
2
2
  import type { Result } from "@effectstream/utils";
3
3
  import type { AlgorandApi } from "./algorand/algorand.ts";
4
4
  import type { CardanoApi } from "./cardano/cardano.ts";
5
+ import type { CardanoLocalNetwork } from "./cardano/local.ts";
5
6
  import type { EthersApi } from "./evm/ethers.ts";
7
+ import type { ViemApi } from "./evm/viem.ts";
6
8
  import type { EvmApi } from "./evm/injected.ts";
9
+ import type { Hex } from "viem";
7
10
  import type {
8
11
  ActiveConnection,
9
12
  IInjectedConnector,
@@ -17,7 +20,9 @@ import {
17
20
  WalletMode,
18
21
  } from "./utils.ts";
19
22
  import { AddressType } from "@effectstream/utils";
23
+ import { formatError } from "./helpers/format-error.ts";
20
24
  import type { MidnightApi } from "./midnight/midnight.ts";
25
+ import type {} from "./midnight/local.ts";
21
26
  import type { Chain } from "viem/chains";
22
27
  import { Wallet } from "ethers";
23
28
 
@@ -42,7 +47,25 @@ export type LoginInfoMap = {
42
47
  connection: ActiveConnection<EthersApi>;
43
48
  preferBatchedMode: boolean;
44
49
  };
50
+ [WalletMode.EvmViem]: {
51
+ /** 0x-prefixed 32-byte hex private key. */
52
+ privateKey: Hex;
53
+ /** RPC URL the viem WalletClient should target. */
54
+ rpcUrl: string;
55
+ /** Optional viem Chain — pass to enable chain-aware tx fields (chainId etc). */
56
+ chain?: Chain;
57
+ /** If true, even EVM inputs are batched (sign + send to batcher). */
58
+ preferBatchedMode?: boolean;
59
+ };
45
60
  [WalletMode.Cardano]: BaseLoginInfo<CardanoApi>;
61
+ [WalletMode.CardanoLocal]: {
62
+ /** BIP-39 mnemonic. Generated when omitted. */
63
+ seedPhrase?: string;
64
+ /** Cardano network — sets the bech32 prefix. */
65
+ network: CardanoLocalNetwork;
66
+ /** Optional Lucid Provider; only needed for on-chain ops, not pure signing. */
67
+ provider?: unknown;
68
+ };
46
69
  [WalletMode.Polkadot]: BaseLoginInfo<PolkadotApi>;
47
70
  [WalletMode.Algorand]: BaseLoginInfo<AlgorandApi>;
48
71
  [WalletMode.Mina]: BaseLoginInfo<MinaApi>;
@@ -54,6 +77,12 @@ export type LoginInfoMap = {
54
77
  [WalletMode.Midnight]: BaseLoginInfo<MidnightApi> & {
55
78
  networkId?: string;
56
79
  };
80
+ [WalletMode.MidnightLocal]: {
81
+ /** 64-char hex seed. Generated when omitted. */
82
+ seed?: string;
83
+ /** Midnight network id ("undeployed" | "testnet" | …) — sets the bech32 prefix. */
84
+ networkId: string;
85
+ };
57
86
  };
58
87
 
59
88
  type ToUnion<T> = {
@@ -93,7 +122,7 @@ export async function connectInjected<Api>(
93
122
 
94
123
  return {
95
124
  success: false,
96
- errorMessage: err instanceof Error ? err.message : String(err),
125
+ errorMessage: formatError(err),
97
126
  };
98
127
  }
99
128
  }
@@ -103,8 +132,10 @@ export function getAddressType(mode: WalletMode): AddressType {
103
132
 
104
133
  case WalletMode.EvmInjected:
105
134
  case WalletMode.EvmEthers:
135
+ case WalletMode.EvmViem:
106
136
  return AddressType.EVM;
107
137
  case WalletMode.Cardano:
138
+ case WalletMode.CardanoLocal:
108
139
  return AddressType.CARDANO;
109
140
  case WalletMode.Polkadot:
110
141
  return AddressType.POLKADOT;
@@ -113,6 +144,7 @@ export function getAddressType(mode: WalletMode): AddressType {
113
144
  case WalletMode.Mina:
114
145
  return AddressType.MINA;
115
146
  case WalletMode.Midnight:
147
+ case WalletMode.MidnightLocal:
116
148
  return AddressType.MIDNIGHT;
117
149
  case WalletMode.AvailJs:
118
150
  return AddressType.AVAIL;
package/src/wallets.ts CHANGED
@@ -1,9 +1,11 @@
1
1
  import type { IProvider } from "./IProvider.ts";
2
2
  import { algorandLoginWrapper } from "./algorand/wrapper.ts";
3
3
  import { cardanoLoginWrapper } from "./cardano/wrapper.ts";
4
+ import { cardanoLocalLoginWrapper } from "./cardano/wrapper-local.ts";
4
5
  import { evmLoginWrapper } from "./evm/wrapper-injected.ts";
5
6
  import { polkadotLoginWrapper } from "./polkadot/wrapper.ts";
6
7
  import { ethersLoginWrapper } from "./evm/wrapper-ethers.ts";
8
+ import { viemLoginWrapper } from "./evm/wrapper-viem.ts";
7
9
  import { minaLoginWrapper } from "./mina/wrapper.ts";
8
10
  import { availJsLoginWrapper } from "./avail/wrapper.ts";
9
11
  import { WalletMode } from "./utils.ts";
@@ -12,6 +14,7 @@ import type { Wallet } from "./types.ts";
12
14
  import type { LoginInfo } from "./wallet-modes.ts";
13
15
  // import { assertNever } from "assert-never";
14
16
  import { midnightLoginWrapper } from "./midnight/wrapper.ts";
17
+ import { midnightLocalLoginWrapper } from "./midnight/wrapper-local.ts";
15
18
 
16
19
  export async function walletLogin(
17
20
  loginInfo: LoginInfo
@@ -38,9 +41,15 @@ async function login(loginInfo: LoginInfo): Promise<Result<IProvider<unknown>>>
38
41
  case WalletMode.EvmEthers: {
39
42
  return await ethersLoginWrapper(loginInfo);
40
43
  }
44
+ case WalletMode.EvmViem: {
45
+ return await viemLoginWrapper(loginInfo);
46
+ }
41
47
  case WalletMode.Cardano: {
42
48
  return await cardanoLoginWrapper(loginInfo);
43
49
  }
50
+ case WalletMode.CardanoLocal: {
51
+ return await cardanoLocalLoginWrapper(loginInfo);
52
+ }
44
53
  case WalletMode.Polkadot: {
45
54
  return await polkadotLoginWrapper(loginInfo);
46
55
  }
@@ -56,6 +65,9 @@ async function login(loginInfo: LoginInfo): Promise<Result<IProvider<unknown>>>
56
65
  case WalletMode.Midnight: {
57
66
  return await midnightLoginWrapper(loginInfo);
58
67
  }
68
+ case WalletMode.MidnightLocal: {
69
+ return await midnightLocalLoginWrapper(loginInfo);
70
+ }
59
71
  default:
60
72
  throw new Error(`Unsupported wallet mode: ${(loginInfo as any).mode}`);
61
73
  }