@prb/effect-evm-safe 1.0.0-beta.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/constants/index.d.ts +5 -0
- package/dist/constants/index.d.ts.map +1 -0
- package/dist/constants/index.js +6 -0
- package/dist/constants/index.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/dist/react-hooks/index.d.ts +5 -0
- package/dist/react-hooks/index.d.ts.map +1 -0
- package/dist/react-hooks/index.js +6 -0
- package/dist/react-hooks/index.js.map +1 -0
- package/dist/react-hooks/safe-app-origins.d.ts +8 -0
- package/dist/react-hooks/safe-app-origins.d.ts.map +1 -0
- package/dist/react-hooks/safe-app-origins.js +124 -0
- package/dist/react-hooks/safe-app-origins.js.map +1 -0
- package/dist/react-hooks/use-is-host-safe-app.d.ts +2 -0
- package/dist/react-hooks/use-is-host-safe-app.d.ts.map +1 -0
- package/dist/react-hooks/use-is-host-safe-app.js +16 -0
- package/dist/react-hooks/use-is-host-safe-app.js.map +1 -0
- package/dist/react-hooks/use-is-safe-app-context.d.ts +2 -0
- package/dist/react-hooks/use-is-safe-app-context.d.ts.map +1 -0
- package/dist/react-hooks/use-is-safe-app-context.js +41 -0
- package/dist/react-hooks/use-is-safe-app-context.js.map +1 -0
- package/dist/react-hooks/use-is-safe-multisig-wallet.d.ts +2 -0
- package/dist/react-hooks/use-is-safe-multisig-wallet.d.ts.map +1 -0
- package/dist/react-hooks/use-is-safe-multisig-wallet.js +24 -0
- package/dist/react-hooks/use-is-safe-multisig-wallet.js.map +1 -0
- package/dist/safe/adapter.d.ts +7 -0
- package/dist/safe/adapter.d.ts.map +1 -0
- package/dist/safe/adapter.js +13 -0
- package/dist/safe/adapter.js.map +1 -0
- package/dist/safe/detection.d.ts +26 -0
- package/dist/safe/detection.d.ts.map +1 -0
- package/dist/safe/detection.js +102 -0
- package/dist/safe/detection.js.map +1 -0
- package/dist/safe/detection.test.integration.d.ts +2 -0
- package/dist/safe/detection.test.integration.d.ts.map +1 -0
- package/dist/safe/detection.test.integration.js +92 -0
- package/dist/safe/detection.test.integration.js.map +1 -0
- package/dist/safe/errors.d.ts +79 -0
- package/dist/safe/errors.d.ts.map +1 -0
- package/dist/safe/errors.js +34 -0
- package/dist/safe/errors.js.map +1 -0
- package/dist/safe/index.d.ts +8 -0
- package/dist/safe/index.d.ts.map +1 -0
- package/dist/safe/index.js +6 -0
- package/dist/safe/index.js.map +1 -0
- package/dist/safe/internal/poll.d.ts +6 -0
- package/dist/safe/internal/poll.d.ts.map +1 -0
- package/dist/safe/internal/poll.js +21 -0
- package/dist/safe/internal/poll.js.map +1 -0
- package/dist/safe/internal/sdk-types.d.ts +3 -0
- package/dist/safe/internal/sdk-types.d.ts.map +1 -0
- package/dist/safe/internal/sdk-types.js +2 -0
- package/dist/safe/internal/sdk-types.js.map +1 -0
- package/dist/safe/live.d.ts +7 -0
- package/dist/safe/live.d.ts.map +1 -0
- package/dist/safe/live.js +203 -0
- package/dist/safe/live.js.map +1 -0
- package/dist/safe/service.d.ts +26 -0
- package/dist/safe/service.d.ts.map +1 -0
- package/dist/safe/service.js +4 -0
- package/dist/safe/service.js.map +1 -0
- package/dist/safe/service.test.integration.d.ts +2 -0
- package/dist/safe/service.test.integration.d.ts.map +1 -0
- package/dist/safe/service.test.integration.js +171 -0
- package/dist/safe/service.test.integration.js.map +1 -0
- package/dist/safe/simulation/abis.d.ts +73 -0
- package/dist/safe/simulation/abis.d.ts.map +1 -0
- package/dist/safe/simulation/abis.js +61 -0
- package/dist/safe/simulation/abis.js.map +1 -0
- package/dist/safe/simulation/addresses.d.ts +4 -0
- package/dist/safe/simulation/addresses.d.ts.map +1 -0
- package/dist/safe/simulation/addresses.js +56 -0
- package/dist/safe/simulation/addresses.js.map +1 -0
- package/dist/safe/simulation/encoding.d.ts +16 -0
- package/dist/safe/simulation/encoding.d.ts.map +1 -0
- package/dist/safe/simulation/encoding.js +36 -0
- package/dist/safe/simulation/encoding.js.map +1 -0
- package/dist/safe/simulation/errors.d.ts +56 -0
- package/dist/safe/simulation/errors.d.ts.map +1 -0
- package/dist/safe/simulation/errors.js +37 -0
- package/dist/safe/simulation/errors.js.map +1 -0
- package/dist/safe/simulation/index.d.ts +7 -0
- package/dist/safe/simulation/index.d.ts.map +1 -0
- package/dist/safe/simulation/index.js +6 -0
- package/dist/safe/simulation/index.js.map +1 -0
- package/dist/safe/simulation/internal/calldata/calldata.d.ts +5 -0
- package/dist/safe/simulation/internal/calldata/calldata.d.ts.map +1 -0
- package/dist/safe/simulation/internal/calldata/calldata.js +17 -0
- package/dist/safe/simulation/internal/calldata/calldata.js.map +1 -0
- package/dist/safe/simulation/internal/calldata/index.d.ts +2 -0
- package/dist/safe/simulation/internal/calldata/index.d.ts.map +1 -0
- package/dist/safe/simulation/internal/calldata/index.js +2 -0
- package/dist/safe/simulation/internal/calldata/index.js.map +1 -0
- package/dist/safe/simulation/internal/contracts/contracts.d.ts +5 -0
- package/dist/safe/simulation/internal/contracts/contracts.d.ts.map +1 -0
- package/dist/safe/simulation/internal/contracts/contracts.js +25 -0
- package/dist/safe/simulation/internal/contracts/contracts.js.map +1 -0
- package/dist/safe/simulation/internal/contracts/index.d.ts +2 -0
- package/dist/safe/simulation/internal/contracts/index.d.ts.map +1 -0
- package/dist/safe/simulation/internal/contracts/index.js +2 -0
- package/dist/safe/simulation/internal/contracts/index.js.map +1 -0
- package/dist/safe/simulation/internal/evaluation/evaluation.d.ts +6 -0
- package/dist/safe/simulation/internal/evaluation/evaluation.d.ts.map +1 -0
- package/dist/safe/simulation/internal/evaluation/evaluation.js +20 -0
- package/dist/safe/simulation/internal/evaluation/evaluation.js.map +1 -0
- package/dist/safe/simulation/internal/evaluation/index.d.ts +2 -0
- package/dist/safe/simulation/internal/evaluation/index.d.ts.map +1 -0
- package/dist/safe/simulation/internal/evaluation/index.js +2 -0
- package/dist/safe/simulation/internal/evaluation/index.js.map +1 -0
- package/dist/safe/simulation/internal/execution/execution.d.ts +9 -0
- package/dist/safe/simulation/internal/execution/execution.d.ts.map +1 -0
- package/dist/safe/simulation/internal/execution/execution.js +65 -0
- package/dist/safe/simulation/internal/execution/execution.js.map +1 -0
- package/dist/safe/simulation/internal/execution/index.d.ts +2 -0
- package/dist/safe/simulation/internal/execution/index.d.ts.map +1 -0
- package/dist/safe/simulation/internal/execution/index.js +2 -0
- package/dist/safe/simulation/internal/execution/index.js.map +1 -0
- package/dist/safe/simulation/internal/limits/index.d.ts +2 -0
- package/dist/safe/simulation/internal/limits/index.d.ts.map +1 -0
- package/dist/safe/simulation/internal/limits/index.js +2 -0
- package/dist/safe/simulation/internal/limits/index.js.map +1 -0
- package/dist/safe/simulation/internal/limits/limits.d.ts +5 -0
- package/dist/safe/simulation/internal/limits/limits.d.ts.map +1 -0
- package/dist/safe/simulation/internal/limits/limits.js +18 -0
- package/dist/safe/simulation/internal/limits/limits.js.map +1 -0
- package/dist/safe/simulation/internal/types/index.d.ts +2 -0
- package/dist/safe/simulation/internal/types/index.d.ts.map +1 -0
- package/dist/safe/simulation/internal/types/index.js +2 -0
- package/dist/safe/simulation/internal/types/index.js.map +1 -0
- package/dist/safe/simulation/internal/types/types.d.ts +11 -0
- package/dist/safe/simulation/internal/types/types.d.ts.map +1 -0
- package/dist/safe/simulation/internal/types/types.js +2 -0
- package/dist/safe/simulation/internal/types/types.js.map +1 -0
- package/dist/safe/simulation/internal/validation/index.d.ts +2 -0
- package/dist/safe/simulation/internal/validation/index.d.ts.map +1 -0
- package/dist/safe/simulation/internal/validation/index.js +2 -0
- package/dist/safe/simulation/internal/validation/index.js.map +1 -0
- package/dist/safe/simulation/internal/validation/validation.d.ts +5 -0
- package/dist/safe/simulation/internal/validation/validation.d.ts.map +1 -0
- package/dist/safe/simulation/internal/validation/validation.js +27 -0
- package/dist/safe/simulation/internal/validation/validation.js.map +1 -0
- package/dist/safe/simulation/service.d.ts +14 -0
- package/dist/safe/simulation/service.d.ts.map +1 -0
- package/dist/safe/simulation/service.js +25 -0
- package/dist/safe/simulation/service.js.map +1 -0
- package/dist/safe/simulation/types.d.ts +20 -0
- package/dist/safe/simulation/types.d.ts.map +1 -0
- package/dist/safe/simulation/types.js +2 -0
- package/dist/safe/simulation/types.js.map +1 -0
- package/dist/safe/types.d.ts +61 -0
- package/dist/safe/types.d.ts.map +1 -0
- package/dist/safe/types.js +2 -0
- package/dist/safe/types.js.map +1 -0
- package/package.json +105 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/constants/index.ts"],"names":[],"mappings":"AAOA,eAAO,MAAM,kBAAkB,QAAiC,CAAC;AAGjE,eAAO,MAAM,sBAAsB,QAAiC,CAAC;AAGrE,eAAO,MAAM,4BAA4B,QAAiC,CAAC;AAG3E,eAAO,MAAM,sBAAsB,QAAiC,CAAC"}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { Duration } from "effect";
|
|
2
|
+
export const SAFE_POLL_INTERVAL = Duration.toMillis("3 seconds");
|
|
3
|
+
export const SAFE_EXECUTION_TIMEOUT = Duration.toMillis("5 minutes");
|
|
4
|
+
export const SAFE_SIGNATURE_POLL_INTERVAL = Duration.toMillis("2 seconds");
|
|
5
|
+
export const SAFE_SIGNATURE_TIMEOUT = Duration.toMillis("2 minutes");
|
|
6
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/constants/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,QAAQ,CAAC;AAOlC,MAAM,CAAC,MAAM,kBAAkB,GAAG,QAAQ,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC;AAGjE,MAAM,CAAC,MAAM,sBAAsB,GAAG,QAAQ,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC;AAGrE,MAAM,CAAC,MAAM,4BAA4B,GAAG,QAAQ,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC;AAG3E,MAAM,CAAC,MAAM,sBAAsB,GAAG,QAAQ,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC","sourcesContent":["import { Duration } from \"effect\";\n\n// =============================================================================\n// Safe Apps Constants\n// =============================================================================\n\n/** Default poll interval for Safe gateway status checks. */\nexport const SAFE_POLL_INTERVAL = Duration.toMillis(\"3 seconds\");\n\n/** Default timeout for Safe transaction execution. */\nexport const SAFE_EXECUTION_TIMEOUT = Duration.toMillis(\"5 minutes\");\n\n/** Default poll interval for off-chain signature checks. */\nexport const SAFE_SIGNATURE_POLL_INTERVAL = Duration.toMillis(\"2 seconds\");\n\n/** Default timeout for off-chain signature availability. */\nexport const SAFE_SIGNATURE_TIMEOUT = Duration.toMillis(\"2 minutes\");\n"]}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,iBAAiB,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,iBAAiB,CAAC","sourcesContent":["export * from \"./safe/index.js\";\n"]}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { DEFAULT_SAFE_APP_ORIGINS, extendSafeAppOrigins, getSafeAppOrigins, setSafeAppOrigins, } from "./safe-app-origins.js";
|
|
2
|
+
export { useIsHostSafeApp } from "./use-is-host-safe-app.js";
|
|
3
|
+
export { useIsSafeAppContext } from "./use-is-safe-app-context.js";
|
|
4
|
+
export { useIsSafeMultisigWallet } from "./use-is-safe-multisig-wallet.js";
|
|
5
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/react-hooks/index.ts"],"names":[],"mappings":"AAEA,OAAO,EACL,wBAAwB,EACxB,oBAAoB,EACpB,iBAAiB,EACjB,iBAAiB,GAClB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EAAE,gBAAgB,EAAE,MAAM,2BAA2B,CAAC;AAC7D,OAAO,EAAE,mBAAmB,EAAE,MAAM,8BAA8B,CAAC;AACnE,OAAO,EAAE,uBAAuB,EAAE,MAAM,kCAAkC,CAAC"}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
export { DEFAULT_SAFE_APP_ORIGINS, extendSafeAppOrigins, getSafeAppOrigins, setSafeAppOrigins, } from "./safe-app-origins.js";
|
|
3
|
+
export { useIsHostSafeApp } from "./use-is-host-safe-app.js";
|
|
4
|
+
export { useIsSafeAppContext } from "./use-is-safe-app-context.js";
|
|
5
|
+
export { useIsSafeMultisigWallet } from "./use-is-safe-multisig-wallet.js";
|
|
6
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/react-hooks/index.ts"],"names":[],"mappings":"AAAA,YAAY,CAAC;AAEb,OAAO,EACL,wBAAwB,EACxB,oBAAoB,EACpB,iBAAiB,EACjB,iBAAiB,GAClB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EAAE,gBAAgB,EAAE,MAAM,2BAA2B,CAAC;AAC7D,OAAO,EAAE,mBAAmB,EAAE,MAAM,8BAA8B,CAAC;AACnE,OAAO,EAAE,uBAAuB,EAAE,MAAM,kCAAkC,CAAC","sourcesContent":["\"use client\";\n\nexport {\n DEFAULT_SAFE_APP_ORIGINS,\n extendSafeAppOrigins,\n getSafeAppOrigins,\n setSafeAppOrigins,\n} from \"./safe-app-origins.js\";\nexport { useIsHostSafeApp } from \"./use-is-host-safe-app.js\";\nexport { useIsSafeAppContext } from \"./use-is-safe-app-context.js\";\nexport { useIsSafeMultisigWallet } from \"./use-is-safe-multisig-wallet.js\";\n"]}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export declare const DEFAULT_SAFE_APP_ORIGINS: readonly ["https://app.safe.global", "https://gnosis-safe.io", "https://safe.global", "https://safe.berachain.com", "https://safe.chiliz.com", "https://safe.lightlink.io", "https://safe.optimism.io"];
|
|
2
|
+
export declare function setSafeAppOrigins(origins: readonly string[]): void;
|
|
3
|
+
export declare function extendSafeAppOrigins(origins: readonly string[]): void;
|
|
4
|
+
export declare function getSafeAppOrigins(): readonly string[];
|
|
5
|
+
export declare function subscribeSafeAppOrigins(listener: () => void): () => void;
|
|
6
|
+
export declare function isValidSafeAppOrigin(): boolean;
|
|
7
|
+
export declare function isHostEmbedded(): boolean;
|
|
8
|
+
//# sourceMappingURL=safe-app-origins.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"safe-app-origins.d.ts","sourceRoot":"","sources":["../../src/react-hooks/safe-app-origins.ts"],"names":[],"mappings":"AAMA,eAAO,MAAM,wBAAwB,yMAU3B,CAAC;AASX,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,QAE3D;AAGD,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,QAE9D;AAGD,wBAAgB,iBAAiB,IAAI,SAAS,MAAM,EAAE,CAErD;AAGD,wBAAgB,uBAAuB,CAAC,QAAQ,EAAE,MAAM,IAAI,GAAG,MAAM,IAAI,CAKxE;AAGD,wBAAgB,oBAAoB,IAAI,OAAO,CAM9C;AAGD,wBAAgB,cAAc,IAAI,OAAO,CAExC"}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
export const DEFAULT_SAFE_APP_ORIGINS = [
|
|
3
|
+
"https://app.safe.global",
|
|
4
|
+
"https://gnosis-safe.io",
|
|
5
|
+
"https://safe.global",
|
|
6
|
+
"https://safe.berachain.com",
|
|
7
|
+
"https://safe.chiliz.com",
|
|
8
|
+
"https://safe.lightlink.io",
|
|
9
|
+
"https://safe.optimism.io",
|
|
10
|
+
];
|
|
11
|
+
const ORIGIN_PROTOCOL_PATTERN = /^[a-z]+:\/\//i;
|
|
12
|
+
let safeAppOrigins = normalizeOrigins(DEFAULT_SAFE_APP_ORIGINS);
|
|
13
|
+
let safeAppOriginSet = new Set(safeAppOrigins);
|
|
14
|
+
const safeAppOriginListeners = new Set();
|
|
15
|
+
let safeAppOriginsConfigured = false;
|
|
16
|
+
export function setSafeAppOrigins(origins) {
|
|
17
|
+
configureSafeAppOrigins(normalizeOrigins(origins));
|
|
18
|
+
}
|
|
19
|
+
export function extendSafeAppOrigins(origins) {
|
|
20
|
+
configureSafeAppOrigins(normalizeOrigins([...safeAppOrigins, ...origins]));
|
|
21
|
+
}
|
|
22
|
+
export function getSafeAppOrigins() {
|
|
23
|
+
return [...safeAppOrigins];
|
|
24
|
+
}
|
|
25
|
+
export function subscribeSafeAppOrigins(listener) {
|
|
26
|
+
safeAppOriginListeners.add(listener);
|
|
27
|
+
return () => {
|
|
28
|
+
safeAppOriginListeners.delete(listener);
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
export function isValidSafeAppOrigin() {
|
|
32
|
+
const origin = getAncestorOrigin();
|
|
33
|
+
if (!origin) {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
return safeAppOriginSet.has(origin);
|
|
37
|
+
}
|
|
38
|
+
export function isHostEmbedded() {
|
|
39
|
+
return typeof window !== "undefined" && window.parent !== window;
|
|
40
|
+
}
|
|
41
|
+
function getAncestorOrigin() {
|
|
42
|
+
if (typeof window === "undefined") {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
try {
|
|
46
|
+
return window.parent.location.origin;
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
if (window.location.ancestorOrigins?.length) {
|
|
50
|
+
return window.location.ancestorOrigins[0];
|
|
51
|
+
}
|
|
52
|
+
if (document.referrer) {
|
|
53
|
+
try {
|
|
54
|
+
return new URL(document.referrer).origin;
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
function updateSafeAppOrigins(nextOrigins) {
|
|
64
|
+
if (areSameOrigins(safeAppOrigins, nextOrigins)) {
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
safeAppOrigins = nextOrigins;
|
|
68
|
+
safeAppOriginSet = new Set(safeAppOrigins);
|
|
69
|
+
notifySafeAppOriginListeners();
|
|
70
|
+
}
|
|
71
|
+
function configureSafeAppOrigins(nextOrigins) {
|
|
72
|
+
if (safeAppOriginsConfigured) {
|
|
73
|
+
throw new Error("Safe App origins already configured.");
|
|
74
|
+
}
|
|
75
|
+
safeAppOriginsConfigured = true;
|
|
76
|
+
updateSafeAppOrigins(nextOrigins);
|
|
77
|
+
}
|
|
78
|
+
function notifySafeAppOriginListeners() {
|
|
79
|
+
for (const listener of safeAppOriginListeners) {
|
|
80
|
+
listener();
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
function normalizeOrigins(origins) {
|
|
84
|
+
const normalized = [];
|
|
85
|
+
const seen = new Set();
|
|
86
|
+
for (const origin of origins) {
|
|
87
|
+
const normalizedOrigin = normalizeOrigin(origin);
|
|
88
|
+
if (!normalizedOrigin || seen.has(normalizedOrigin)) {
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
seen.add(normalizedOrigin);
|
|
92
|
+
normalized.push(normalizedOrigin);
|
|
93
|
+
}
|
|
94
|
+
return normalized;
|
|
95
|
+
}
|
|
96
|
+
function normalizeOrigin(origin) {
|
|
97
|
+
const trimmed = origin.trim();
|
|
98
|
+
if (!trimmed) {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
const candidate = ORIGIN_PROTOCOL_PATTERN.test(trimmed) ? trimmed : `https://${trimmed}`;
|
|
102
|
+
try {
|
|
103
|
+
const url = new URL(candidate);
|
|
104
|
+
if (url.protocol !== "https:" && url.protocol !== "http:") {
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
return url.origin;
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
function areSameOrigins(left, right) {
|
|
114
|
+
if (left.length !== right.length) {
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
for (let index = 0; index < left.length; index += 1) {
|
|
118
|
+
if (left[index] !== right[index]) {
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return true;
|
|
123
|
+
}
|
|
124
|
+
//# sourceMappingURL=safe-app-origins.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"safe-app-origins.js","sourceRoot":"","sources":["../../src/react-hooks/safe-app-origins.ts"],"names":[],"mappings":"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","sourcesContent":["\"use client\";\n\n/**\n * Known Safe App domains for iframe origin validation.\n * Includes the main Safe App and chain-specific Safe deployments.\n */\nexport const DEFAULT_SAFE_APP_ORIGINS = [\n // Main Safe domains\n \"https://app.safe.global\",\n \"https://gnosis-safe.io\",\n \"https://safe.global\",\n // Chain-specific Safe deployments\n \"https://safe.berachain.com\",\n \"https://safe.chiliz.com\",\n \"https://safe.lightlink.io\",\n \"https://safe.optimism.io\",\n] as const;\n\nconst ORIGIN_PROTOCOL_PATTERN = /^[a-z]+:\\/\\//i;\nlet safeAppOrigins = normalizeOrigins(DEFAULT_SAFE_APP_ORIGINS);\nlet safeAppOriginSet = new Set(safeAppOrigins);\nconst safeAppOriginListeners = new Set<() => void>();\nlet safeAppOriginsConfigured = false;\n\n/** Replace the Safe App origins list (one-time configuration). */\nexport function setSafeAppOrigins(origins: readonly string[]) {\n configureSafeAppOrigins(normalizeOrigins(origins));\n}\n\n/** Extend the Safe App origins list (one-time configuration). */\nexport function extendSafeAppOrigins(origins: readonly string[]) {\n configureSafeAppOrigins(normalizeOrigins([...safeAppOrigins, ...origins]));\n}\n\n/** Read the currently configured Safe App origins list. */\nexport function getSafeAppOrigins(): readonly string[] {\n return [...safeAppOrigins];\n}\n\n/** Subscribe to Safe App origin changes. */\nexport function subscribeSafeAppOrigins(listener: () => void): () => void {\n safeAppOriginListeners.add(listener);\n return () => {\n safeAppOriginListeners.delete(listener);\n };\n}\n\n/** Check if the parent browsing context origin is a Safe App domain. */\nexport function isValidSafeAppOrigin(): boolean {\n const origin = getAncestorOrigin();\n if (!origin) {\n return false;\n }\n return safeAppOriginSet.has(origin);\n}\n\n/** Check whether the app is running inside an iframe. */\nexport function isHostEmbedded(): boolean {\n return typeof window !== \"undefined\" && window.parent !== window;\n}\n\n/**\n * Get the origin of the parent browsing context.\n * Returns null if it cannot be determined due to cross-origin restrictions.\n */\nfunction getAncestorOrigin(): string | null {\n if (typeof window === \"undefined\") {\n return null;\n }\n\n try {\n return window.parent.location.origin;\n } catch {\n if (window.location.ancestorOrigins?.length) {\n return window.location.ancestorOrigins[0];\n }\n\n if (document.referrer) {\n try {\n return new URL(document.referrer).origin;\n } catch {\n return null;\n }\n }\n\n return null;\n }\n}\n\nfunction updateSafeAppOrigins(nextOrigins: string[]) {\n if (areSameOrigins(safeAppOrigins, nextOrigins)) {\n return;\n }\n\n safeAppOrigins = nextOrigins;\n safeAppOriginSet = new Set(safeAppOrigins);\n notifySafeAppOriginListeners();\n}\n\nfunction configureSafeAppOrigins(nextOrigins: string[]) {\n if (safeAppOriginsConfigured) {\n throw new Error(\"Safe App origins already configured.\");\n }\n\n safeAppOriginsConfigured = true;\n updateSafeAppOrigins(nextOrigins);\n}\n\nfunction notifySafeAppOriginListeners() {\n for (const listener of safeAppOriginListeners) {\n listener();\n }\n}\n\nfunction normalizeOrigins(origins: readonly string[]): string[] {\n const normalized: string[] = [];\n const seen = new Set<string>();\n\n for (const origin of origins) {\n const normalizedOrigin = normalizeOrigin(origin);\n if (!normalizedOrigin || seen.has(normalizedOrigin)) {\n continue;\n }\n seen.add(normalizedOrigin);\n normalized.push(normalizedOrigin);\n }\n\n return normalized;\n}\n\nfunction normalizeOrigin(origin: string): string | null {\n const trimmed = origin.trim();\n if (!trimmed) {\n return null;\n }\n\n const candidate = ORIGIN_PROTOCOL_PATTERN.test(trimmed) ? trimmed : `https://${trimmed}`;\n\n try {\n const url = new URL(candidate);\n if (url.protocol !== \"https:\" && url.protocol !== \"http:\") {\n return null;\n }\n return url.origin;\n } catch {\n return null;\n }\n}\n\nfunction areSameOrigins(left: readonly string[], right: readonly string[]): boolean {\n if (left.length !== right.length) {\n return false;\n }\n\n for (let index = 0; index < left.length; index += 1) {\n if (left[index] !== right[index]) {\n return false;\n }\n }\n\n return true;\n}\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-is-host-safe-app.d.ts","sourceRoot":"","sources":["../../src/react-hooks/use-is-host-safe-app.ts"],"names":[],"mappings":"AAsBA,wBAAgB,gBAAgB,IAAI,OAAO,CAS1C"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useSyncExternalStore } from "react";
|
|
3
|
+
import { isHostEmbedded, isValidSafeAppOrigin, subscribeSafeAppOrigins, } from "./safe-app-origins.js";
|
|
4
|
+
import { useIsSafeAppContext } from "./use-is-safe-app-context.js";
|
|
5
|
+
export function useIsHostSafeApp() {
|
|
6
|
+
const isSafeContext = useIsSafeAppContext();
|
|
7
|
+
const isSafeHost = useSyncExternalStore(subscribeSafeAppOrigins, getSafeHostSnapshot, getServerSnapshot);
|
|
8
|
+
return isSafeContext || isSafeHost;
|
|
9
|
+
}
|
|
10
|
+
function getSafeHostSnapshot() {
|
|
11
|
+
return isHostEmbedded() && isValidSafeAppOrigin();
|
|
12
|
+
}
|
|
13
|
+
function getServerSnapshot() {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
//# sourceMappingURL=use-is-host-safe-app.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-is-host-safe-app.js","sourceRoot":"","sources":["../../src/react-hooks/use-is-host-safe-app.ts"],"names":[],"mappings":"AAAA,YAAY,CAAC;AAEb,OAAO,EAAE,oBAAoB,EAAE,MAAM,OAAO,CAAC;AAC7C,OAAO,EACL,cAAc,EACd,oBAAoB,EACpB,uBAAuB,GACxB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EAAE,mBAAmB,EAAE,MAAM,8BAA8B,CAAC;AAcnE,MAAM,UAAU,gBAAgB;IAC9B,MAAM,aAAa,GAAG,mBAAmB,EAAE,CAAC;IAC5C,MAAM,UAAU,GAAG,oBAAoB,CACrC,uBAAuB,EACvB,mBAAmB,EACnB,iBAAiB,CAClB,CAAC;IAEF,OAAO,aAAa,IAAI,UAAU,CAAC;AACrC,CAAC;AAED,SAAS,mBAAmB;IAC1B,OAAO,cAAc,EAAE,IAAI,oBAAoB,EAAE,CAAC;AACpD,CAAC;AAED,SAAS,iBAAiB;IACxB,OAAO,KAAK,CAAC;AACf,CAAC","sourcesContent":["\"use client\";\n\nimport { useSyncExternalStore } from \"react\";\nimport {\n isHostEmbedded,\n isValidSafeAppOrigin,\n subscribeSafeAppOrigins,\n} from \"./safe-app-origins.js\";\nimport { useIsSafeAppContext } from \"./use-is-safe-app-context.js\";\n\n/**\n * Detect if the app is running within a Safe App context.\n *\n * This hook returns `true` when either:\n * - Safe Apps SDK confirms the Safe context (async, most reliable), or\n * - The app is embedded in a Safe App-origin iframe (sync check).\n *\n * Notes:\n * - This answers “is the host Safe App?” and will return true even without a wallet.\n * - For full wallet detection heuristics, use `useIsSafeMultisigWallet`.\n * - For SDK-only checks, use `useIsSafeAppContext`.\n */\nexport function useIsHostSafeApp(): boolean {\n const isSafeContext = useIsSafeAppContext();\n const isSafeHost = useSyncExternalStore(\n subscribeSafeAppOrigins,\n getSafeHostSnapshot,\n getServerSnapshot\n );\n\n return isSafeContext || isSafeHost;\n}\n\nfunction getSafeHostSnapshot(): boolean {\n return isHostEmbedded() && isValidSafeAppOrigin();\n}\n\nfunction getServerSnapshot(): boolean {\n return false;\n}\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-is-safe-app-context.d.ts","sourceRoot":"","sources":["../../src/react-hooks/use-is-safe-app-context.ts"],"names":[],"mappings":"AAuCA,wBAAgB,mBAAmB,IAAI,OAAO,CA6C7C"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useEffect, useState } from "react";
|
|
3
|
+
const SAFE_SDK_TIMEOUT = 3000;
|
|
4
|
+
export function useIsSafeAppContext() {
|
|
5
|
+
const [isSafe, setIsSafe] = useState(false);
|
|
6
|
+
useEffect(() => {
|
|
7
|
+
if (typeof window === "undefined") {
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
let cancelled = false;
|
|
11
|
+
const timeout = setTimeout(() => {
|
|
12
|
+
if (!cancelled) {
|
|
13
|
+
setIsSafe(false);
|
|
14
|
+
}
|
|
15
|
+
}, SAFE_SDK_TIMEOUT);
|
|
16
|
+
import("@safe-global/safe-apps-sdk")
|
|
17
|
+
.then(({ default: SafeAppsSDK }) => {
|
|
18
|
+
const sdk = new SafeAppsSDK();
|
|
19
|
+
return sdk.safe.getInfo();
|
|
20
|
+
})
|
|
21
|
+
.then(() => {
|
|
22
|
+
if (!cancelled) {
|
|
23
|
+
setIsSafe(true);
|
|
24
|
+
}
|
|
25
|
+
})
|
|
26
|
+
.catch(() => {
|
|
27
|
+
if (!cancelled) {
|
|
28
|
+
setIsSafe(false);
|
|
29
|
+
}
|
|
30
|
+
})
|
|
31
|
+
.finally(() => {
|
|
32
|
+
clearTimeout(timeout);
|
|
33
|
+
});
|
|
34
|
+
return () => {
|
|
35
|
+
cancelled = true;
|
|
36
|
+
clearTimeout(timeout);
|
|
37
|
+
};
|
|
38
|
+
}, []);
|
|
39
|
+
return isSafe;
|
|
40
|
+
}
|
|
41
|
+
//# sourceMappingURL=use-is-safe-app-context.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-is-safe-app-context.js","sourceRoot":"","sources":["../../src/react-hooks/use-is-safe-app-context.ts"],"names":[],"mappings":"AAAA,YAAY,CAAC;AAEb,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AAM5C,MAAM,gBAAgB,GAAG,IAAI,CAAC;AA+B9B,MAAM,UAAU,mBAAmB;IACjC,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC;IAE5C,SAAS,CAAC,GAAG,EAAE;QAEb,IAAI,OAAO,MAAM,KAAK,WAAW,EAAE,CAAC;YAClC,OAAO;QACT,CAAC;QAED,IAAI,SAAS,GAAG,KAAK,CAAC;QAGtB,MAAM,OAAO,GAAG,UAAU,CAAC,GAAG,EAAE;YAC9B,IAAI,CAAC,SAAS,EAAE,CAAC;gBACf,SAAS,CAAC,KAAK,CAAC,CAAC;YACnB,CAAC;QACH,CAAC,EAAE,gBAAgB,CAAC,CAAC;QAGrB,MAAM,CAAC,4BAA4B,CAAC;aACjC,IAAI,CAAC,CAAC,EAAE,OAAO,EAAE,WAAW,EAAE,EAAE,EAAE;YACjC,MAAM,GAAG,GAAG,IAAI,WAAW,EAAE,CAAC;YAC9B,OAAO,GAAG,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC;QAC5B,CAAC,CAAC;aACD,IAAI,CAAC,GAAG,EAAE;YACT,IAAI,CAAC,SAAS,EAAE,CAAC;gBACf,SAAS,CAAC,IAAI,CAAC,CAAC;YAClB,CAAC;QACH,CAAC,CAAC;aACD,KAAK,CAAC,GAAG,EAAE;YACV,IAAI,CAAC,SAAS,EAAE,CAAC;gBACf,SAAS,CAAC,KAAK,CAAC,CAAC;YACnB,CAAC;QACH,CAAC,CAAC;aACD,OAAO,CAAC,GAAG,EAAE;YACZ,YAAY,CAAC,OAAO,CAAC,CAAC;QACxB,CAAC,CAAC,CAAC;QAEL,OAAO,GAAG,EAAE;YACV,SAAS,GAAG,IAAI,CAAC;YACjB,YAAY,CAAC,OAAO,CAAC,CAAC;QACxB,CAAC,CAAC;IACJ,CAAC,EAAE,EAAE,CAAC,CAAC;IAEP,OAAO,MAAM,CAAC;AAChB,CAAC","sourcesContent":["\"use client\";\n\nimport { useEffect, useState } from \"react\";\n\n/**\n * Timeout for Safe SDK detection in milliseconds.\n * Safe Apps SDK uses postMessage which can hang if not in Safe context.\n */\nconst SAFE_SDK_TIMEOUT = 3000;\n\n/**\n * Detect if the app is running in a Safe App context using the Safe Apps SDK.\n *\n * This hook asynchronously checks with the Safe Apps SDK to determine if\n * we're running inside a Safe App iframe. More reliable than iframe origin\n * detection because:\n * - Uses postMessage (works cross-origin)\n * - Gets definitive answer from Safe\n * - Includes timeout to prevent hangs\n *\n * @returns true if running in Safe App context, false otherwise\n *\n * Notes:\n * - This only verifies the Safe Apps SDK context (async).\n * - For a host-based sync check, use `useIsHostSafeApp`.\n * - For a full wallet detection heuristic, use `useIsSafeMultisigWallet`.\n *\n * @example\n * ```tsx\n * function MyComponent() {\n * const isSafeContext = useIsSafeAppContext();\n *\n * if (isSafeContext) {\n * return <p>Running in Safe App</p>;\n * }\n * return <p>Regular browser context</p>;\n * }\n * ```\n */\nexport function useIsSafeAppContext(): boolean {\n const [isSafe, setIsSafe] = useState(false);\n\n useEffect(() => {\n // SSR guard\n if (typeof window === \"undefined\") {\n return;\n }\n\n let cancelled = false;\n\n // Timeout fallback - resolve to false if SDK doesn't respond\n const timeout = setTimeout(() => {\n if (!cancelled) {\n setIsSafe(false);\n }\n }, SAFE_SDK_TIMEOUT);\n\n // Dynamically import SDK to keep it optional\n import(\"@safe-global/safe-apps-sdk\")\n .then(({ default: SafeAppsSDK }) => {\n const sdk = new SafeAppsSDK();\n return sdk.safe.getInfo();\n })\n .then(() => {\n if (!cancelled) {\n setIsSafe(true);\n }\n })\n .catch(() => {\n if (!cancelled) {\n setIsSafe(false);\n }\n })\n .finally(() => {\n clearTimeout(timeout);\n });\n\n return () => {\n cancelled = true;\n clearTimeout(timeout);\n };\n }, []);\n\n return isSafe;\n}\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-is-safe-multisig-wallet.d.ts","sourceRoot":"","sources":["../../src/react-hooks/use-is-safe-multisig-wallet.ts"],"names":[],"mappings":"AA0BA,wBAAgB,uBAAuB,IAAI,OAAO,CAuBjD"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useSyncExternalStore } from "react";
|
|
3
|
+
import { useAccount } from "wagmi";
|
|
4
|
+
import { isHostEmbedded, isValidSafeAppOrigin, subscribeSafeAppOrigins, } from "./safe-app-origins.js";
|
|
5
|
+
import { useIsSafeAppContext } from "./use-is-safe-app-context.js";
|
|
6
|
+
export function useIsSafeMultisigWallet() {
|
|
7
|
+
const { connector, isConnected } = useAccount();
|
|
8
|
+
const isSafeContext = useIsSafeAppContext();
|
|
9
|
+
const isSafeIframe = useSyncExternalStore(subscribeSafeAppOrigins, getSafeIframeSnapshot, getServerSnapshot);
|
|
10
|
+
if (isSafeContext) {
|
|
11
|
+
return true;
|
|
12
|
+
}
|
|
13
|
+
if (isConnected && connector?.id === "safe") {
|
|
14
|
+
return true;
|
|
15
|
+
}
|
|
16
|
+
return isSafeIframe;
|
|
17
|
+
}
|
|
18
|
+
function getSafeIframeSnapshot() {
|
|
19
|
+
return isHostEmbedded() && isValidSafeAppOrigin();
|
|
20
|
+
}
|
|
21
|
+
function getServerSnapshot() {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
//# sourceMappingURL=use-is-safe-multisig-wallet.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-is-safe-multisig-wallet.js","sourceRoot":"","sources":["../../src/react-hooks/use-is-safe-multisig-wallet.ts"],"names":[],"mappings":"AAAA,YAAY,CAAC;AAEb,OAAO,EAAE,oBAAoB,EAAE,MAAM,OAAO,CAAC;AAC7C,OAAO,EAAE,UAAU,EAAE,MAAM,OAAO,CAAC;AACnC,OAAO,EACL,cAAc,EACd,oBAAoB,EACpB,uBAAuB,GACxB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EAAE,mBAAmB,EAAE,MAAM,8BAA8B,CAAC;AAiBnE,MAAM,UAAU,uBAAuB;IACrC,MAAM,EAAE,SAAS,EAAE,WAAW,EAAE,GAAG,UAAU,EAAE,CAAC;IAChD,MAAM,aAAa,GAAG,mBAAmB,EAAE,CAAC;IAG5C,MAAM,YAAY,GAAG,oBAAoB,CACvC,uBAAuB,EACvB,qBAAqB,EACrB,iBAAiB,CAClB,CAAC;IAGF,IAAI,aAAa,EAAE,CAAC;QAClB,OAAO,IAAI,CAAC;IACd,CAAC;IAGD,IAAI,WAAW,IAAI,SAAS,EAAE,EAAE,KAAK,MAAM,EAAE,CAAC;QAC5C,OAAO,IAAI,CAAC;IACd,CAAC;IAGD,OAAO,YAAY,CAAC;AACtB,CAAC;AAED,SAAS,qBAAqB;IAC5B,OAAO,cAAc,EAAE,IAAI,oBAAoB,EAAE,CAAC;AACpD,CAAC;AAED,SAAS,iBAAiB;IACxB,OAAO,KAAK,CAAC;AACf,CAAC","sourcesContent":["\"use client\";\n\nimport { useSyncExternalStore } from \"react\";\nimport { useAccount } from \"wagmi\";\nimport {\n isHostEmbedded,\n isValidSafeAppOrigin,\n subscribeSafeAppOrigins,\n} from \"./safe-app-origins.js\";\nimport { useIsSafeAppContext } from \"./use-is-safe-app-context.js\";\n\n/**\n * Detect if the connected wallet is a Safe multisig wallet.\n *\n * Detection strategy (in order of reliability):\n * 1. Safe Apps SDK detection via postMessage (most reliable, works cross-origin)\n * 2. Wagmi connector ID check\n * 3. Iframe origin validation (fallback, may fail cross-origin)\n *\n * @returns true if wallet is a Safe multisig\n *\n * Notes:\n * - This is a broader heuristic: SDK context, connector ID, then host origin.\n * - For just SDK context, use `useIsSafeAppContext`.\n * - For a sync host-only check, use `useIsHostSafeApp`.\n */\nexport function useIsSafeMultisigWallet(): boolean {\n const { connector, isConnected } = useAccount();\n const isSafeContext = useIsSafeAppContext();\n\n // Method 3: Iframe origin fallback (may fail cross-origin)\n const isSafeIframe = useSyncExternalStore(\n subscribeSafeAppOrigins,\n getSafeIframeSnapshot,\n getServerSnapshot\n );\n\n // Method 1: Safe Apps SDK (most reliable - uses postMessage)\n if (isSafeContext) {\n return true;\n }\n\n // Method 2: Connected via Safe connector\n if (isConnected && connector?.id === \"safe\") {\n return true;\n }\n\n // Method 3: Fallback\n return isSafeIframe;\n}\n\nfunction getSafeIframeSnapshot(): boolean {\n return isHostEmbedded() && isValidSafeAppOrigin();\n}\n\nfunction getServerSnapshot(): boolean {\n return false;\n}\n"]}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { Opts } from "@safe-global/safe-apps-sdk";
|
|
2
|
+
import { Effect } from "effect";
|
|
3
|
+
import { SafeAppsSdkUnavailableError } from "./errors.js";
|
|
4
|
+
export type SafeAppsSdkConfig = Opts;
|
|
5
|
+
export declare const loadSafeSdk: (config?: SafeAppsSdkConfig) => Effect.Effect<import("@safe-global/safe-apps-sdk").default, SafeAppsSdkUnavailableError, never>;
|
|
6
|
+
export type { SafeAppsSDKInstance } from "./internal/sdk-types.js";
|
|
7
|
+
//# sourceMappingURL=adapter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../../src/safe/adapter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,4BAA4B,CAAC;AACvD,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAChC,OAAO,EAAE,2BAA2B,EAAE,MAAM,aAAa,CAAC;AAG1D,MAAM,MAAM,iBAAiB,GAAG,IAAI,CAAC;AAGrC,eAAO,MAAM,WAAW,GAAI,SAAS,iBAAiB,oGAYlD,CAAC;AAGL,YAAY,EAAE,mBAAmB,EAAE,MAAM,yBAAyB,CAAC"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Effect } from "effect";
|
|
2
|
+
import { SafeAppsSdkUnavailableError } from "./errors.js";
|
|
3
|
+
export const loadSafeSdk = (config) => Effect.tryPromise({
|
|
4
|
+
catch: (cause) => new SafeAppsSdkUnavailableError({
|
|
5
|
+
cause,
|
|
6
|
+
message: "Failed to load @safe-global/safe-apps-sdk. Ensure it is installed: bun add @safe-global/safe-apps-sdk",
|
|
7
|
+
}),
|
|
8
|
+
try: async () => {
|
|
9
|
+
const { default: SafeAppsSDK } = await import("@safe-global/safe-apps-sdk");
|
|
10
|
+
return new SafeAppsSDK(config);
|
|
11
|
+
},
|
|
12
|
+
});
|
|
13
|
+
//# sourceMappingURL=adapter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"adapter.js","sourceRoot":"","sources":["../../src/safe/adapter.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAChC,OAAO,EAAE,2BAA2B,EAAE,MAAM,aAAa,CAAC;AAM1D,MAAM,CAAC,MAAM,WAAW,GAAG,CAAC,MAA0B,EAAE,EAAE,CACxD,MAAM,CAAC,UAAU,CAAC;IAChB,KAAK,EAAE,CAAC,KAAK,EAAE,EAAE,CACf,IAAI,2BAA2B,CAAC;QAC9B,KAAK;QACL,OAAO,EACL,uGAAuG;KAC1G,CAAC;IACJ,GAAG,EAAE,KAAK,IAAI,EAAE;QACd,MAAM,EAAE,OAAO,EAAE,WAAW,EAAE,GAAG,MAAM,MAAM,CAAC,4BAA4B,CAAC,CAAC;QAC5E,OAAO,IAAI,WAAW,CAAC,MAAM,CAAC,CAAC;IACjC,CAAC;CACF,CAAC,CAAC","sourcesContent":["import type { Opts } from \"@safe-global/safe-apps-sdk\";\nimport { Effect } from \"effect\";\nimport { SafeAppsSdkUnavailableError } from \"./errors.js\";\n\n/** Configuration for SDK initialization */\nexport type SafeAppsSdkConfig = Opts;\n\n/** Load SDK dynamically to avoid SSR import issues. */\nexport const loadSafeSdk = (config?: SafeAppsSdkConfig) =>\n Effect.tryPromise({\n catch: (cause) =>\n new SafeAppsSdkUnavailableError({\n cause,\n message:\n \"Failed to load @safe-global/safe-apps-sdk. Ensure it is installed: bun add @safe-global/safe-apps-sdk\",\n }),\n try: async () => {\n const { default: SafeAppsSDK } = await import(\"@safe-global/safe-apps-sdk\");\n return new SafeAppsSDK(config);\n },\n });\n\n/** Type for the loaded SDK instance - uses a minimal structural type. */\nexport type { SafeAppsSDKInstance } from \"./internal/sdk-types.js\";\n"]}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { ClientNotFoundError } from "@prb/effect-evm/core";
|
|
2
|
+
import { PublicClientService } from "@prb/effect-evm/core";
|
|
3
|
+
import { Effect, Schema } from "effect";
|
|
4
|
+
import type { Address, Hex } from "viem";
|
|
5
|
+
declare const SafeMultisigDetectionError_base: Schema.TaggedErrorClass<SafeMultisigDetectionError, "SafeMultisigDetectionError", {
|
|
6
|
+
readonly _tag: Schema.tag<"SafeMultisigDetectionError">;
|
|
7
|
+
} & {
|
|
8
|
+
address: typeof Schema.String;
|
|
9
|
+
cause: Schema.optional<typeof Schema.Unknown>;
|
|
10
|
+
message: typeof Schema.String;
|
|
11
|
+
}>;
|
|
12
|
+
export declare class SafeMultisigDetectionError extends SafeMultisigDetectionError_base {
|
|
13
|
+
}
|
|
14
|
+
export type SafeDetectionParams = {
|
|
15
|
+
address: Address;
|
|
16
|
+
chainId: number;
|
|
17
|
+
};
|
|
18
|
+
export type SafeDetectionResult = {
|
|
19
|
+
isSafe: boolean;
|
|
20
|
+
proxyHash: Hex | undefined;
|
|
21
|
+
singletonAddress: Address | undefined;
|
|
22
|
+
singletonHash: Hex | undefined;
|
|
23
|
+
};
|
|
24
|
+
export declare function isSafeMultisig(params: SafeDetectionParams): Effect.Effect<SafeDetectionResult, SafeMultisigDetectionError | ClientNotFoundError, PublicClientService>;
|
|
25
|
+
export {};
|
|
26
|
+
//# sourceMappingURL=detection.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"detection.d.ts","sourceRoot":"","sources":["../../src/safe/detection.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAChE,OAAO,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAC3D,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AACxC,OAAO,KAAK,EAAE,OAAO,EAAE,GAAG,EAAE,MAAM,MAAM,CAAC;;;;;;;;AAwDzC,qBAAa,0BAA2B,SAAQ,+BAO/C;CAAG;AAoBJ,MAAM,MAAM,mBAAmB,GAAG;IAEhC,OAAO,EAAE,OAAO,CAAC;IAEjB,OAAO,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG;IAEhC,MAAM,EAAE,OAAO,CAAC;IAEhB,SAAS,EAAE,GAAG,GAAG,SAAS,CAAC;IAE3B,gBAAgB,EAAE,OAAO,GAAG,SAAS,CAAC;IAEtC,aAAa,EAAE,GAAG,GAAG,SAAS,CAAC;CAChC,CAAC;AA8BF,wBAAgB,cAAc,CAC5B,MAAM,EAAE,mBAAmB,GAC1B,MAAM,CAAC,MAAM,CACd,mBAAmB,EACnB,0BAA0B,GAAG,mBAAmB,EAChD,mBAAmB,CACpB,CAuFA"}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { PublicClientService } from "@prb/effect-evm/core";
|
|
2
|
+
import { Effect, Schema } from "effect";
|
|
3
|
+
import { keccak256 } from "viem";
|
|
4
|
+
const KNOWN_SAFE_PROXY_HASHES = new Set([
|
|
5
|
+
"0xb89c1b3bdf2cf8827818646bce9a8f6e372885f8c55e5c07acbd307cb133b000",
|
|
6
|
+
"0xd7d408ebcd99b2b70be43e20253d6d92a8ea8fab29bd3be7f55b10032331fb4c",
|
|
7
|
+
]);
|
|
8
|
+
const KNOWN_SAFE_SINGLETON_HASHES = new Set([
|
|
9
|
+
"0xe1f1593df76e69abc2d692792c80f329457551d5e83dde597546a1d58764da80",
|
|
10
|
+
"0x56b8be58b5ad629a621593a2e5e5e8e9a28408dc06e95597497b303902772e45",
|
|
11
|
+
"0x2ae2d1231f0d754a7fa4f5e5d0e5554085e1b500d8e09f95aaaaa3f49c0db922",
|
|
12
|
+
"0xbba688fbdb21ad2bb58bc320638b43d94e7d100f6f3ebaab0a4e4de6304b1c2e",
|
|
13
|
+
"0x21842597390c4c6e3c1239e434a682b054bd9548eee5e9b1d6a4482731023c0f",
|
|
14
|
+
"0x1fe2df852ba3299d6534ef416eefa406e56ced995bca886ab7a553e6d0c5e1c4",
|
|
15
|
+
"0xb1f926978a0f44a2c0ec8fe822418ae969bd8c3f18d61e5103100339894f81ff",
|
|
16
|
+
"0xdda019cbd7c867a533a2a86e5c53434fdc50b13122b5a5ddb4a8df61b31c20f2",
|
|
17
|
+
"0x180193227186ccb85316c94db1f0d156ed932b14712cfaac78901899178572dc",
|
|
18
|
+
]);
|
|
19
|
+
export class SafeMultisigDetectionError extends Schema.TaggedError()("SafeMultisigDetectionError", {
|
|
20
|
+
address: Schema.String,
|
|
21
|
+
cause: Schema.optional(Schema.Unknown),
|
|
22
|
+
message: Schema.String,
|
|
23
|
+
}) {
|
|
24
|
+
}
|
|
25
|
+
const masterCopyAbi = [
|
|
26
|
+
{
|
|
27
|
+
inputs: [],
|
|
28
|
+
name: "masterCopy",
|
|
29
|
+
outputs: [{ name: "", type: "address" }],
|
|
30
|
+
stateMutability: "view",
|
|
31
|
+
type: "function",
|
|
32
|
+
},
|
|
33
|
+
];
|
|
34
|
+
export function isSafeMultisig(params) {
|
|
35
|
+
return Effect.gen(function* () {
|
|
36
|
+
const { address, chainId } = params;
|
|
37
|
+
const publicClientService = yield* PublicClientService;
|
|
38
|
+
const client = yield* publicClientService.get(chainId);
|
|
39
|
+
const proxyBytecode = yield* Effect.tryPromise({
|
|
40
|
+
catch: (cause) => new SafeMultisigDetectionError({
|
|
41
|
+
address,
|
|
42
|
+
cause,
|
|
43
|
+
message: `Failed to get bytecode for ${address}`,
|
|
44
|
+
}),
|
|
45
|
+
try: () => client.getCode({ address }),
|
|
46
|
+
});
|
|
47
|
+
if (!proxyBytecode || proxyBytecode === "0x") {
|
|
48
|
+
return {
|
|
49
|
+
isSafe: false,
|
|
50
|
+
proxyHash: undefined,
|
|
51
|
+
singletonAddress: undefined,
|
|
52
|
+
singletonHash: undefined,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
const proxyHash = keccak256(proxyBytecode);
|
|
56
|
+
if (!KNOWN_SAFE_PROXY_HASHES.has(proxyHash)) {
|
|
57
|
+
return {
|
|
58
|
+
isSafe: false,
|
|
59
|
+
proxyHash,
|
|
60
|
+
singletonAddress: undefined,
|
|
61
|
+
singletonHash: undefined,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
const singletonAddress = yield* Effect.tryPromise({
|
|
65
|
+
catch: (cause) => new SafeMultisigDetectionError({
|
|
66
|
+
address,
|
|
67
|
+
cause,
|
|
68
|
+
message: `Failed to call masterCopy() on ${address}`,
|
|
69
|
+
}),
|
|
70
|
+
try: () => client.readContract({
|
|
71
|
+
abi: masterCopyAbi,
|
|
72
|
+
address,
|
|
73
|
+
functionName: "masterCopy",
|
|
74
|
+
}),
|
|
75
|
+
});
|
|
76
|
+
const singletonBytecode = yield* Effect.tryPromise({
|
|
77
|
+
catch: (cause) => new SafeMultisigDetectionError({
|
|
78
|
+
address,
|
|
79
|
+
cause,
|
|
80
|
+
message: `Failed to get bytecode for singleton ${singletonAddress}`,
|
|
81
|
+
}),
|
|
82
|
+
try: () => client.getCode({ address: singletonAddress }),
|
|
83
|
+
});
|
|
84
|
+
if (!singletonBytecode || singletonBytecode === "0x") {
|
|
85
|
+
return {
|
|
86
|
+
isSafe: false,
|
|
87
|
+
proxyHash,
|
|
88
|
+
singletonAddress,
|
|
89
|
+
singletonHash: undefined,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
const singletonHash = keccak256(singletonBytecode);
|
|
93
|
+
const isSafe = KNOWN_SAFE_SINGLETON_HASHES.has(singletonHash);
|
|
94
|
+
return {
|
|
95
|
+
isSafe,
|
|
96
|
+
proxyHash,
|
|
97
|
+
singletonAddress,
|
|
98
|
+
singletonHash,
|
|
99
|
+
};
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
//# sourceMappingURL=detection.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"detection.js","sourceRoot":"","sources":["../../src/safe/detection.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Safe multisig detection using EXTCODEHASH comparisons.\n *\n * Determines if an address is a Safe multisig by comparing bytecode hashes\n * against known Safe proxy and singleton hashes.\n *\n * @see https://github.com/safe-fndn/safe-smart-account/issues/714\n */\n\nimport type { ClientNotFoundError } from \"@prb/effect-evm/core\";\nimport { PublicClientService } from \"@prb/effect-evm/core\";\nimport { Effect, Schema } from \"effect\";\nimport type { Address, Hex } from \"viem\";\nimport { keccak256 } from \"viem\";\n\n// -----------------------------------------------------------------------------\n// Known Safe bytecode hashes\n// -----------------------------------------------------------------------------\n\n/**\n * Known Safe proxy bytecode hashes (keccak256 of deployed bytecode).\n *\n * These are the proxy contracts that delegate to Safe singletons.\n * All Safe accounts use one of these proxy implementations.\n *\n * @see https://github.com/safe-global/safe-deployments\n */\nconst KNOWN_SAFE_PROXY_HASHES: ReadonlySet<Hex> = new Set([\n // SafeProxy v1.3.0\n \"0xb89c1b3bdf2cf8827818646bce9a8f6e372885f8c55e5c07acbd307cb133b000\",\n // SafeProxy v1.4.1\n \"0xd7d408ebcd99b2b70be43e20253d6d92a8ea8fab29bd3be7f55b10032331fb4c\",\n] as Hex[]);\n\n/**\n * Known Safe singleton (implementation) bytecode hashes.\n *\n * These are the actual Safe contract implementations that proxies delegate to.\n *\n * Derivation: `cast keccak $(cast code <address> --rpc-url <mainnet>)`\n *\n * @see https://github.com/safe-global/safe-deployments\n */\nconst KNOWN_SAFE_SINGLETON_HASHES: ReadonlySet<Hex> = new Set([\n // Safe v1.0.0\n \"0xe1f1593df76e69abc2d692792c80f329457551d5e83dde597546a1d58764da80\",\n // Safe v1.1.1\n \"0x56b8be58b5ad629a621593a2e5e5e8e9a28408dc06e95597497b303902772e45\",\n // Safe v1.2.0\n \"0x2ae2d1231f0d754a7fa4f5e5d0e5554085e1b500d8e09f95aaaaa3f49c0db922\",\n // Safe v1.3.0\n \"0xbba688fbdb21ad2bb58bc320638b43d94e7d100f6f3ebaab0a4e4de6304b1c2e\",\n // Safe v1.3.0 L2\n \"0x21842597390c4c6e3c1239e434a682b054bd9548eee5e9b1d6a4482731023c0f\",\n // Safe v1.4.1\n \"0x1fe2df852ba3299d6534ef416eefa406e56ced995bca886ab7a553e6d0c5e1c4\",\n // Safe v1.4.1 L2\n \"0xb1f926978a0f44a2c0ec8fe822418ae969bd8c3f18d61e5103100339894f81ff\",\n // Safe v1.5.0\n \"0xdda019cbd7c867a533a2a86e5c53434fdc50b13122b5a5ddb4a8df61b31c20f2\",\n // Safe v1.5.0 L2\n \"0x180193227186ccb85316c94db1f0d156ed932b14712cfaac78901899178572dc\",\n] as Hex[]);\n\n// -----------------------------------------------------------------------------\n// Errors\n// -----------------------------------------------------------------------------\n\nexport class SafeMultisigDetectionError extends Schema.TaggedError<SafeMultisigDetectionError>()(\n \"SafeMultisigDetectionError\",\n {\n address: Schema.String,\n cause: Schema.optional(Schema.Unknown),\n message: Schema.String,\n }\n) {}\n\n// -----------------------------------------------------------------------------\n// ABI for masterCopy() call\n// -----------------------------------------------------------------------------\n\nconst masterCopyAbi = [\n {\n inputs: [],\n name: \"masterCopy\",\n outputs: [{ name: \"\", type: \"address\" }],\n stateMutability: \"view\",\n type: \"function\",\n },\n] as const;\n\n// -----------------------------------------------------------------------------\n// Detection function\n// -----------------------------------------------------------------------------\n\nexport type SafeDetectionParams = {\n /** The address to check */\n address: Address;\n /** The chain ID to query */\n chainId: number;\n};\n\nexport type SafeDetectionResult = {\n /** Whether the address appears to be a Safe multisig */\n isSafe: boolean;\n /** The proxy bytecode hash (if contract exists) */\n proxyHash: Hex | undefined;\n /** The singleton address (if Safe proxy detected) */\n singletonAddress: Address | undefined;\n /** The singleton bytecode hash (if singleton found) */\n singletonHash: Hex | undefined;\n};\n\n/**\n * Check if an address is a Safe multisig by comparing EXTCODEHASH values.\n *\n * This function:\n * 1. Gets the bytecode of the target address and computes its hash\n * 2. Checks if the hash matches a known Safe proxy\n * 3. If proxy matches, calls `masterCopy()` to get the singleton address\n * 4. Verifies the singleton's bytecode hash matches a known Safe singleton\n *\n * @example\n * ```typescript\n * const result = yield* isSafeMultisig({\n * address: \"0x...\",\n * chainId: 1,\n * });\n *\n * if (result.isSafe) {\n * console.log(\"This is a Safe multisig!\");\n * }\n * ```\n *\n * @remarks\n * This is a heuristic check, not a guarantee. Any contract can technically\n * deploy bytecode that matches Safe's, though this is extremely unlikely\n * in practice. For most use cases, this provides sufficient confidence.\n *\n * @see https://github.com/safe-fndn/safe-smart-account/issues/714\n */\nexport function isSafeMultisig(\n params: SafeDetectionParams\n): Effect.Effect<\n SafeDetectionResult,\n SafeMultisigDetectionError | ClientNotFoundError,\n PublicClientService\n> {\n return Effect.gen(function* () {\n const { address, chainId } = params;\n const publicClientService = yield* PublicClientService;\n const client = yield* publicClientService.get(chainId);\n\n // 1. Get proxy bytecode\n const proxyBytecode = yield* Effect.tryPromise({\n catch: (cause) =>\n new SafeMultisigDetectionError({\n address,\n cause,\n message: `Failed to get bytecode for ${address}`,\n }),\n try: () => client.getCode({ address }),\n });\n\n // No bytecode = EOA or empty contract\n if (!proxyBytecode || proxyBytecode === \"0x\") {\n return {\n isSafe: false,\n proxyHash: undefined,\n singletonAddress: undefined,\n singletonHash: undefined,\n };\n }\n\n // 2. Compute proxy hash\n const proxyHash = keccak256(proxyBytecode);\n\n // 3. Check if proxy hash matches known Safe proxies\n if (!KNOWN_SAFE_PROXY_HASHES.has(proxyHash)) {\n return {\n isSafe: false,\n proxyHash,\n singletonAddress: undefined,\n singletonHash: undefined,\n };\n }\n\n // 4. Call masterCopy() to get singleton address\n const singletonAddress = yield* Effect.tryPromise({\n catch: (cause) =>\n new SafeMultisigDetectionError({\n address,\n cause,\n message: `Failed to call masterCopy() on ${address}`,\n }),\n try: () =>\n client.readContract({\n abi: masterCopyAbi,\n address,\n functionName: \"masterCopy\",\n }),\n });\n\n // 5. Get singleton bytecode\n const singletonBytecode = yield* Effect.tryPromise({\n catch: (cause) =>\n new SafeMultisigDetectionError({\n address,\n cause,\n message: `Failed to get bytecode for singleton ${singletonAddress}`,\n }),\n try: () => client.getCode({ address: singletonAddress }),\n });\n\n if (!singletonBytecode || singletonBytecode === \"0x\") {\n return {\n isSafe: false,\n proxyHash,\n singletonAddress,\n singletonHash: undefined,\n };\n }\n\n // 6. Compute and check singleton hash\n const singletonHash = keccak256(singletonBytecode);\n const isSafe = KNOWN_SAFE_SINGLETON_HASHES.has(singletonHash);\n\n return {\n isSafe,\n proxyHash,\n singletonAddress,\n singletonHash,\n };\n });\n}\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"detection.test.integration.d.ts","sourceRoot":"","sources":["../../src/safe/detection.test.integration.ts"],"names":[],"mappings":""}
|