@multiplatform.one/web3 7.32.0 → 7.33.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.
Files changed (46) hide show
  1. package/README.md +21 -0
  2. package/dist/cjs/phantom/evm.cjs +133 -0
  3. package/dist/cjs/phantom/evm.native.js +269 -0
  4. package/dist/cjs/phantom/evm.native.js.map +1 -0
  5. package/dist/cjs/phantom/index.cjs +36 -0
  6. package/dist/cjs/phantom/index.native.js +39 -0
  7. package/dist/cjs/phantom/index.native.js.map +1 -0
  8. package/dist/cjs/phantom/urls.cjs +33 -0
  9. package/dist/cjs/phantom/urls.native.js +38 -0
  10. package/dist/cjs/phantom/urls.native.js.map +1 -0
  11. package/dist/esm/phantom/evm.mjs +103 -0
  12. package/dist/esm/phantom/evm.mjs.map +1 -0
  13. package/dist/esm/phantom/evm.native.js +236 -0
  14. package/dist/esm/phantom/evm.native.js.map +1 -0
  15. package/dist/esm/phantom/index.mjs +4 -0
  16. package/dist/esm/phantom/index.mjs.map +1 -0
  17. package/dist/esm/phantom/index.native.js +4 -0
  18. package/dist/esm/phantom/index.native.js.map +1 -0
  19. package/dist/esm/phantom/urls.mjs +7 -0
  20. package/dist/esm/phantom/urls.mjs.map +1 -0
  21. package/dist/esm/phantom/urls.native.js +9 -0
  22. package/dist/esm/phantom/urls.native.js.map +1 -0
  23. package/dist/jsx/phantom/evm.mjs +103 -0
  24. package/dist/jsx/phantom/evm.mjs.map +1 -0
  25. package/dist/jsx/phantom/evm.native.js +269 -0
  26. package/dist/jsx/phantom/evm.native.js.map +1 -0
  27. package/dist/jsx/phantom/index.mjs +4 -0
  28. package/dist/jsx/phantom/index.mjs.map +1 -0
  29. package/dist/jsx/phantom/index.native.js +39 -0
  30. package/dist/jsx/phantom/index.native.js.map +1 -0
  31. package/dist/jsx/phantom/urls.mjs +7 -0
  32. package/dist/jsx/phantom/urls.mjs.map +1 -0
  33. package/dist/jsx/phantom/urls.native.js +38 -0
  34. package/dist/jsx/phantom/urls.native.js.map +1 -0
  35. package/package.json +12 -6
  36. package/src/phantom/evm.spec.ts +195 -0
  37. package/src/phantom/evm.ts +163 -0
  38. package/src/phantom/index.ts +16 -0
  39. package/src/phantom/urls.spec.ts +29 -0
  40. package/src/phantom/urls.ts +16 -0
  41. package/types/phantom/evm.d.ts +76 -0
  42. package/types/phantom/evm.d.ts.map +1 -0
  43. package/types/phantom/index.d.ts +3 -0
  44. package/types/phantom/index.d.ts.map +1 -0
  45. package/types/phantom/urls.d.ts +11 -0
  46. package/types/phantom/urls.d.ts.map +1 -0
