@multiplatform.one/web3 7.25.0 → 7.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/core/family.cjs +88 -0
- package/dist/cjs/core/family.native.js +115 -0
- package/dist/cjs/core/family.native.js.map +1 -0
- package/dist/cjs/core/index.cjs +41 -0
- package/dist/cjs/core/index.native.js +44 -0
- package/dist/cjs/core/index.native.js.map +1 -0
- package/dist/cjs/core/message.cjs +89 -0
- package/dist/cjs/core/message.native.js +104 -0
- package/dist/cjs/core/message.native.js.map +1 -0
- package/dist/cjs/core/names.cjs +51 -0
- package/dist/cjs/core/names.native.js +95 -0
- package/dist/cjs/core/names.native.js.map +1 -0
- package/dist/cjs/core/react.cjs +48 -0
- package/dist/cjs/core/react.native.js +52 -0
- package/dist/cjs/core/react.native.js.map +1 -0
- package/dist/cjs/core/store.cjs +450 -0
- package/dist/cjs/core/store.native.js +637 -0
- package/dist/cjs/core/store.native.js.map +1 -0
- package/dist/cjs/core/types.cjs +18 -0
- package/dist/cjs/core/types.native.js +21 -0
- package/dist/cjs/core/types.native.js.map +1 -0
- package/dist/cjs/index.cjs +2 -1
- package/dist/cjs/index.native.js +1 -0
- package/dist/cjs/index.native.js.map +1 -1
- package/dist/esm/core/family.mjs +62 -0
- package/dist/esm/core/family.mjs.map +1 -0
- package/dist/esm/core/family.native.js +86 -0
- package/dist/esm/core/family.native.js.map +1 -0
- package/dist/esm/core/index.mjs +7 -0
- package/dist/esm/core/index.mjs.map +1 -0
- package/dist/esm/core/index.native.js +7 -0
- package/dist/esm/core/index.native.js.map +1 -0
- package/dist/esm/core/message.mjs +63 -0
- package/dist/esm/core/message.mjs.map +1 -0
- package/dist/esm/core/message.native.js +75 -0
- package/dist/esm/core/message.native.js.map +1 -0
- package/dist/esm/core/names.mjs +25 -0
- package/dist/esm/core/names.mjs.map +1 -0
- package/dist/esm/core/names.native.js +66 -0
- package/dist/esm/core/names.native.js.map +1 -0
- package/dist/esm/core/react.mjs +21 -0
- package/dist/esm/core/react.mjs.map +1 -0
- package/dist/esm/core/react.native.js +22 -0
- package/dist/esm/core/react.native.js.map +1 -0
- package/dist/esm/core/store.mjs +424 -0
- package/dist/esm/core/store.mjs.map +1 -0
- package/dist/esm/core/store.native.js +608 -0
- package/dist/esm/core/store.native.js.map +1 -0
- package/dist/esm/core/types.mjs +2 -0
- package/dist/esm/core/types.mjs.map +1 -0
- package/dist/esm/core/types.native.js +2 -0
- package/dist/esm/core/types.native.js.map +1 -0
- package/dist/esm/index.mjs +1 -0
- package/dist/esm/index.mjs.map +1 -1
- package/dist/esm/index.native.js +1 -0
- package/dist/esm/index.native.js.map +1 -1
- package/dist/jsx/core/family.mjs +62 -0
- package/dist/jsx/core/family.mjs.map +1 -0
- package/dist/jsx/core/family.native.js +115 -0
- package/dist/jsx/core/family.native.js.map +1 -0
- package/dist/jsx/core/index.mjs +7 -0
- package/dist/jsx/core/index.mjs.map +1 -0
- package/dist/jsx/core/index.native.js +44 -0
- package/dist/jsx/core/index.native.js.map +1 -0
- package/dist/jsx/core/message.mjs +63 -0
- package/dist/jsx/core/message.mjs.map +1 -0
- package/dist/jsx/core/message.native.js +104 -0
- package/dist/jsx/core/message.native.js.map +1 -0
- package/dist/jsx/core/names.mjs +25 -0
- package/dist/jsx/core/names.mjs.map +1 -0
- package/dist/jsx/core/names.native.js +95 -0
- package/dist/jsx/core/names.native.js.map +1 -0
- package/dist/jsx/core/react.mjs +21 -0
- package/dist/jsx/core/react.mjs.map +1 -0
- package/dist/jsx/core/react.native.js +52 -0
- package/dist/jsx/core/react.native.js.map +1 -0
- package/dist/jsx/core/store.mjs +424 -0
- package/dist/jsx/core/store.mjs.map +1 -0
- package/dist/jsx/core/store.native.js +637 -0
- package/dist/jsx/core/store.native.js.map +1 -0
- package/dist/jsx/core/types.mjs +2 -0
- package/dist/jsx/core/types.mjs.map +1 -0
- package/dist/jsx/core/types.native.js +21 -0
- package/dist/jsx/core/types.native.js.map +1 -0
- package/dist/jsx/index.js +1 -0
- package/dist/jsx/index.js.map +1 -1
- package/dist/jsx/index.mjs +1 -0
- package/dist/jsx/index.mjs.map +1 -1
- package/dist/jsx/index.native.js +1 -0
- package/dist/jsx/index.native.js.map +1 -1
- package/package.json +6 -6
- package/src/core/family.spec.ts +117 -0
- package/src/core/family.ts +103 -0
- package/src/core/imports.spec.ts +82 -0
- package/src/core/index.ts +47 -0
- package/src/core/message.spec.ts +178 -0
- package/src/core/message.ts +119 -0
- package/src/core/names.ts +29 -0
- package/src/core/react.spec.tsx +75 -0
- package/src/core/react.ts +34 -0
- package/src/core/store.spec.ts +890 -0
- package/src/core/store.ts +558 -0
- package/src/core/types.ts +96 -0
- package/src/index.ts +4 -0
- package/types/core/family.d.ts +44 -0
- package/types/core/family.d.ts.map +1 -0
- package/types/core/index.d.ts +7 -0
- package/types/core/index.d.ts.map +1 -0
- package/types/core/message.d.ts +44 -0
- package/types/core/message.d.ts.map +1 -0
- package/types/core/names.d.ts +7 -0
- package/types/core/names.d.ts.map +1 -0
- package/types/core/react.d.ts +14 -0
- package/types/core/react.d.ts.map +1 -0
- package/types/core/store.d.ts +64 -0
- package/types/core/store.d.ts.map +1 -0
- package/types/core/types.d.ts +88 -0
- package/types/core/types.d.ts.map +1 -0
- package/types/index.d.ts +1 -0
- package/types/index.d.ts.map +1 -1
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import type { WalletMessageOptions } from "../message/buildWalletMessage";
|
|
2
|
+
import { nameTable } from "./names";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* How one family's sign-in message differs from the shared envelope. A family
|
|
6
|
+
* package defines its spec; the envelope (domain line, statement, URI, Version,
|
|
7
|
+
* Chain ID, Nonce, Issued At) is the same bytes the Keycloak verifiers parse.
|
|
8
|
+
*/
|
|
9
|
+
export interface WalletMessageSpec {
|
|
10
|
+
/** First-line text after the domain, which the verifiers content-sniff. */
|
|
11
|
+
readonly header: string;
|
|
12
|
+
/** Default statement. Null means none. */
|
|
13
|
+
readonly statement: string | null;
|
|
14
|
+
/** The `Chain ID:` value when the caller gives none. Null omits the line. */
|
|
15
|
+
readonly chainId: string | null;
|
|
16
|
+
/** Whether the message names the signer's address. GPG's identity is its key. Default true. */
|
|
17
|
+
readonly address?: boolean;
|
|
18
|
+
/**
|
|
19
|
+
* Honour a numeric `chainId` option. Only EVM chain ids are numbers; elsewhere
|
|
20
|
+
* a number is a connector's synthetic id and is ignored.
|
|
21
|
+
*/
|
|
22
|
+
readonly numericChainId?: boolean;
|
|
23
|
+
/** EIP-4361 keeps an empty line where an omitted statement would be. */
|
|
24
|
+
readonly keepStatementLine?: boolean;
|
|
25
|
+
/** Canonical address form, such as EIP-55 for EVM. */
|
|
26
|
+
readonly normalizeAddress?: (address: string) => string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface MessageFamily {
|
|
30
|
+
readonly id: string;
|
|
31
|
+
readonly aliases?: readonly string[];
|
|
32
|
+
readonly message: WalletMessageSpec;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface WalletMessageRegistry {
|
|
36
|
+
/** Registered family ids, in registration order. */
|
|
37
|
+
readonly families: readonly string[];
|
|
38
|
+
/** The family id for an id or alias, or undefined. */
|
|
39
|
+
resolve(family: string): string | undefined;
|
|
40
|
+
spec(family: string): WalletMessageSpec | undefined;
|
|
41
|
+
/** Builds the sign-in message. Throws for a family nobody registered. */
|
|
42
|
+
build(family: string, opts: WalletMessageOptions): string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function singleLine(field: string, value: string | null | undefined) {
|
|
46
|
+
if (value && /[\r\n]/.test(value)) {
|
|
47
|
+
throw new Error(`wallet message: ${field} must be a single line`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function pageLocation(): { host: string; origin: string } {
|
|
52
|
+
const location = (globalThis as { location?: { host?: string; origin?: string } }).location;
|
|
53
|
+
return { host: location?.host ?? "", origin: location?.origin ?? "" };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Builds one sign-in message from a family spec. `family` only names the family in errors. */
|
|
57
|
+
export function formatWalletMessage(
|
|
58
|
+
spec: WalletMessageSpec,
|
|
59
|
+
opts: WalletMessageOptions,
|
|
60
|
+
family = "wallet",
|
|
61
|
+
): string {
|
|
62
|
+
const page = pageLocation();
|
|
63
|
+
const domain = opts.domain ?? page.host;
|
|
64
|
+
if (!domain) {
|
|
65
|
+
throw new Error(
|
|
66
|
+
"wallet message: no domain. Pass opts.domain; a file:// or native page has no host.",
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
const pageOrigin = page.origin && page.origin !== "null" ? page.origin : "";
|
|
70
|
+
const uri = opts.uri ?? (pageOrigin || `https://${domain}`);
|
|
71
|
+
const statement = opts.statement === undefined ? spec.statement : opts.statement;
|
|
72
|
+
singleLine("domain", domain);
|
|
73
|
+
singleLine("uri", uri);
|
|
74
|
+
singleLine("statement", statement);
|
|
75
|
+
const lines = [`${domain} ${spec.header}`];
|
|
76
|
+
if (spec.address !== false) {
|
|
77
|
+
if (!opts.address) throw new Error(`wallet message: ${family} needs an address`);
|
|
78
|
+
lines.push(spec.normalizeAddress ? spec.normalizeAddress(opts.address) : opts.address);
|
|
79
|
+
}
|
|
80
|
+
lines.push("");
|
|
81
|
+
if (statement) lines.push(statement, "");
|
|
82
|
+
else if (spec.keepStatementLine) lines.push("");
|
|
83
|
+
lines.push(`URI: ${uri}`, `Version: ${opts.version ?? "1"}`);
|
|
84
|
+
if (spec.chainId !== null) {
|
|
85
|
+
const chainId = spec.numericChainId
|
|
86
|
+
? (opts.chainId ?? spec.chainId)
|
|
87
|
+
: typeof opts.chainId === "string"
|
|
88
|
+
? opts.chainId
|
|
89
|
+
: spec.chainId;
|
|
90
|
+
lines.push(`Chain ID: ${chainId}`);
|
|
91
|
+
}
|
|
92
|
+
lines.push(`Nonce: ${opts.nonce}`, `Issued At: ${opts.issuedAt ?? new Date().toISOString()}`);
|
|
93
|
+
return lines.join("\n");
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** The message half of the family registry: family id or alias to spec. */
|
|
97
|
+
export function createMessageRegistry(families: Iterable<MessageFamily>): WalletMessageRegistry {
|
|
98
|
+
const list = [...families];
|
|
99
|
+
const names = nameTable(list);
|
|
100
|
+
const specs = new Map(list.map((family) => [family.id, family.message]));
|
|
101
|
+
const spec = (family: string) => {
|
|
102
|
+
const id = names.get(family);
|
|
103
|
+
return id === undefined ? undefined : specs.get(id);
|
|
104
|
+
};
|
|
105
|
+
return {
|
|
106
|
+
families: list.map((family) => family.id),
|
|
107
|
+
resolve: (family) => names.get(family),
|
|
108
|
+
spec,
|
|
109
|
+
build(family, opts) {
|
|
110
|
+
const found = spec(family);
|
|
111
|
+
if (found === undefined) {
|
|
112
|
+
throw new Error(
|
|
113
|
+
`no wallet message for family ${family}; registered: ${list.map((entry) => entry.id).join(", ") || "none"}`,
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
return formatWalletMessage(found, opts, names.get(family));
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
const pattern = /^[a-z][a-z0-9-]*$/;
|
|
2
|
+
|
|
3
|
+
export function assertName(kind: string, name: unknown): asserts name is string {
|
|
4
|
+
if (typeof name !== "string" || !pattern.test(name)) {
|
|
5
|
+
throw new Error(`${kind} must be lowercase letters, digits and dashes: ${String(name)}`);
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/** Maps every id and alias to its entry's id, refusing a name two entries claim. */
|
|
10
|
+
export function nameTable(
|
|
11
|
+
entries: Iterable<{ readonly id: string; readonly aliases?: readonly string[] }>,
|
|
12
|
+
): Map<string, string> {
|
|
13
|
+
const table = new Map<string, string>();
|
|
14
|
+
const claim = (name: string, id: string) => {
|
|
15
|
+
const owner = table.get(name);
|
|
16
|
+
if (owner === undefined) table.set(name, id);
|
|
17
|
+
else if (owner !== id) {
|
|
18
|
+
throw new Error(`wallet family name "${name}" is claimed by ${owner} and ${id}`);
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
for (const entry of entries) {
|
|
22
|
+
if (table.get(entry.id) === entry.id) {
|
|
23
|
+
throw new Error(`wallet family ${entry.id} is registered twice`);
|
|
24
|
+
}
|
|
25
|
+
claim(entry.id, entry.id);
|
|
26
|
+
for (const alias of entry.aliases ?? []) claim(alias, entry.id);
|
|
27
|
+
}
|
|
28
|
+
return table;
|
|
29
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
import { act, renderHook } from "@testing-library/react";
|
|
3
|
+
import type { ReactNode } from "react";
|
|
4
|
+
import { describe, expect, it } from "vitest";
|
|
5
|
+
import { testDriver } from "../../tests/core/testDriver";
|
|
6
|
+
import { defineWalletFamily } from "./family";
|
|
7
|
+
import { ConnectionProvider, useConnection, useConnectionStore } from "./react";
|
|
8
|
+
import { createConnectionStore } from "./store";
|
|
9
|
+
|
|
10
|
+
function setup() {
|
|
11
|
+
const sol = testDriver("solana", [{ id: "phantom", name: "Phantom" }], {
|
|
12
|
+
phantom: { address: "7EcDhSYGxXyscszYEp35KHN8vvw3svAuLKTzXwCFLtV", chainId: "solana:mainnet" },
|
|
13
|
+
});
|
|
14
|
+
const store = createConnectionStore({
|
|
15
|
+
storage: null,
|
|
16
|
+
families: [
|
|
17
|
+
defineWalletFamily({
|
|
18
|
+
id: "sol",
|
|
19
|
+
label: "Solana",
|
|
20
|
+
message: { header: "wants you to sign in:", statement: null, chainId: null },
|
|
21
|
+
driver: () => sol,
|
|
22
|
+
}),
|
|
23
|
+
],
|
|
24
|
+
});
|
|
25
|
+
const wrapper = ({ children }: { children: ReactNode }) => (
|
|
26
|
+
<ConnectionProvider store={store}>{children}</ConnectionProvider>
|
|
27
|
+
);
|
|
28
|
+
return { store, sol, wrapper };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
describe("useConnection", () => {
|
|
32
|
+
it("renders the store's state and follows connect, account changes and disconnect", async () => {
|
|
33
|
+
const { store, sol, wrapper } = setup();
|
|
34
|
+
const { result } = renderHook(() => useConnection(), { wrapper });
|
|
35
|
+
expect(result.current.status).toBe("disconnected");
|
|
36
|
+
|
|
37
|
+
await act(() => store.connect("sol", "phantom"));
|
|
38
|
+
expect(result.current).toMatchObject({
|
|
39
|
+
status: "connected",
|
|
40
|
+
family: "sol",
|
|
41
|
+
connector: { id: "phantom", name: "Phantom" },
|
|
42
|
+
address: "7EcDhSYGxXyscszYEp35KHN8vvw3svAuLKTzXwCFLtV",
|
|
43
|
+
chainId: "solana:mainnet",
|
|
44
|
+
});
|
|
45
|
+
expect(result.current.client?.family).toBe("solana");
|
|
46
|
+
|
|
47
|
+
act(() =>
|
|
48
|
+
sol.emit({
|
|
49
|
+
status: "connected",
|
|
50
|
+
connectorId: "phantom",
|
|
51
|
+
account: {
|
|
52
|
+
address: "9xQeWvG816bUx9EPjHmaT23yvVM2ZWbrrpZb9PusVFin",
|
|
53
|
+
chainId: "solana:devnet",
|
|
54
|
+
},
|
|
55
|
+
}),
|
|
56
|
+
);
|
|
57
|
+
expect(result.current).toMatchObject({
|
|
58
|
+
address: "9xQeWvG816bUx9EPjHmaT23yvVM2ZWbrrpZb9PusVFin",
|
|
59
|
+
chainId: "solana:devnet",
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
await act(() => store.disconnect());
|
|
63
|
+
expect(result.current.status).toBe("disconnected");
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("useConnectionStore hands back the provided store", () => {
|
|
67
|
+
const { store, wrapper } = setup();
|
|
68
|
+
const { result } = renderHook(() => useConnectionStore(), { wrapper });
|
|
69
|
+
expect(result.current).toBe(store);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("refuses to run outside a ConnectionProvider", () => {
|
|
73
|
+
expect(() => renderHook(() => useConnection())).toThrow(/needs a ConnectionProvider/);
|
|
74
|
+
});
|
|
75
|
+
});
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createContext,
|
|
3
|
+
createElement,
|
|
4
|
+
useContext,
|
|
5
|
+
useSyncExternalStore,
|
|
6
|
+
type ReactNode,
|
|
7
|
+
} from "react";
|
|
8
|
+
import type { ConnectionStore } from "./store";
|
|
9
|
+
import type { ConnectionState } from "./types";
|
|
10
|
+
|
|
11
|
+
const ConnectionStoreContext = createContext<ConnectionStore | null>(null);
|
|
12
|
+
|
|
13
|
+
export interface ConnectionProviderProps {
|
|
14
|
+
store: ConnectionStore;
|
|
15
|
+
children?: ReactNode;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Makes one connection store the app's. Web3Provider mounts this once it owns a store. */
|
|
19
|
+
export function ConnectionProvider({ store, children }: ConnectionProviderProps) {
|
|
20
|
+
return createElement(ConnectionStoreContext.Provider, { value: store }, children);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** The store under the nearest ConnectionProvider, for connect, disconnect and reconnect. */
|
|
24
|
+
export function useConnectionStore(): ConnectionStore {
|
|
25
|
+
const store = useContext(ConnectionStoreContext);
|
|
26
|
+
if (!store) throw new Error("useConnectionStore needs a ConnectionProvider above it");
|
|
27
|
+
return store;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** The active connection: status, family, connector, address, chainId and client. */
|
|
31
|
+
export function useConnection(): ConnectionState {
|
|
32
|
+
const store = useConnectionStore();
|
|
33
|
+
return useSyncExternalStore(store.subscribe, store.getState, store.getState);
|
|
34
|
+
}
|