@effectstream/wallets 0.100.17 → 0.100.19

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@effectstream/wallets",
3
- "version": "0.100.17",
3
+ "version": "0.100.19",
4
4
  "license": "MIT",
5
5
  "homepage": "https://effectstream.github.io/docs/",
6
6
  "repository": {
@@ -11,12 +11,58 @@
11
11
  "bugs": {
12
12
  "url": "https://github.com/effectstream/effectstream/issues"
13
13
  },
14
- "exports": "./src/mod.ts",
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
+ "@effectstream/midnight-contracts": "0.100.19",
22
+ "@lucid-evolution/lucid": "^0.4.29",
23
+ "@lucid-evolution/provider": "^0.1.90",
24
+ "@lucid-evolution/utils": "^0.1.66",
25
+ "@midnight-ntwrk/ledger-v8": "^8.0.3",
26
+ "@midnight-ntwrk/wallet-sdk-hd": "^3.0.1",
27
+ "@midnight-ntwrk/wallet-sdk-unshielded-wallet": "^2.1.0"
28
+ },
29
+ "peerDependenciesMeta": {
30
+ "@effectstream/midnight-contracts": {
31
+ "optional": true
32
+ },
33
+ "@lucid-evolution/lucid": {
34
+ "optional": true
35
+ },
36
+ "@lucid-evolution/provider": {
37
+ "optional": true
38
+ },
39
+ "@lucid-evolution/utils": {
40
+ "optional": true
41
+ },
42
+ "@midnight-ntwrk/ledger-v8": {
43
+ "optional": true
44
+ },
45
+ "@midnight-ntwrk/wallet-sdk-hd": {
46
+ "optional": true
47
+ },
48
+ "@midnight-ntwrk/wallet-sdk-unshielded-wallet": {
49
+ "optional": true
50
+ }
51
+ },
52
+ "devDependencies": {
53
+ "@effectstream/midnight-contracts": "0.100.19",
54
+ "@lucid-evolution/lucid": "^0.4.30",
55
+ "@lucid-evolution/provider": "^0.1.91",
56
+ "@lucid-evolution/utils": "^0.1.66",
57
+ "@midnight-ntwrk/ledger-v8": "8.0.3",
58
+ "@midnight-ntwrk/wallet-sdk-hd": "3.0.1",
59
+ "@midnight-ntwrk/wallet-sdk-unshielded-wallet": "2.1.0"
60
+ },
15
61
  "dependencies": {
16
- "@effectstream/concise": "0.100.17",
17
- "@effectstream/crypto": "0.100.17",
18
- "@effectstream/event-client": "0.100.17",
19
- "@effectstream/utils": "0.100.17",
62
+ "@effectstream/concise": "0.100.19",
63
+ "@effectstream/crypto": "0.100.19",
64
+ "@effectstream/event-client": "0.100.19",
65
+ "@effectstream/utils": "0.100.19",
20
66
  "@aurowallet/mina-provider": "^1.0.12",
21
67
  "@metamask/providers": "^22.1.0",
22
68
  "@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
+ }
@@ -5,6 +5,7 @@ import { type Chain, numberToHex } from "viem";
5
5
  import { utf8ToHex } from "web3-utils";
6
6
  import type { EthersEvmProvider } from "./evm/ethers.ts";
7
7
  import type { EvmInjectedProvider } from "./evm/injected.ts";
8
+ import type { ViemEvmProvider } from "./evm/viem.ts";
8
9
  import type { AbiItem } from "web3-utils";
9
10
  import { type TransactionReceipt, Web3 } from "web3";
10
11
  import { createMessageForBatcher } from "@effectstream/concise";