@@ -0,0 +1,163 @@
1
+ import { numberToHex, type Chain, type EIP1193Provider, type PublicClient } from "viem";
2
+ import { evmClient, type EvmClient } from "../clients/evm";
3
+ import { phantomBrowseUrl } from "./urls";
4
+
5
+ export type PhantomEthereumProvider = EIP1193Provider & { isPhantom?: boolean };
6
+
7
+ interface PhantomWindow {
8
+ phantom?: { ethereum?: PhantomEthereumProvider };
9
+ ethereum?: PhantomEthereumProvider;
10
+ location?: { href: string; origin: string; assign(url: string): void };
11
+ addEventListener?: (type: string, listener: (event: Event) => void) => void;
12
+ removeEventListener?: (type: string, listener: (event: Event) => void) => void;
13
+ dispatchEvent?: (event: Event) => boolean;
14
+ }
15
+
16
+ const phantomRdns = "app.phantom";
17
+
18
+ /**
19
+ * Phantom's injected EVM provider: `window.phantom.ethereum`, else a
20
+ * `window.ethereum` that says it is Phantom. Present in Phantom's desktop
21
+ * extension and its in-app browser, never in Chrome or Safari on a phone.
22
+ */
23
+ export function phantomEthereumProvider(
24
+ win: PhantomWindow = globalThis as PhantomWindow,
25
+ ): PhantomEthereumProvider | undefined {
26
+ const own = win.phantom?.ethereum;
27
+ if (own?.isPhantom) return own;
28
+ if (win.ethereum?.isPhantom) return win.ethereum;
29
+ return undefined;
30
+ }
31
+
32
+ /**
33
+ * phantomEthereumProvider, or Phantom's EIP-6963 announcement (rdns
34
+ * `app.phantom`) when the provider is not on `window` yet. Resolves undefined
35
+ * after `timeout` ms.
36
+ */
37
+ export function discoverPhantomEthereumProvider(
38
+ options: { timeout?: number; window?: PhantomWindow } = {},
39
+ ): Promise<PhantomEthereumProvider | undefined> {
40
+ const win = options.window ?? (globalThis as PhantomWindow);
41
+ const found = phantomEthereumProvider(win);
42
+ if (found || !win.addEventListener || !win.dispatchEvent) return Promise.resolve(found);
43
+ return new Promise((resolve) => {
44
+ const onAnnounce = (event: Event) => {
45
+ const detail = (
46
+ event as CustomEvent<{ info?: { rdns?: string }; provider?: PhantomEthereumProvider }>
47
+ ).detail;
48
+ if (detail?.info?.rdns !== phantomRdns || !detail.provider) return;
49
+ done(detail.provider);
50
+ };
51
+ const timer = setTimeout(() => done(phantomEthereumProvider(win)), options.timeout ?? 500);
52
+ const done = (provider: PhantomEthereumProvider | undefined) => {
53
+ clearTimeout(timer);
54
+ win.removeEventListener?.("eip6963:announceProvider", onAnnounce);
55
+ resolve(provider);
56
+ };
57
+ win.addEventListener?.("eip6963:announceProvider", onAnnounce);
58
+ win.dispatchEvent?.(new Event("eip6963:requestProvider"));
59
+ });
60
+ }
61
+
62
+ /**
63
+ * How this page reaches Phantom's EVM wallet: `injected` when Phantom's
64
+ * provider is here (extension or Phantom's browser), `browse` when the page
65
+ * must first reopen inside Phantom with openInPhantom.
66
+ */
67
+ export function phantomEvmRoute(win: PhantomWindow = globalThis as PhantomWindow) {
68
+ return phantomEthereumProvider(win) ? ("injected" as const) : ("browse" as const);
69
+ }
70
+
71
+ export interface OpenInPhantomOptions {
72
+ /** The page to open in Phantom's browser. Defaults to this page. */
73
+ url?: string;
74
+ /** The requesting app. Defaults to this page's origin. */
75
+ ref?: string;
76
+ /** Defaults to `location.assign`. */
77
+ navigate?: (url: string) => void;
78
+ base?: string;
79
+ window?: PhantomWindow;
80
+ }
81
+
82
+ /**
83
+ * Reopens the page inside Phantom's in-app browser. Call it straight from the
84
+ * tap handler with nothing awaited first: mobile browsers only follow a link
85
+ * into another app without a prompt inside the tap's user activation.
86
+ * Returns the link, for an `<a href>` instead.
87
+ */
88
+ export function openInPhantom(options: OpenInPhantomOptions = {}): string {
89
+ const win = options.window ?? (globalThis as PhantomWindow);
90
+ const url = options.url ?? win.location?.href;
91
+ const ref = options.ref ?? win.location?.origin ?? url;
92
+ if (!url || !ref) throw new Error("openInPhantom: no page URL here; pass `url` and `ref`");
93
+ const link = phantomBrowseUrl(url, ref, { base: options.base });
94
+ (options.navigate ?? ((target: string) => win.location?.assign(target)))(link);
95
+ return link;
96
+ }
97
+
98
+ export interface ConnectPhantomEvmOptions {
99
+ chain: Chain;
100
+ /** Defaults to phantomEthereumProvider(). */
101
+ provider?: PhantomEthereumProvider;
102
+ publicClient?: PublicClient;
103
+ rpcUrl?: string;
104
+ }
105
+
106
+ export class PhantomNotInjectedError extends Error {
107
+ constructor() {
108
+ super(
109
+ "Phantom's EVM provider is not on this page: on a phone, reopen it in Phantom with openInPhantom()",
110
+ );
111
+ this.name = "PhantomNotInjectedError";
112
+ }
113
+ }
114
+
115
+ const errorCode = (error: unknown) =>
116
+ (error as { code?: unknown; cause?: { code?: unknown } } | undefined)?.code ??
117
+ (error as { cause?: { code?: unknown } } | undefined)?.cause?.code;
118
+
119
+ async function ensureChain(provider: PhantomEthereumProvider, chain: Chain) {
120
+ const current = await provider.request({ method: "eth_chainId" });
121
+ if (Number(current) === chain.id) return;
122
+ const chainId = numberToHex(chain.id);
123
+ try {
124
+ await provider.request({ method: "wallet_switchEthereumChain", params: [{ chainId }] });
125
+ } catch (error) {
126
+ if (errorCode(error) !== 4902) throw error;
127
+ await provider.request({
128
+ method: "wallet_addEthereumChain",
129
+ params: [
130
+ {
131
+ chainId,
132
+ chainName: chain.name,
133
+ nativeCurrency: chain.nativeCurrency,
134
+ rpcUrls: [...chain.rpcUrls.default.http],
135
+ blockExplorerUrls: chain.blockExplorers ? [chain.blockExplorers.default.url] : undefined,
136
+ },
137
+ ],
138
+ });
139
+ await provider.request({ method: "wallet_switchEthereumChain", params: [{ chainId }] });
140
+ }
141
+ }
142
+
143
+ /**
144
+ * Connects Phantom's injected EVM wallet, moves it to `chain`, and returns
145
+ * the mpo EVM client on it, so `erc20.transfer` and `writeContract` go to
146
+ * Phantom for approval. Testnets such as Base Sepolia need Phantom's Testnet
147
+ * Mode on.
148
+ */
149
+ export async function connectPhantomEvm(options: ConnectPhantomEvmOptions): Promise<EvmClient> {
150
+ const provider = options.provider ?? phantomEthereumProvider();
151
+ if (!provider) throw new PhantomNotInjectedError();
152
+ const accounts = await provider.request({ method: "eth_requestAccounts" });
153
+ const account = accounts[0];
154
+ if (!account) throw new Error("Phantom returned no EVM account");
155
+ await ensureChain(provider, options.chain);
156
+ return evmClient({
157
+ provider,
158
+ account,
159
+ chain: options.chain,
160
+ publicClient: options.publicClient,
161
+ rpcUrl: options.rpcUrl,
162
+ });
163
+ }
@@ -0,0 +1,16 @@
1
+ // `@multiplatform.one/web3/phantom`: Phantom's EVM wallet from a phone's own
2
+ // browser. Phantom's deeplinks carry no EVM methods, so the page reopens in
3
+ // Phantom's in-app browser (openInPhantom) and connects the provider Phantom
4
+ // injects there (connectPhantomEvm).
5
+ export {
6
+ connectPhantomEvm,
7
+ discoverPhantomEthereumProvider,
8
+ openInPhantom,
9
+ phantomEthereumProvider,
10
+ phantomEvmRoute,
11
+ PhantomNotInjectedError,
12
+ type ConnectPhantomEvmOptions,
13
+ type OpenInPhantomOptions,
14
+ type PhantomEthereumProvider,
15
+ } from "./evm";
16
+ export { phantomBrowseUrl, phantomUniversalLinkBase } from "./urls";
@@ -0,0 +1,29 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { phantomBrowseUrl } from "./urls";
3
+
4
+ describe("phantomBrowseUrl", () => {
5
+ it("builds the browse link Phantom documents", () => {
6
+ expect(
7
+ phantomBrowseUrl(
8
+ "https://magiceden.io/item-details/8yjN8iRuoiYiKW487cnW9vn6mLBR5E8aCNKsBRmTP9vN",
9
+ "https://magiceden.io",
10
+ ),
11
+ ).toBe(
12
+ "https://phantom.com/ul/browse/https%3A%2F%2Fmagiceden.io%2Fitem-details%2F8yjN8iRuoiYiKW487cnW9vn6mLBR5E8aCNKsBRmTP9vN?ref=https%3A%2F%2Fmagiceden.io",
13
+ );
14
+ });
15
+
16
+ it("keeps a page's own query inside the browse link", () => {
17
+ expect(
18
+ phantomBrowseUrl(
19
+ "https://dev.promanager.vip/u/buy?mobile=1&x=a b",
20
+ "https://dev.promanager.vip",
21
+ {
22
+ base: "https://phantom.app/ul/",
23
+ },
24
+ ),
25
+ ).toBe(
26
+ "https://phantom.app/ul/browse/https%3A%2F%2Fdev.promanager.vip%2Fu%2Fbuy%3Fmobile%3D1%26x%3Da%20b?ref=https%3A%2F%2Fdev.promanager.vip",
27
+ );
28
+ });
29
+ });
@@ -0,0 +1,16 @@
1
+ /** Phantom's universal-link host. `https://phantom.app/ul` still answers; Phantom asks new code to use this one. */
2
+ export const phantomUniversalLinkBase = "https://phantom.com/ul";
3
+
4
+ /**
5
+ * Opens `url` inside Phantom's in-app browser, where Phantom injects
6
+ * `window.phantom.ethereum`. Phantom's deeplinks carry no EVM methods, so
7
+ * this is how a phone's browser reaches its EVM wallet.
8
+ */
9
+ export function phantomBrowseUrl(
10
+ url: string,
11
+ ref: string,
12
+ options: { base?: string } = {},
13
+ ): string {
14
+ const base = (options.base ?? phantomUniversalLinkBase).replace(/\/+$/, "");
15
+ return `${base}/browse/${encodeURIComponent(url)}?ref=${encodeURIComponent(ref)}`;
16
+ }
@@ -0,0 +1,76 @@
1
+ import { type Chain, type EIP1193Provider, type PublicClient } from "viem";
2
+ import { type EvmClient } from "../clients/evm";
3
+ export type PhantomEthereumProvider = EIP1193Provider & {
4
+ isPhantom?: boolean;
5
+ };
6
+ interface PhantomWindow {
7
+ phantom?: {
8
+ ethereum?: PhantomEthereumProvider;
9
+ };
10
+ ethereum?: PhantomEthereumProvider;
11
+ location?: {
12
+ href: string;
13
+ origin: string;
14
+ assign(url: string): void;
15
+ };
16
+ addEventListener?: (type: string, listener: (event: Event) => void) => void;
17
+ removeEventListener?: (type: string, listener: (event: Event) => void) => void;
18
+ dispatchEvent?: (event: Event) => boolean;
19
+ }
20
+ /**
21
+ * Phantom's injected EVM provider: `window.phantom.ethereum`, else a
22
+ * `window.ethereum` that says it is Phantom. Present in Phantom's desktop
23
+ * extension and its in-app browser, never in Chrome or Safari on a phone.
24
+ */
25
+ export declare function phantomEthereumProvider(win?: PhantomWindow): PhantomEthereumProvider | undefined;
26
+ /**
27
+ * phantomEthereumProvider, or Phantom's EIP-6963 announcement (rdns
28
+ * `app.phantom`) when the provider is not on `window` yet. Resolves undefined
29
+ * after `timeout` ms.
30
+ */
31
+ export declare function discoverPhantomEthereumProvider(options?: {
32
+ timeout?: number;
33
+ window?: PhantomWindow;
34
+ }): Promise<PhantomEthereumProvider | undefined>;
35
+ /**
36
+ * How this page reaches Phantom's EVM wallet: `injected` when Phantom's
37
+ * provider is here (extension or Phantom's browser), `browse` when the page
38
+ * must first reopen inside Phantom with openInPhantom.
39
+ */
40
+ export declare function phantomEvmRoute(win?: PhantomWindow): "injected" | "browse";
41
+ export interface OpenInPhantomOptions {
42
+ /** The page to open in Phantom's browser. Defaults to this page. */
43
+ url?: string;
44
+ /** The requesting app. Defaults to this page's origin. */
45
+ ref?: string;
46
+ /** Defaults to `location.assign`. */
47
+ navigate?: (url: string) => void;
48
+ base?: string;
49
+ window?: PhantomWindow;
50
+ }
51
+ /**
52
+ * Reopens the page inside Phantom's in-app browser. Call it straight from the
53
+ * tap handler with nothing awaited first: mobile browsers only follow a link
54
+ * into another app without a prompt inside the tap's user activation.
55
+ * Returns the link, for an `<a href>` instead.
56
+ */
57
+ export declare function openInPhantom(options?: OpenInPhantomOptions): string;
58
+ export interface ConnectPhantomEvmOptions {
59
+ chain: Chain;
60
+ /** Defaults to phantomEthereumProvider(). */
61
+ provider?: PhantomEthereumProvider;
62
+ publicClient?: PublicClient;
63
+ rpcUrl?: string;
64
+ }
65
+ export declare class PhantomNotInjectedError extends Error {
66
+ constructor();
67
+ }
68
+ /**
69
+ * Connects Phantom's injected EVM wallet, moves it to `chain`, and returns
70
+ * the mpo EVM client on it, so `erc20.transfer` and `writeContract` go to
71
+ * Phantom for approval. Testnets such as Base Sepolia need Phantom's Testnet
72
+ * Mode on.
73
+ */
74
+ export declare function connectPhantomEvm(options: ConnectPhantomEvmOptions): Promise<EvmClient>;
75
+ export {};
76
+ //# sourceMappingURL=evm.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"evm.d.ts","sourceRoot":"","sources":["../../src/phantom/evm.ts"],"names":[],"mappings":"AAAA,OAAO,EAAe,KAAK,KAAK,EAAE,KAAK,eAAe,EAAE,KAAK,YAAY,EAAE,MAAM,MAAM,CAAC;AACxF,OAAO,EAAa,KAAK,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAG3D,MAAM,MAAM,uBAAuB,GAAG,eAAe,GAAG;IAAE,SAAS,CAAC,EAAE,OAAO,CAAA;CAAE,CAAC;AAEhF,UAAU,aAAa;IACrB,OAAO,CAAC,EAAE;QAAE,QAAQ,CAAC,EAAE,uBAAuB,CAAA;KAAE,CAAC;IACjD,QAAQ,CAAC,EAAE,uBAAuB,CAAC;IACnC,QAAQ,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,CAAC;IACvE,gBAAgB,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,KAAK,IAAI,CAAC;IAC5E,mBAAmB,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,KAAK,IAAI,CAAC;IAC/E,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,OAAO,CAAC;CAC3C;AAID;;;;GAIG;AACH,wBAAgB,uBAAuB,CACrC,GAAG,GAAE,aAA2C,GAC/C,uBAAuB,GAAG,SAAS,CAKrC;AAED;;;;GAIG;AACH,wBAAgB,+BAA+B,CAC7C,OAAO,GAAE;IAAE,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,aAAa,CAAA;CAAO,GACzD,OAAO,CAAC,uBAAuB,GAAG,SAAS,CAAC,CAqB9C;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,GAAG,GAAE,aAA2C,yBAE/E;AAED,MAAM,WAAW,oBAAoB;IACnC,oEAAoE;IACpE,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,0DAA0D;IAC1D,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,qCAAqC;IACrC,QAAQ,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;IACjC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,aAAa,CAAC;CACxB;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,OAAO,GAAE,oBAAyB,GAAG,MAAM,CAQxE;AAED,MAAM,WAAW,wBAAwB;IACvC,KAAK,EAAE,KAAK,CAAC;IACb,6CAA6C;IAC7C,QAAQ,CAAC,EAAE,uBAAuB,CAAC;IACnC,YAAY,CAAC,EAAE,YAAY,CAAC;IAC5B,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,qBAAa,uBAAwB,SAAQ,KAAK;;CAOjD;AA8BD;;;;;GAKG;AACH,wBAAsB,iBAAiB,CAAC,OAAO,EAAE,wBAAwB,GAAG,OAAO,CAAC,SAAS,CAAC,CAc7F"}
@@ -0,0 +1,3 @@
1
+ export { connectPhantomEvm, discoverPhantomEthereumProvider, openInPhantom, phantomEthereumProvider, phantomEvmRoute, PhantomNotInjectedError, type ConnectPhantomEvmOptions, type OpenInPhantomOptions, type PhantomEthereumProvider, } from "./evm";
2
+ export { phantomBrowseUrl, phantomUniversalLinkBase } from "./urls";
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/phantom/index.ts"],"names":[],"mappings":"AAIA,OAAO,EACL,iBAAiB,EACjB,+BAA+B,EAC/B,aAAa,EACb,uBAAuB,EACvB,eAAe,EACf,uBAAuB,EACvB,KAAK,wBAAwB,EAC7B,KAAK,oBAAoB,EACzB,KAAK,uBAAuB,GAC7B,MAAM,OAAO,CAAC;AACf,OAAO,EAAE,gBAAgB,EAAE,wBAAwB,EAAE,MAAM,QAAQ,CAAC"}
@@ -0,0 +1,11 @@
1
+ /** Phantom's universal-link host. `https://phantom.app/ul` still answers; Phantom asks new code to use this one. */
2
+ export declare const phantomUniversalLinkBase = "https://phantom.com/ul";
3
+ /**
4
+ * Opens `url` inside Phantom's in-app browser, where Phantom injects
5
+ * `window.phantom.ethereum`. Phantom's deeplinks carry no EVM methods, so
6
+ * this is how a phone's browser reaches its EVM wallet.
7
+ */
8
+ export declare function phantomBrowseUrl(url: string, ref: string, options?: {
9
+ base?: string;
10
+ }): string;
11
+ //# sourceMappingURL=urls.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"urls.d.ts","sourceRoot":"","sources":["../../src/phantom/urls.ts"],"names":[],"mappings":"AAAA,oHAAoH;AACpH,eAAO,MAAM,wBAAwB,2BAA2B,CAAC;AAEjE;;;;GAIG;AACH,wBAAgB,gBAAgB,CAC9B,GAAG,EAAE,MAAM,EACX,GAAG,EAAE,MAAM,EACX,OAAO,GAAE;IAAE,IAAI,CAAC,EAAE,MAAM,CAAA;CAAO,GAC9B,MAAM,CAGR"}