@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 +3 -3
- package/package.json +49 -8
- package/src/avail/wrapper.ts +2 -1
- package/src/cardano/local.test.ts +31 -0
- package/src/cardano/local.ts +159 -0
- package/src/cardano/wrapper-local.ts +31 -0
- package/src/evm/viem.test.ts +37 -0
- package/src/evm/viem.ts +122 -0
- package/src/evm/wrapper-ethers.ts +2 -1
- package/src/evm/wrapper-viem.ts +31 -0
- package/src/helpers/format-error.ts +31 -0
- package/src/helpers/local-icons.ts +35 -0
- package/src/helpers/pick-mode.ts +89 -0
- package/src/midnight/local.test.ts +82 -0
- package/src/midnight/local.ts +205 -0
- package/src/midnight/midnight.ts +57 -11
- package/src/midnight/wrapper-local.ts +30 -0
- package/src/mod.ts +19 -0
- package/src/utils.ts +17 -2
- package/src/wallet-modes.ts +33 -1
- package/src/wallets.ts +12 -0
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.
|
|
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)`
|
|
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(...)`
|
|
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.
|
|
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/
|
|
8
|
+
"url": "https://github.com/effectstream/effectstream",
|
|
9
9
|
"directory": "packages/effectstream-sdk/wallets"
|
|
10
10
|
},
|
|
11
11
|
"bugs": {
|
|
12
|
-
"url": "https://github.com/
|
|
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.
|
|
17
|
-
"@effectstream/crypto": "0.100.
|
|
18
|
-
"@effectstream/event-client": "0.100.
|
|
19
|
-
"@effectstream/utils": "0.100.
|
|
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",
|
package/src/avail/wrapper.ts
CHANGED
|
@@ -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:
|
|
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
|
+
});
|
package/src/evm/viem.ts
ADDED
|
@@ -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:
|
|
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
|
+
}
|
package/src/midnight/midnight.ts
CHANGED
|
@@ -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
|
-
|
|
136
|
-
//
|
|
137
|
-
//
|
|
138
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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
|
-
|
|
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;
|
package/src/wallet-modes.ts
CHANGED
|
@@ -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:
|
|
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
|
}
|