@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 +52 -6
- 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/effectstream.ts +3 -1
- package/src/evm/viem.test.ts +37 -0
- package/src/evm/viem.ts +195 -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 +157 -0
- package/src/midnight/local.ts +375 -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
|
@@ -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,157 @@
|
|
|
1
|
+
import { describe, expect, mock, 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("facade mode: networkUrls triggers buildWalletFacade and exposes the full WalletFacade on the api", async () => {
|
|
59
|
+
// Sentinel objects so we can assert the facade-path code actually wired
|
|
60
|
+
// through whatever buildWalletFacade returned.
|
|
61
|
+
const fakeFacade = { __kind: "WalletFacade", shielded: {}, dust: {}, unshielded: {} };
|
|
62
|
+
const fakeShielded = {
|
|
63
|
+
address: {
|
|
64
|
+
coinPublicKeyString: () => "deadbeef-coin-pub-key",
|
|
65
|
+
encryptionPublicKeyString: () => "deadbeef-enc-pub-key",
|
|
66
|
+
},
|
|
67
|
+
balances: {},
|
|
68
|
+
};
|
|
69
|
+
const buildSpy = mock(async (
|
|
70
|
+
networkUrls: unknown,
|
|
71
|
+
seed: string,
|
|
72
|
+
_networkId: unknown,
|
|
73
|
+
_syncMode: unknown,
|
|
74
|
+
) => ({
|
|
75
|
+
wallet: fakeFacade,
|
|
76
|
+
zswapSecretKeys: {} as never,
|
|
77
|
+
walletZswapSecretKeys: {} as never,
|
|
78
|
+
dustSecretKey: {} as never,
|
|
79
|
+
walletDustSecretKey: {} as never,
|
|
80
|
+
dustAddress: `dust-for-${seed.slice(0, 8)}`,
|
|
81
|
+
unshieldedAddress: `unshielded-for-${seed.slice(0, 8)}`,
|
|
82
|
+
unshieldedKeystore: {} as never,
|
|
83
|
+
__echoedNetworkUrls: networkUrls,
|
|
84
|
+
}));
|
|
85
|
+
const getInitialSpy = mock(async (_shielded: unknown) => fakeShielded);
|
|
86
|
+
|
|
87
|
+
mock.module("@effectstream/midnight-contracts/wallet-info", () => ({
|
|
88
|
+
buildWalletFacade: buildSpy,
|
|
89
|
+
getInitialShieldedState: getInitialSpy,
|
|
90
|
+
}));
|
|
91
|
+
|
|
92
|
+
const provider = await MidnightLocalConnector.instance().connectFromSeed({
|
|
93
|
+
seed: DETERMINISTIC_SEED,
|
|
94
|
+
networkId: "undeployed",
|
|
95
|
+
networkUrls: {
|
|
96
|
+
indexer: "http://127.0.0.1:8088/api/v3/graphql",
|
|
97
|
+
indexerWS: "ws://127.0.0.1:8088/api/v3/graphql/ws",
|
|
98
|
+
node: "http://127.0.0.1:9944",
|
|
99
|
+
proofServer: "http://127.0.0.1:6300",
|
|
100
|
+
},
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
expect(buildSpy).toHaveBeenCalledTimes(1);
|
|
104
|
+
const [networkUrlsArg, seedArg, networkIdArg, syncModeArg] = buildSpy.mock.calls[0]!;
|
|
105
|
+
expect(seedArg).toBe(DETERMINISTIC_SEED);
|
|
106
|
+
expect(networkIdArg).toBe("undeployed");
|
|
107
|
+
expect(syncModeArg).toBe("all");
|
|
108
|
+
expect(networkUrlsArg).toMatchObject({
|
|
109
|
+
indexer: "http://127.0.0.1:8088/api/v3/graphql",
|
|
110
|
+
indexerWS: "ws://127.0.0.1:8088/api/v3/graphql/ws",
|
|
111
|
+
node: "http://127.0.0.1:9944",
|
|
112
|
+
proofServer: "http://127.0.0.1:6300",
|
|
113
|
+
id: "undeployed",
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
const api = provider.getConnection().api as unknown as MidnightLocalApi;
|
|
117
|
+
expect(api.walletFacade).toBe(fakeFacade);
|
|
118
|
+
expect(api.dustAddress).toBe(`dust-for-${DETERMINISTIC_SEED.slice(0, 8)}`);
|
|
119
|
+
expect(api.shieldedAddress).toBe("deadbeef-coin-pub-key");
|
|
120
|
+
|
|
121
|
+
// getShieldedAddresses no longer throws in facade mode.
|
|
122
|
+
await expect(api.getShieldedAddresses()).resolves.toEqual({
|
|
123
|
+
shieldedAddress: "deadbeef-coin-pub-key",
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
expect(provider.getConnection().metadata.displayName).toBe(
|
|
127
|
+
"Midnight (local seed, facade)",
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
mock.restore();
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test("signMessage round-trips through CryptoManager.Midnight().verifySignature", async () => {
|
|
134
|
+
const provider = await MidnightLocalConnector.instance().connectFromSeed({
|
|
135
|
+
seed: DETERMINISTIC_SEED,
|
|
136
|
+
networkId: "undeployed",
|
|
137
|
+
});
|
|
138
|
+
const message = "hello effectstream from @effectstream/crypto";
|
|
139
|
+
const sig = await provider.signMessage(message);
|
|
140
|
+
|
|
141
|
+
const midnightCrypto = CryptoManager.getCryptoManager(AddressType.MIDNIGHT);
|
|
142
|
+
const ok = await midnightCrypto.verifySignature(
|
|
143
|
+
provider.getAddress().address,
|
|
144
|
+
message,
|
|
145
|
+
sig,
|
|
146
|
+
);
|
|
147
|
+
expect(ok).toBe(true);
|
|
148
|
+
|
|
149
|
+
// Tampering with the message must fail verification.
|
|
150
|
+
const tampered = await midnightCrypto.verifySignature(
|
|
151
|
+
provider.getAddress().address,
|
|
152
|
+
`${message} tampered`,
|
|
153
|
+
sig,
|
|
154
|
+
);
|
|
155
|
+
expect(tampered).toBe(false);
|
|
156
|
+
});
|
|
157
|
+
});
|
|
@@ -0,0 +1,375 @@
|
|
|
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
|
+
/**
|
|
12
|
+
* Network endpoints required to bring up the full WalletFacade. Mirrors
|
|
13
|
+
* `NetworkUrls` from `@effectstream/midnight-contracts` but with every field
|
|
14
|
+
* required (so consumers can't accidentally leave one unset).
|
|
15
|
+
*/
|
|
16
|
+
export type MidnightLocalNetworkUrls = {
|
|
17
|
+
/** GraphQL indexer HTTP endpoint. */
|
|
18
|
+
indexer: string;
|
|
19
|
+
/** GraphQL indexer WebSocket endpoint. */
|
|
20
|
+
indexerWS: string;
|
|
21
|
+
/** Midnight node RPC endpoint (used as the wallet relay; http→ws is rewritten internally). */
|
|
22
|
+
node: string;
|
|
23
|
+
/** Proof-server HTTP endpoint. */
|
|
24
|
+
proofServer: string;
|
|
25
|
+
/** Optional network id override; defaults to `connectFromSeed`'s `networkId` arg. */
|
|
26
|
+
id?: string;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export type MidnightLocalSyncMode = "all" | "dust-only";
|
|
30
|
+
|
|
31
|
+
export type MidnightLocalConnectArgs = {
|
|
32
|
+
/** 64-character hex seed. Generated when omitted. */
|
|
33
|
+
seed?: string;
|
|
34
|
+
/** Midnight network id — sets the bech32 prefix on the unshielded address. */
|
|
35
|
+
networkId: string;
|
|
36
|
+
/**
|
|
37
|
+
* Network URLs for the full WalletFacade. When supplied, the connector builds
|
|
38
|
+
* a complete wallet (shielded + dust + unshielded) via
|
|
39
|
+
* `@effectstream/midnight-contracts.buildWalletFacade` and exposes it on
|
|
40
|
+
* `getConnection().api.walletFacade`. Required for shielded tx submission,
|
|
41
|
+
* `balanceAndProveTransaction`, dust fee payment, etc.
|
|
42
|
+
*
|
|
43
|
+
* When omitted, the connector returns the signing-only path (unshielded
|
|
44
|
+
* keystore `signData`) — useful for offline signature flows or signature
|
|
45
|
+
* verification dry runs that don't need to stand up indexer + proof-server.
|
|
46
|
+
*/
|
|
47
|
+
networkUrls?: MidnightLocalNetworkUrls;
|
|
48
|
+
/** Defaults to "all" when `networkUrls` is supplied. Ignored otherwise. */
|
|
49
|
+
syncMode?: MidnightLocalSyncMode;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Shape exposed on `getConnection().api`. Mirrors a subset of `ConnectedAPI`
|
|
54
|
+
* that the rest of Effectstream's Midnight code consumes:
|
|
55
|
+
* - `signData` mirrors the dapp-connector-api shape (works in both modes).
|
|
56
|
+
* - `getShieldedAddresses` returns the real shielded coin public key when
|
|
57
|
+
* the full facade is built; throws in signing-only mode.
|
|
58
|
+
* - `walletFacade` / `dustAddress` / `shieldedAddress` are populated only
|
|
59
|
+
* when `networkUrls` was supplied at connect time.
|
|
60
|
+
*/
|
|
61
|
+
export type MidnightLocalApi = {
|
|
62
|
+
signData: (
|
|
63
|
+
data: string,
|
|
64
|
+
options: { encoding: "text" | "hex"; keyType: "unshielded" },
|
|
65
|
+
) => Promise<{ data: string; signature: string; verifyingKey: string }>;
|
|
66
|
+
getShieldedAddresses: () => Promise<{ shieldedAddress: string }>;
|
|
67
|
+
/** The derived seed, exposed so tests can persist/reuse the wallet. */
|
|
68
|
+
seed: string;
|
|
69
|
+
/** Public verifying key for the unshielded signing role (hex). */
|
|
70
|
+
verifyingKey: string;
|
|
71
|
+
/** Bech32-encoded unshielded address. */
|
|
72
|
+
unshieldedAddress: string;
|
|
73
|
+
/**
|
|
74
|
+
* Full Midnight `WalletFacade`. Populated only when `networkUrls` was passed
|
|
75
|
+
* to `connectFromSeed`. Cast to
|
|
76
|
+
* `import("@midnight-ntwrk/wallet-sdk-facade").WalletFacade` for typed
|
|
77
|
+
* access. Carries `submitTransaction`, `balanceFinalizedTransaction`,
|
|
78
|
+
* `balanceUnboundTransaction`, `balanceUnprovenTransaction`, `signRecipe`,
|
|
79
|
+
* `signUnprovenTransaction`, `state()`, `waitForSyncedState()`, etc.
|
|
80
|
+
*/
|
|
81
|
+
walletFacade?: unknown;
|
|
82
|
+
/** Bech32-encoded dust address. Populated only in facade mode. */
|
|
83
|
+
dustAddress?: string;
|
|
84
|
+
/** Hex-encoded shielded coin public key. Populated only in facade mode. */
|
|
85
|
+
shieldedAddress?: string;
|
|
86
|
+
/**
|
|
87
|
+
* Escape hatch: the entire `WalletResult` returned by
|
|
88
|
+
* `@effectstream/midnight-contracts.buildWalletFacade`. Exposes the raw
|
|
89
|
+
* secret keys (`zswapSecretKeys`, `dustSecretKey`, `unshieldedKeystore`) that
|
|
90
|
+
* advanced flows (manual tx balancing, custom dust handling) need. Cast to
|
|
91
|
+
* `import("@effectstream/midnight-contracts").WalletResult`.
|
|
92
|
+
* Populated only in facade mode.
|
|
93
|
+
*/
|
|
94
|
+
walletResult?: unknown;
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
export class MidnightLocalConnector {
|
|
98
|
+
private provider: MidnightLocalProvider | undefined;
|
|
99
|
+
private static INSTANCE: undefined | MidnightLocalConnector = undefined;
|
|
100
|
+
|
|
101
|
+
static instance(): MidnightLocalConnector {
|
|
102
|
+
if (MidnightLocalConnector.INSTANCE == null) {
|
|
103
|
+
MidnightLocalConnector.INSTANCE = new MidnightLocalConnector();
|
|
104
|
+
}
|
|
105
|
+
return MidnightLocalConnector.INSTANCE;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Build a Midnight wallet identity from a hex seed.
|
|
110
|
+
*
|
|
111
|
+
* Two modes, picked by whether `args.networkUrls` is supplied:
|
|
112
|
+
*
|
|
113
|
+
* - **Facade mode** (`networkUrls` set): builds the full Midnight wallet via
|
|
114
|
+
* `@effectstream/midnight-contracts.buildWalletFacade` — shielded + dust +
|
|
115
|
+
* unshielded sub-wallets, connected to a live indexer + node + proof
|
|
116
|
+
* server. The complete `WalletFacade` is exposed on
|
|
117
|
+
* `getConnection().api.walletFacade`, enabling `submitTransaction`,
|
|
118
|
+
* `balanceAndProveTransaction`, dust fee payment, etc. Requires
|
|
119
|
+
* `@effectstream/midnight-contracts` to be installed (optional peer dep).
|
|
120
|
+
*
|
|
121
|
+
* - **Signing-only mode** (`networkUrls` omitted): derives the unshielded
|
|
122
|
+
* keystore from the seed and exposes a CIP-30-shaped `signData` backed by
|
|
123
|
+
* `unshieldedKeystore.signData(...)`. No network IO. Suitable for offline
|
|
124
|
+
* signature flows and signature-verification dry runs.
|
|
125
|
+
*/
|
|
126
|
+
connectFromSeed = async (
|
|
127
|
+
args: MidnightLocalConnectArgs,
|
|
128
|
+
): Promise<MidnightLocalProvider> => {
|
|
129
|
+
if (args.networkUrls != null) {
|
|
130
|
+
return await this.connectWithFacade(args, args.networkUrls);
|
|
131
|
+
}
|
|
132
|
+
return await this.connectSigningOnly(args);
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
private connectSigningOnly = async (
|
|
136
|
+
args: MidnightLocalConnectArgs,
|
|
137
|
+
): Promise<MidnightLocalProvider> => {
|
|
138
|
+
const { hdMod, keystoreMod } = await loadKeystoreDeps();
|
|
139
|
+
|
|
140
|
+
const seed = args.seed ?? generateRandomHexSeed(32);
|
|
141
|
+
const unshieldedSeed = deriveUnshieldedSeed(
|
|
142
|
+
hdMod,
|
|
143
|
+
seed,
|
|
144
|
+
hdMod.Roles.NightExternal,
|
|
145
|
+
);
|
|
146
|
+
const unshieldedKeystore = keystoreMod.createKeystore(
|
|
147
|
+
unshieldedSeed,
|
|
148
|
+
args.networkId as never,
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
const unshieldedAddress = unshieldedKeystore.getBech32Address().asString();
|
|
152
|
+
const verifyingKey = String(unshieldedKeystore.getPublicKey());
|
|
153
|
+
|
|
154
|
+
const api: MidnightLocalApi = {
|
|
155
|
+
signData: async (data, options) => {
|
|
156
|
+
const bytes =
|
|
157
|
+
options.encoding === "hex"
|
|
158
|
+
? Buffer.from(data, "hex")
|
|
159
|
+
: Buffer.from(data, "utf-8");
|
|
160
|
+
const signature = unshieldedKeystore.signData(bytes);
|
|
161
|
+
return {
|
|
162
|
+
data,
|
|
163
|
+
signature: String(signature),
|
|
164
|
+
verifyingKey,
|
|
165
|
+
};
|
|
166
|
+
},
|
|
167
|
+
getShieldedAddresses: async () => {
|
|
168
|
+
throw new Error(
|
|
169
|
+
"MidnightLocal signing-only mode does not expose a shielded address. Pass `networkUrls` to connectFromSeed to build the full WalletFacade.",
|
|
170
|
+
);
|
|
171
|
+
},
|
|
172
|
+
seed,
|
|
173
|
+
verifyingKey,
|
|
174
|
+
unshieldedAddress,
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
const conn: ActiveConnection<MidnightApi> = {
|
|
178
|
+
api: api as unknown as MidnightApi,
|
|
179
|
+
metadata: {
|
|
180
|
+
name: "midnight-local",
|
|
181
|
+
displayName: "Midnight (local seed)",
|
|
182
|
+
},
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
this.provider = new MidnightLocalProvider(
|
|
186
|
+
conn,
|
|
187
|
+
unshieldedAddress as MidnightAddress,
|
|
188
|
+
unshieldedKeystore,
|
|
189
|
+
);
|
|
190
|
+
return this.provider;
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
private connectWithFacade = async (
|
|
194
|
+
args: MidnightLocalConnectArgs,
|
|
195
|
+
networkUrls: MidnightLocalNetworkUrls,
|
|
196
|
+
): Promise<MidnightLocalProvider> => {
|
|
197
|
+
// Use the `/wallet-info` subpath, not the package barrel: the barrel
|
|
198
|
+
// re-exports `deploy.ts` (`node:fs/promises`), which breaks browser bundles
|
|
199
|
+
// of `@effectstream/wallets`.
|
|
200
|
+
const contractsMod = await import(
|
|
201
|
+
"@effectstream/midnight-contracts/wallet-info"
|
|
202
|
+
).catch(() => {
|
|
203
|
+
throw new Error(
|
|
204
|
+
"@effectstream/midnight-contracts is required when `networkUrls` is passed to MidnightLocal.connectFromSeed. Install it as a peer dependency.",
|
|
205
|
+
);
|
|
206
|
+
});
|
|
207
|
+
const { hdMod, keystoreMod } = await loadKeystoreDeps();
|
|
208
|
+
|
|
209
|
+
const seed = args.seed ?? generateRandomHexSeed(32);
|
|
210
|
+
const unshieldedSeed = deriveUnshieldedSeed(
|
|
211
|
+
hdMod,
|
|
212
|
+
seed,
|
|
213
|
+
hdMod.Roles.NightExternal,
|
|
214
|
+
);
|
|
215
|
+
const unshieldedKeystore = keystoreMod.createKeystore(
|
|
216
|
+
unshieldedSeed,
|
|
217
|
+
args.networkId as never,
|
|
218
|
+
);
|
|
219
|
+
const verifyingKey = String(unshieldedKeystore.getPublicKey());
|
|
220
|
+
|
|
221
|
+
const fullNetworkUrls = {
|
|
222
|
+
indexer: networkUrls.indexer,
|
|
223
|
+
indexerWS: networkUrls.indexerWS,
|
|
224
|
+
node: networkUrls.node,
|
|
225
|
+
proofServer: networkUrls.proofServer,
|
|
226
|
+
id: networkUrls.id ?? args.networkId,
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
const walletResult = await contractsMod.buildWalletFacade(
|
|
230
|
+
fullNetworkUrls,
|
|
231
|
+
seed,
|
|
232
|
+
args.networkId as never,
|
|
233
|
+
args.syncMode ?? "all",
|
|
234
|
+
);
|
|
235
|
+
|
|
236
|
+
const initialShielded = await contractsMod.getInitialShieldedState(
|
|
237
|
+
walletResult.wallet.shielded,
|
|
238
|
+
);
|
|
239
|
+
const shieldedAddress = initialShielded.address.coinPublicKeyString();
|
|
240
|
+
|
|
241
|
+
const api: MidnightLocalApi = {
|
|
242
|
+
signData: async (data, options) => {
|
|
243
|
+
const bytes =
|
|
244
|
+
options.encoding === "hex"
|
|
245
|
+
? Buffer.from(data, "hex")
|
|
246
|
+
: Buffer.from(data, "utf-8");
|
|
247
|
+
const signature = unshieldedKeystore.signData(bytes);
|
|
248
|
+
return {
|
|
249
|
+
data,
|
|
250
|
+
signature: String(signature),
|
|
251
|
+
verifyingKey,
|
|
252
|
+
};
|
|
253
|
+
},
|
|
254
|
+
getShieldedAddresses: async () => ({ shieldedAddress }),
|
|
255
|
+
seed,
|
|
256
|
+
verifyingKey,
|
|
257
|
+
unshieldedAddress: walletResult.unshieldedAddress,
|
|
258
|
+
walletFacade: walletResult.wallet,
|
|
259
|
+
dustAddress: walletResult.dustAddress,
|
|
260
|
+
shieldedAddress,
|
|
261
|
+
walletResult,
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
const conn: ActiveConnection<MidnightApi> = {
|
|
265
|
+
api: api as unknown as MidnightApi,
|
|
266
|
+
metadata: {
|
|
267
|
+
name: "midnight-local",
|
|
268
|
+
displayName: "Midnight (local seed, facade)",
|
|
269
|
+
},
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
this.provider = new MidnightLocalProvider(
|
|
273
|
+
conn,
|
|
274
|
+
walletResult.unshieldedAddress as MidnightAddress,
|
|
275
|
+
unshieldedKeystore,
|
|
276
|
+
);
|
|
277
|
+
return this.provider;
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
getProvider = (): undefined | MidnightLocalProvider => {
|
|
281
|
+
return this.provider;
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
getOrThrowProvider = (): MidnightLocalProvider => {
|
|
285
|
+
if (this.provider == null) {
|
|
286
|
+
throw new Error("MidnightLocalConnector not initialized yet");
|
|
287
|
+
}
|
|
288
|
+
return this.provider;
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
isConnected = (): boolean => {
|
|
292
|
+
return this.provider != null;
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
export class MidnightLocalProvider implements IProvider<MidnightApi> {
|
|
297
|
+
constructor(
|
|
298
|
+
private readonly conn: ActiveConnection<MidnightApi>,
|
|
299
|
+
readonly address: MidnightAddress,
|
|
300
|
+
private readonly unshieldedKeystore: unknown,
|
|
301
|
+
) {}
|
|
302
|
+
|
|
303
|
+
getConnection = (): ActiveConnection<MidnightApi> => {
|
|
304
|
+
return this.conn;
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
getAddress = (): AddressAndType => {
|
|
308
|
+
return {
|
|
309
|
+
type: AddressType.MIDNIGHT,
|
|
310
|
+
address: this.address,
|
|
311
|
+
};
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
signMessage = async (message: string): Promise<UserSignature> => {
|
|
315
|
+
const signed = await (this.conn.api as unknown as MidnightLocalApi).signData(
|
|
316
|
+
message,
|
|
317
|
+
{ encoding: "text", keyType: "unshielded" },
|
|
318
|
+
);
|
|
319
|
+
// Format: "signature|verifyingKey". Midnight signatures are not
|
|
320
|
+
// self-recovering (unlike EVM ECDSA), so the verifier needs the public
|
|
321
|
+
// key to call ledger-v8.verifySignature. Mirrors Cardano's "sig+key"
|
|
322
|
+
// convention, just with `|` as the separator to keep the formats distinct.
|
|
323
|
+
return `${signed.signature}|${signed.verifyingKey}`;
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
/** Expose the keystore for callers that need the raw signing primitive. */
|
|
327
|
+
getUnshieldedKeystore = (): unknown => this.unshieldedKeystore;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
async function loadKeystoreDeps(): Promise<{
|
|
331
|
+
hdMod: typeof import("@midnight-ntwrk/wallet-sdk-hd");
|
|
332
|
+
keystoreMod: typeof import("@midnight-ntwrk/wallet-sdk-unshielded-wallet");
|
|
333
|
+
}> {
|
|
334
|
+
const hdMod = await import("@midnight-ntwrk/wallet-sdk-hd").catch(() => {
|
|
335
|
+
throw new Error(
|
|
336
|
+
"@midnight-ntwrk/wallet-sdk-hd is required for WalletMode.MidnightLocal.",
|
|
337
|
+
);
|
|
338
|
+
});
|
|
339
|
+
const keystoreMod = await import(
|
|
340
|
+
"@midnight-ntwrk/wallet-sdk-unshielded-wallet"
|
|
341
|
+
).catch(() => {
|
|
342
|
+
throw new Error(
|
|
343
|
+
"@midnight-ntwrk/wallet-sdk-unshielded-wallet is required for WalletMode.MidnightLocal.",
|
|
344
|
+
);
|
|
345
|
+
});
|
|
346
|
+
return { hdMod, keystoreMod };
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function deriveUnshieldedSeed(
|
|
350
|
+
hdMod: typeof import("@midnight-ntwrk/wallet-sdk-hd"),
|
|
351
|
+
seedHex: string,
|
|
352
|
+
role: import("@midnight-ntwrk/wallet-sdk-hd").Role,
|
|
353
|
+
): Uint8Array {
|
|
354
|
+
const seedBuffer = Buffer.from(seedHex, "hex");
|
|
355
|
+
const hdResult = hdMod.HDWallet.fromSeed(seedBuffer);
|
|
356
|
+
if (hdResult.type !== "seedOk") {
|
|
357
|
+
throw new Error(`HDWallet.fromSeed failed: ${hdResult.type}`);
|
|
358
|
+
}
|
|
359
|
+
const derivation = hdResult.hdWallet
|
|
360
|
+
.selectAccount(0)
|
|
361
|
+
.selectRole(role)
|
|
362
|
+
.deriveKeyAt(0);
|
|
363
|
+
if (derivation.type !== "keyDerived") {
|
|
364
|
+
throw new Error(`HDWallet.deriveKeyAt failed: ${derivation.type}`);
|
|
365
|
+
}
|
|
366
|
+
return Buffer.from(derivation.key);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function generateRandomHexSeed(byteLength: number): string {
|
|
370
|
+
const bytes = new Uint8Array(byteLength);
|
|
371
|
+
crypto.getRandomValues(bytes);
|
|
372
|
+
return Array.from(bytes)
|
|
373
|
+
.map((b) => b.toString(16).padStart(2, "0"))
|
|
374
|
+
.join("");
|
|
375
|
+
}
|