@@ -189,7 +190,8 @@ export async function sendSelfSequencedTransaction(
189
190
 
190
191
  const evmProvider = wallet.provider as
191
192
  | EthersEvmProvider
192
- | EvmInjectedProvider;
193
+ | EvmInjectedProvider
194
+ | ViemEvmProvider;
193
195
 
194
196
  const hexData = utf8ToHex(JSON.stringify(conciseData));
195
197
  const effectstreamL2Contract = await effectstreamConfig.getEffectstreamL2Contract();
@@ -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,195 @@
1
+ import {
2
+ createPublicClient,
3
+ createWalletClient,
4
+ http,
5
+ type Chain,
6
+ type Hex,
7
+ type PrivateKeyAccount,
8
+ type WalletClient,
9
+ } from "viem";
10
+ import { privateKeyToAccount } from "viem/accounts";
11
+ import { AddressType } from "@effectstream/utils";
12
+ import type {
13
+ ActiveConnection,
14
+ AddressAndType,
15
+ IProvider,
16
+ UserSignature,
17
+ } from "../IProvider.ts";
18
+ import type { EvmAddress } from "./types.ts";
19
+
20
+ export type ViemApi = WalletClient;
21
+
22
+ export type ViemConnectArgs = {
23
+ privateKey: Hex;
24
+ rpcUrl: string;
25
+ chain?: Chain;
26
+ };
27
+
28
+ export class ViemConnector {
29
+ private provider: ViemEvmProvider | undefined;
30
+ private static INSTANCE: undefined | ViemConnector = undefined;
31
+
32
+ static instance(): ViemConnector {
33
+ if (ViemConnector.INSTANCE == null) {
34
+ ViemConnector.INSTANCE = new ViemConnector();
35
+ }
36
+ return ViemConnector.INSTANCE;
37
+ }
38
+
39
+ connectFromPrivateKey = async (
40
+ args: ViemConnectArgs,
41
+ ): Promise<ViemEvmProvider> => {
42
+ const account = privateKeyToAccount(args.privateKey);
43
+ const client = createWalletClient({
44
+ account,
45
+ chain: args.chain,
46
+ transport: http(args.rpcUrl),
47
+ });
48
+ this.provider = await ViemEvmProvider.init(client, account);
49
+ return this.provider;
50
+ };
51
+
52
+ connectExternal = async (
53
+ conn: ActiveConnection<ViemApi>,
54
+ ): Promise<ViemEvmProvider> => {
55
+ const account = conn.api.account;
56
+ if (account == null) {
57
+ throw new Error(
58
+ "ViemConnector.connectExternal: WalletClient.account is null. Build the client with createWalletClient({ account, ... }).",
59
+ );
60
+ }
61
+ this.provider = await ViemEvmProvider.init(conn.api, account, conn.metadata);
62
+ return this.provider;
63
+ };
64
+
65
+ getProvider = (): undefined | ViemEvmProvider => {
66
+ return this.provider;
67
+ };
68
+
69
+ getOrThrowProvider = (): ViemEvmProvider => {
70
+ if (this.provider == null) {
71
+ throw new Error("ViemConnector not initialized yet");
72
+ }
73
+ return this.provider;
74
+ };
75
+
76
+ isConnected = (): boolean => {
77
+ return this.provider != null;
78
+ };
79
+ }
80
+
81
+ export class ViemEvmProvider implements IProvider<ViemApi> {
82
+ constructor(
83
+ private readonly conn: ActiveConnection<ViemApi>,
84
+ private readonly account: PrivateKeyAccount | WalletClient["account"],
85
+ readonly address: EvmAddress,
86
+ ) {}
87
+
88
+ static init = async (
89
+ client: ViemApi,
90
+ account: NonNullable<WalletClient["account"]>,
91
+ metadata?: { name: string; displayName: string },
92
+ ): Promise<ViemEvmProvider> => {
93
+ const conn: ActiveConnection<ViemApi> = {
94
+ api: client,
95
+ metadata: metadata ?? {
96
+ name: "viem-local",
97
+ displayName: "Viem (local)",
98
+ },
99
+ };
100
+ return new ViemEvmProvider(conn, account, account.address as EvmAddress);
101
+ };
102
+
103
+ getConnection = (): ActiveConnection<ViemApi> => {
104
+ return this.conn;
105
+ };
106
+
107
+ getAddress = (): AddressAndType => {
108
+ return {
109
+ type: AddressType.EVM,
110
+ address: this.address.toLowerCase(),
111
+ };
112
+ };
113
+
114
+ signMessage = async (message: string): Promise<UserSignature> => {
115
+ if (this.account == null) {
116
+ throw new Error("ViemEvmProvider.signMessage: account is missing");
117
+ }
118
+ return await this.conn.api.signMessage({
119
+ account: this.account,
120
+ message,
121
+ });
122
+ };
123
+
124
+ /**
125
+ * Submit an EIP-1193-shaped transaction request (matching the
126
+ * `EvmInjectedProvider.sendTransaction` shape — all numeric fields are
127
+ * hex strings) by delegating to viem's hoisted `walletClient.sendTransaction`.
128
+ *
129
+ * `account` + `chain` are hoisted on the underlying `WalletClient` at
130
+ * connect time (see `ViemConnector.connectFromPrivateKey`), so we only
131
+ * need to convert the hex-string numeric fields to bigints that viem
132
+ * expects, and pass through `to` / `data` as 0x-strings.
133
+ *
134
+ * Without this method, the high-level `sendTransaction(wallet, …)` helper
135
+ * in `effectstream.ts` rejects with `evmProvider.sendTransaction is not a
136
+ * function` when invoked against a `WalletMode.EvmViem` wallet — blocking
137
+ * any frontend that wants to drive the engine's tx flow via the local-JS
138
+ * wallet (e.g. headless Playwright e2e tests).
139
+ */
140
+ sendTransaction = async (tx: {
141
+ to?: string;
142
+ from: string;
143
+ gas?: string;
144
+ gasPrice?: string;
145
+ data: string;
146
+ value?: string;
147
+ maxPriorityFeePerGas?: string;
148
+ maxFeePerGas?: string;
149
+ }): Promise<{ txHash: string }> => {
150
+ if (this.account == null) {
151
+ throw new Error("ViemEvmProvider.sendTransaction: account is missing");
152
+ }
153
+ const toBigInt = (h?: string): bigint | undefined =>
154
+ h == null ? undefined : BigInt(h);
155
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
156
+ const hash = await (this.conn.api as any).sendTransaction({
157
+ account: this.account,
158
+ to: tx.to as `0x${string}` | undefined,
159
+ data: tx.data as `0x${string}`,
160
+ value: toBigInt(tx.value),
161
+ gas: toBigInt(tx.gas),
162
+ gasPrice: toBigInt(tx.gasPrice),
163
+ maxFeePerGas: toBigInt(tx.maxFeePerGas),
164
+ maxPriorityFeePerGas: toBigInt(tx.maxPriorityFeePerGas),
165
+ });
166
+ if (typeof hash !== "string") {
167
+ throw new Error(
168
+ `[ViemEvmProvider.sendTransaction] expected string tx hash, got ${typeof hash}`,
169
+ );
170
+ }
171
+
172
+ // Wait for the receipt + verify status. viem's sendTransaction returns
173
+ // the hash as soon as the tx is broadcast, NOT once it has mined. If we
174
+ // returned the hash without checking the receipt, a reverted tx would
175
+ // look "successful" to the engine's `wait-receipt` confirmation path,
176
+ // and the indexer would never see a matching event because the contract
177
+ // call reverted on chain. Match the EvmInjected / EthersEvmProvider
178
+ // contract: only return when the tx is mined AND its status is 0x1.
179
+ const chain = (this.conn.api as WalletClient).chain;
180
+ const transport = (this.conn.api as WalletClient).transport;
181
+ const publicClient = createPublicClient({
182
+ chain: chain ?? undefined,
183
+ transport: http((transport as { url?: string })?.url),
184
+ });
185
+ const receipt = await publicClient.waitForTransactionReceipt({
186
+ hash: hash as `0x${string}`,
187
+ });
188
+ if (receipt.status !== "success") {
189
+ throw new Error(
190
+ `[ViemEvmProvider.sendTransaction] tx ${hash} reverted on chain (status=${receipt.status})`,
191
+ );
192
+ }
193
+ return { txHash: hash };
194
+ };
195
+ }
@@ -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"));