@prb/effect-evm-safe 2.0.0 → 2.0.2
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/CHANGELOG.md +18 -0
- package/dist/react-hooks/safe-app-origins.d.ts.map +1 -1
- package/dist/react-hooks/safe-app-origins.js +2 -10
- package/dist/react-hooks/safe-app-origins.js.map +1 -1
- package/dist/safe/tx-lifecycle.d.ts +5 -26
- package/dist/safe/tx-lifecycle.d.ts.map +1 -1
- package/dist/safe/tx-lifecycle.js +65 -46
- package/dist/safe/tx-lifecycle.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -9,6 +9,24 @@ The format is based on [Common Changelog](https://common-changelog.org/).
|
|
|
9
9
|
[1.1.0]: https://github.com/PaulRBerg/prb-effect/releases/tag/%40prb%2Feffect-evm-safe%401.1.0
|
|
10
10
|
[1.1.1]: https://github.com/PaulRBerg/prb-effect/releases/tag/%40prb%2Feffect-evm-safe%401.1.1
|
|
11
11
|
[2.0.0]: https://github.com/PaulRBerg/prb-effect/releases/tag/%40prb%2Feffect-evm-safe%402.0.0
|
|
12
|
+
[2.0.1]: https://github.com/PaulRBerg/prb-effect/releases/tag/%40prb%2Feffect-evm-safe%402.0.1
|
|
13
|
+
[2.0.2]: https://github.com/PaulRBerg/prb-effect/releases/tag/%40prb%2Feffect-evm-safe%402.0.2
|
|
14
|
+
|
|
15
|
+
## [2.0.2] - 2026-02-18
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
|
|
19
|
+
- Allow `setSafeAppOrigins` and `extendSafeAppOrigins` to be called multiple times by removing the one-time
|
|
20
|
+
configuration guard
|
|
21
|
+
|
|
22
|
+
## [2.0.1] - 2026-02-18
|
|
23
|
+
|
|
24
|
+
### Changed
|
|
25
|
+
|
|
26
|
+
- Enrich `waitForSafeMultisigTx` timeout `queued` result with confirmation progress: `confirmations`,
|
|
27
|
+
`confirmationsRequired`, and normalized `lastStatus`
|
|
28
|
+
- Preserve last known Safe API status during polling so timeout responses include actionable state
|
|
29
|
+
- Reuse shared status mapping between `waitForSafeMultisigTx` and `getSafeMultisigTxStatus`
|
|
12
30
|
|
|
13
31
|
## [2.0.0] - 2026-02-17
|
|
14
32
|
|
|
@@ -1 +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;
|
|
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;AAQX,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"}
|
|
@@ -12,12 +12,11 @@ const ORIGIN_PROTOCOL_PATTERN = /^[a-z]+:\/\//i;
|
|
|
12
12
|
let safeAppOrigins = normalizeOrigins(DEFAULT_SAFE_APP_ORIGINS);
|
|
13
13
|
let safeAppOriginSet = new Set(safeAppOrigins);
|
|
14
14
|
const safeAppOriginListeners = new Set();
|
|
15
|
-
let safeAppOriginsConfigured = false;
|
|
16
15
|
export function setSafeAppOrigins(origins) {
|
|
17
|
-
|
|
16
|
+
updateSafeAppOrigins(normalizeOrigins(origins));
|
|
18
17
|
}
|
|
19
18
|
export function extendSafeAppOrigins(origins) {
|
|
20
|
-
|
|
19
|
+
updateSafeAppOrigins(normalizeOrigins([...safeAppOrigins, ...origins]));
|
|
21
20
|
}
|
|
22
21
|
export function getSafeAppOrigins() {
|
|
23
22
|
return [...safeAppOrigins];
|
|
@@ -68,13 +67,6 @@ function updateSafeAppOrigins(nextOrigins) {
|
|
|
68
67
|
safeAppOriginSet = new Set(safeAppOrigins);
|
|
69
68
|
notifySafeAppOriginListeners();
|
|
70
69
|
}
|
|
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
70
|
function notifySafeAppOriginListeners() {
|
|
79
71
|
for (const listener of safeAppOriginListeners) {
|
|
80
72
|
listener();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"safe-app-origins.js","sourceRoot":"","sources":["../../src/react-hooks/safe-app-origins.ts"],"names":[],"mappings":"AAAA,YAAY,CAAC;AAMb,MAAM,CAAC,MAAM,wBAAwB,GAAG;IAEtC,yBAAyB;IACzB,wBAAwB;IACxB,qBAAqB;IAErB,4BAA4B;IAC5B,yBAAyB;IACzB,2BAA2B;IAC3B,0BAA0B;CAClB,CAAC;AAEX,MAAM,uBAAuB,GAAG,eAAe,CAAC;AAChD,IAAI,cAAc,GAAG,gBAAgB,CAAC,wBAAwB,CAAC,CAAC;AAChE,IAAI,gBAAgB,GAAG,IAAI,GAAG,CAAC,cAAc,CAAC,CAAC;AAC/C,MAAM,sBAAsB,GAAG,IAAI,GAAG,EAAc,CAAC;
|
|
1
|
+
{"version":3,"file":"safe-app-origins.js","sourceRoot":"","sources":["../../src/react-hooks/safe-app-origins.ts"],"names":[],"mappings":"AAAA,YAAY,CAAC;AAMb,MAAM,CAAC,MAAM,wBAAwB,GAAG;IAEtC,yBAAyB;IACzB,wBAAwB;IACxB,qBAAqB;IAErB,4BAA4B;IAC5B,yBAAyB;IACzB,2BAA2B;IAC3B,0BAA0B;CAClB,CAAC;AAEX,MAAM,uBAAuB,GAAG,eAAe,CAAC;AAChD,IAAI,cAAc,GAAG,gBAAgB,CAAC,wBAAwB,CAAC,CAAC;AAChE,IAAI,gBAAgB,GAAG,IAAI,GAAG,CAAC,cAAc,CAAC,CAAC;AAC/C,MAAM,sBAAsB,GAAG,IAAI,GAAG,EAAc,CAAC;AAGrD,MAAM,UAAU,iBAAiB,CAAC,OAA0B;IAC1D,oBAAoB,CAAC,gBAAgB,CAAC,OAAO,CAAC,CAAC,CAAC;AAClD,CAAC;AAGD,MAAM,UAAU,oBAAoB,CAAC,OAA0B;IAC7D,oBAAoB,CAAC,gBAAgB,CAAC,CAAC,GAAG,cAAc,EAAE,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;AAC1E,CAAC;AAGD,MAAM,UAAU,iBAAiB;IAC/B,OAAO,CAAC,GAAG,cAAc,CAAC,CAAC;AAC7B,CAAC;AAGD,MAAM,UAAU,uBAAuB,CAAC,QAAoB;IAC1D,sBAAsB,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;IACrC,OAAO,GAAG,EAAE;QACV,sBAAsB,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;IAC1C,CAAC,CAAC;AACJ,CAAC;AAGD,MAAM,UAAU,oBAAoB;IAClC,MAAM,MAAM,GAAG,iBAAiB,EAAE,CAAC;IACnC,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,OAAO,KAAK,CAAC;IACf,CAAC;IACD,OAAO,gBAAgB,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;AACtC,CAAC;AAGD,MAAM,UAAU,cAAc;IAC5B,OAAO,OAAO,MAAM,KAAK,WAAW,IAAI,MAAM,CAAC,MAAM,KAAK,MAAM,CAAC;AACnE,CAAC;AAMD,SAAS,iBAAiB;IACxB,IAAI,OAAO,MAAM,KAAK,WAAW,EAAE,CAAC;QAClC,OAAO,IAAI,CAAC;IACd,CAAC;IAED,IAAI,CAAC;QACH,OAAO,MAAM,CAAC,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC;IACvC,CAAC;IAAC,MAAM,CAAC;QACP,IAAI,MAAM,CAAC,QAAQ,CAAC,eAAe,EAAE,MAAM,EAAE,CAAC;YAC5C,OAAO,MAAM,CAAC,QAAQ,CAAC,eAAe,CAAC,CAAC,CAAC,CAAC;QAC5C,CAAC;QAED,IAAI,QAAQ,CAAC,QAAQ,EAAE,CAAC;YACtB,IAAI,CAAC;gBACH,OAAO,IAAI,GAAG,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC;YAC3C,CAAC;YAAC,MAAM,CAAC;gBACP,OAAO,IAAI,CAAC;YACd,CAAC;QACH,CAAC;QAED,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED,SAAS,oBAAoB,CAAC,WAAqB;IACjD,IAAI,cAAc,CAAC,cAAc,EAAE,WAAW,CAAC,EAAE,CAAC;QAChD,OAAO;IACT,CAAC;IAED,cAAc,GAAG,WAAW,CAAC;IAC7B,gBAAgB,GAAG,IAAI,GAAG,CAAC,cAAc,CAAC,CAAC;IAC3C,4BAA4B,EAAE,CAAC;AACjC,CAAC;AAED,SAAS,4BAA4B;IACnC,KAAK,MAAM,QAAQ,IAAI,sBAAsB,EAAE,CAAC;QAC9C,QAAQ,EAAE,CAAC;IACb,CAAC;AACH,CAAC;AAED,SAAS,gBAAgB,CAAC,OAA0B;IAClD,MAAM,UAAU,GAAa,EAAE,CAAC;IAChC,MAAM,IAAI,GAAG,IAAI,GAAG,EAAU,CAAC;IAE/B,KAAK,MAAM,MAAM,IAAI,OAAO,EAAE,CAAC;QAC7B,MAAM,gBAAgB,GAAG,eAAe,CAAC,MAAM,CAAC,CAAC;QACjD,IAAI,CAAC,gBAAgB,IAAI,IAAI,CAAC,GAAG,CAAC,gBAAgB,CAAC,EAAE,CAAC;YACpD,SAAS;QACX,CAAC;QACD,IAAI,CAAC,GAAG,CAAC,gBAAgB,CAAC,CAAC;QAC3B,UAAU,CAAC,IAAI,CAAC,gBAAgB,CAAC,CAAC;IACpC,CAAC;IAED,OAAO,UAAU,CAAC;AACpB,CAAC;AAED,SAAS,eAAe,CAAC,MAAc;IACrC,MAAM,OAAO,GAAG,MAAM,CAAC,IAAI,EAAE,CAAC;IAC9B,IAAI,CAAC,OAAO,EAAE,CAAC;QACb,OAAO,IAAI,CAAC;IACd,CAAC;IAED,MAAM,SAAS,GAAG,uBAAuB,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,WAAW,OAAO,EAAE,CAAC;IAEzF,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,SAAS,CAAC,CAAC;QAC/B,IAAI,GAAG,CAAC,QAAQ,KAAK,QAAQ,IAAI,GAAG,CAAC,QAAQ,KAAK,OAAO,EAAE,CAAC;YAC1D,OAAO,IAAI,CAAC;QACd,CAAC;QACD,OAAO,GAAG,CAAC,MAAM,CAAC;IACpB,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED,SAAS,cAAc,CAAC,IAAuB,EAAE,KAAwB;IACvE,IAAI,IAAI,CAAC,MAAM,KAAK,KAAK,CAAC,MAAM,EAAE,CAAC;QACjC,OAAO,KAAK,CAAC;IACf,CAAC;IAED,KAAK,IAAI,KAAK,GAAG,CAAC,EAAE,KAAK,GAAG,IAAI,CAAC,MAAM,EAAE,KAAK,IAAI,CAAC,EAAE,CAAC;QACpD,IAAI,IAAI,CAAC,KAAK,CAAC,KAAK,KAAK,CAAC,KAAK,CAAC,EAAE,CAAC;YACjC,OAAO,KAAK,CAAC;QACf,CAAC;IACH,CAAC;IAED,OAAO,IAAI,CAAC;AACd,CAAC","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>();\n\n/** Replace the Safe App origins list. */\nexport function setSafeAppOrigins(origins: readonly string[]) {\n updateSafeAppOrigins(normalizeOrigins(origins));\n}\n\n/** Extend the Safe App origins list. */\nexport function extendSafeAppOrigins(origins: readonly string[]) {\n updateSafeAppOrigins(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 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"]}
|
|
@@ -13,6 +13,9 @@ export type SafeMultisigWaitResult = {
|
|
|
13
13
|
readonly safeTxHash: Hash;
|
|
14
14
|
} | {
|
|
15
15
|
readonly _tag: "queued";
|
|
16
|
+
readonly confirmations: number | null;
|
|
17
|
+
readonly confirmationsRequired: number | null;
|
|
18
|
+
readonly lastStatus: SafeMultisigTxStatus;
|
|
16
19
|
readonly onchainHash: null;
|
|
17
20
|
readonly safeTxHash: Hash;
|
|
18
21
|
} | {
|
|
@@ -26,30 +29,6 @@ export type SafeMultisigWaitResult = {
|
|
|
26
29
|
readonly safeTxHash: Hash;
|
|
27
30
|
};
|
|
28
31
|
export type SafeMultisigTxStatus = "awaiting_confirmations" | "awaiting_execution" | "pending" | "success" | "failed";
|
|
29
|
-
export declare const waitForSafeMultisigTx: (safeTxHash: `0x${string}`, getReceipt: (hash: Hash) => Effect.Effect<TransactionReceipt, SafeMultisigTxLookupError>, options?: SafeMultisigWaitOptions | undefined) => Effect.Effect<
|
|
30
|
-
|
|
31
|
-
onchainHash: null;
|
|
32
|
-
safeTxHash: `0x${string}`;
|
|
33
|
-
error?: undefined;
|
|
34
|
-
receipt?: undefined;
|
|
35
|
-
} | {
|
|
36
|
-
_tag: "failed";
|
|
37
|
-
error: string;
|
|
38
|
-
onchainHash: null;
|
|
39
|
-
safeTxHash: `0x${string}`;
|
|
40
|
-
receipt?: undefined;
|
|
41
|
-
} | {
|
|
42
|
-
_tag: "success";
|
|
43
|
-
onchainHash: `0x${string}`;
|
|
44
|
-
receipt: TransactionReceipt;
|
|
45
|
-
safeTxHash: `0x${string}`;
|
|
46
|
-
error?: undefined;
|
|
47
|
-
} | {
|
|
48
|
-
_tag: "queued";
|
|
49
|
-
onchainHash: null;
|
|
50
|
-
safeTxHash: `0x${string}`;
|
|
51
|
-
error?: undefined;
|
|
52
|
-
receipt?: undefined;
|
|
53
|
-
}, SafeMultisigTxLookupError, SafeAppsService>;
|
|
54
|
-
export declare const getSafeMultisigTxStatus: (safeTxHash: `0x${string}`) => Effect.Effect<"pending" | "success" | "failed" | "awaiting_confirmations" | "awaiting_execution", SafeMultisigTxLookupError, SafeAppsService>;
|
|
32
|
+
export declare const waitForSafeMultisigTx: (safeTxHash: `0x${string}`, getReceipt: (hash: Hash) => Effect.Effect<TransactionReceipt, SafeMultisigTxLookupError>, options?: SafeMultisigWaitOptions | undefined) => Effect.Effect<SafeMultisigWaitResult, SafeMultisigTxLookupError, SafeAppsService>;
|
|
33
|
+
export declare const getSafeMultisigTxStatus: (safeTxHash: `0x${string}`) => Effect.Effect<SafeMultisigTxStatus, SafeMultisigTxLookupError, SafeAppsService>;
|
|
55
34
|
//# sourceMappingURL=tx-lifecycle.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tx-lifecycle.d.ts","sourceRoot":"","sources":["../../src/safe/tx-lifecycle.ts"],"names":[],"mappings":"AAUA,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAU,MAAM,QAAQ,CAAC;AAClD,OAAO,KAAK,EAAE,IAAI,EAAE,kBAAkB,EAAE,MAAM,MAAM,CAAC;AACrD,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,aAAa,CAAC;AAC7D,OAAO,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAe/C,MAAM,MAAM,uBAAuB,GAAG;IAEpC,QAAQ,CAAC,QAAQ,CAAC,EAAE,QAAQ,CAAC,aAAa,CAAC;IAE3C,QAAQ,CAAC,OAAO,CAAC,EAAE,QAAQ,CAAC,aAAa,CAAC;CAC3C,CAAC;AAEF,MAAM,MAAM,sBAAsB,GAC9B;IACE,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IACzB,QAAQ,CAAC,WAAW,EAAE,IAAI,CAAC;IAC3B,QAAQ,CAAC,OAAO,EAAE,kBAAkB,CAAC;IACrC,QAAQ,CAAC,UAAU,EAAE,IAAI,CAAC;CAC3B,GACD;
|
|
1
|
+
{"version":3,"file":"tx-lifecycle.d.ts","sourceRoot":"","sources":["../../src/safe/tx-lifecycle.ts"],"names":[],"mappings":"AAUA,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAU,MAAM,QAAQ,CAAC;AAClD,OAAO,KAAK,EAAE,IAAI,EAAE,kBAAkB,EAAE,MAAM,MAAM,CAAC;AACrD,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,aAAa,CAAC;AAC7D,OAAO,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAe/C,MAAM,MAAM,uBAAuB,GAAG;IAEpC,QAAQ,CAAC,QAAQ,CAAC,EAAE,QAAQ,CAAC,aAAa,CAAC;IAE3C,QAAQ,CAAC,OAAO,CAAC,EAAE,QAAQ,CAAC,aAAa,CAAC;CAC3C,CAAC;AAEF,MAAM,MAAM,sBAAsB,GAC9B;IACE,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IACzB,QAAQ,CAAC,WAAW,EAAE,IAAI,CAAC;IAC3B,QAAQ,CAAC,OAAO,EAAE,kBAAkB,CAAC;IACrC,QAAQ,CAAC,UAAU,EAAE,IAAI,CAAC;CAC3B,GACD;IACE,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC;IACxB,QAAQ,CAAC,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IACtC,QAAQ,CAAC,qBAAqB,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9C,QAAQ,CAAC,UAAU,EAAE,oBAAoB,CAAC;IAC1C,QAAQ,CAAC,WAAW,EAAE,IAAI,CAAC;IAC3B,QAAQ,CAAC,UAAU,EAAE,IAAI,CAAC;CAC3B,GACD;IAAE,QAAQ,CAAC,IAAI,EAAE,WAAW,CAAC;IAAC,QAAQ,CAAC,WAAW,EAAE,IAAI,CAAC;IAAC,QAAQ,CAAC,UAAU,EAAE,IAAI,CAAA;CAAE,GACrF;IACE,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC;IACxB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,WAAW,EAAE,IAAI,CAAC;IAC3B,QAAQ,CAAC,UAAU,EAAE,IAAI,CAAC;CAC3B,CAAC;AAEN,MAAM,MAAM,oBAAoB,GAC5B,wBAAwB,GACxB,oBAAoB,GACpB,SAAS,GACT,SAAS,GACT,QAAQ,CAAC;AAwFb,eAAO,MAAM,qBAAqB,iDAEb,IAAI,KAAK,MAAM,CAAC,MAAM,CAAC,kBAAkB,EAAE,yBAAyB,CAAC,qIA2ExF,CAAC;AAaH,eAAO,MAAM,uBAAuB,gHAMlC,CAAC"}
|
|
@@ -3,11 +3,63 @@ import { SafeAppsService } from "./service.js";
|
|
|
3
3
|
const DEFAULT_POLL_INTERVAL = Duration.seconds(5);
|
|
4
4
|
const DEFAULT_MAX_WAIT = Duration.minutes(90);
|
|
5
5
|
const MIN_POLL_INTERVAL = Duration.seconds(1);
|
|
6
|
+
function mapStatus(raw) {
|
|
7
|
+
switch (raw) {
|
|
8
|
+
case "AWAITING_CONFIRMATIONS":
|
|
9
|
+
return "awaiting_confirmations";
|
|
10
|
+
case "AWAITING_EXECUTION":
|
|
11
|
+
return "awaiting_execution";
|
|
12
|
+
case "SUCCESS":
|
|
13
|
+
return "success";
|
|
14
|
+
case "CANCELLED":
|
|
15
|
+
case "FAILED":
|
|
16
|
+
return "failed";
|
|
17
|
+
default:
|
|
18
|
+
return "pending";
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
function resolveTerminalWaitResult(queued, safeTxHash, getReceipt) {
|
|
22
|
+
switch (queued.status) {
|
|
23
|
+
case "CANCELLED":
|
|
24
|
+
return Effect.succeed(Option.some({
|
|
25
|
+
_tag: "cancelled",
|
|
26
|
+
onchainHash: null,
|
|
27
|
+
safeTxHash,
|
|
28
|
+
}));
|
|
29
|
+
case "FAILED":
|
|
30
|
+
return Effect.succeed(Option.some({
|
|
31
|
+
_tag: "failed",
|
|
32
|
+
error: "Safe transaction failed",
|
|
33
|
+
onchainHash: null,
|
|
34
|
+
safeTxHash,
|
|
35
|
+
}));
|
|
36
|
+
case "SUCCESS": {
|
|
37
|
+
if (Option.isNone(queued.onchainHash)) {
|
|
38
|
+
return Effect.succeed(Option.some({
|
|
39
|
+
_tag: "failed",
|
|
40
|
+
error: "Safe transaction succeeded but no on-chain hash available",
|
|
41
|
+
onchainHash: null,
|
|
42
|
+
safeTxHash,
|
|
43
|
+
}));
|
|
44
|
+
}
|
|
45
|
+
const txHash = queued.onchainHash.value;
|
|
46
|
+
return getReceipt(txHash).pipe(Effect.map((receipt) => Option.some({
|
|
47
|
+
_tag: "success",
|
|
48
|
+
onchainHash: txHash,
|
|
49
|
+
receipt,
|
|
50
|
+
safeTxHash,
|
|
51
|
+
})));
|
|
52
|
+
}
|
|
53
|
+
default:
|
|
54
|
+
return Effect.succeed(Option.none());
|
|
55
|
+
}
|
|
56
|
+
}
|
|
6
57
|
export const waitForSafeMultisigTx = Effect.fn("waitForSafeMultisigTx")(function* (safeTxHash, getReceipt, options = {}) {
|
|
7
58
|
const interval = Duration.max(Duration.decode(options.interval ?? DEFAULT_POLL_INTERVAL), MIN_POLL_INTERVAL);
|
|
8
59
|
const maxWait = Duration.decode(options.maxWait ?? DEFAULT_MAX_WAIT);
|
|
9
60
|
const maxAttempts = Math.floor(Duration.toMillis(maxWait) / Duration.toMillis(interval));
|
|
10
61
|
const safeApps = yield* SafeAppsService;
|
|
62
|
+
let lastInfo = null;
|
|
11
63
|
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
12
64
|
const queuedResult = yield* safeApps.getTx(safeTxHash).pipe(Effect.map(Option.some), Effect.catchTag("SafeMultisigTxLookupError", (error) => {
|
|
13
65
|
if (error.retryable) {
|
|
@@ -16,50 +68,26 @@ export const waitForSafeMultisigTx = Effect.fn("waitForSafeMultisigTx")(function
|
|
|
16
68
|
return Effect.fail(error);
|
|
17
69
|
}));
|
|
18
70
|
if (Option.isNone(queuedResult)) {
|
|
19
|
-
|
|
71
|
+
if (attempt < maxAttempts - 1) {
|
|
72
|
+
yield* Effect.sleep(interval);
|
|
73
|
+
}
|
|
20
74
|
continue;
|
|
21
75
|
}
|
|
22
76
|
const queued = queuedResult.value;
|
|
77
|
+
lastInfo = queued;
|
|
23
78
|
yield* Effect.logDebug("Safe tx poll status").pipe(Effect.annotateLogs({
|
|
24
79
|
attempt,
|
|
25
80
|
hash: Option.isSome(queued.onchainHash) ? queued.onchainHash.value : "pending",
|
|
26
81
|
safeTxHash,
|
|
27
82
|
status: queued.status,
|
|
28
83
|
}));
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
onchainHash: null,
|
|
33
|
-
safeTxHash,
|
|
34
|
-
};
|
|
35
|
-
}
|
|
36
|
-
if (queued.status === "FAILED") {
|
|
37
|
-
return {
|
|
38
|
-
_tag: "failed",
|
|
39
|
-
error: "Safe transaction failed",
|
|
40
|
-
onchainHash: null,
|
|
41
|
-
safeTxHash,
|
|
42
|
-
};
|
|
84
|
+
const terminalResult = yield* resolveTerminalWaitResult(queued, safeTxHash, getReceipt);
|
|
85
|
+
if (Option.isSome(terminalResult)) {
|
|
86
|
+
return terminalResult.value;
|
|
43
87
|
}
|
|
44
|
-
if (
|
|
45
|
-
|
|
46
|
-
return {
|
|
47
|
-
_tag: "failed",
|
|
48
|
-
error: "Safe transaction succeeded but no on-chain hash available",
|
|
49
|
-
onchainHash: null,
|
|
50
|
-
safeTxHash,
|
|
51
|
-
};
|
|
52
|
-
}
|
|
53
|
-
const txHash = queued.onchainHash.value;
|
|
54
|
-
const receipt = yield* getReceipt(txHash);
|
|
55
|
-
return {
|
|
56
|
-
_tag: "success",
|
|
57
|
-
onchainHash: txHash,
|
|
58
|
-
receipt,
|
|
59
|
-
safeTxHash,
|
|
60
|
-
};
|
|
88
|
+
if (attempt < maxAttempts - 1) {
|
|
89
|
+
yield* Effect.sleep(interval);
|
|
61
90
|
}
|
|
62
|
-
yield* Effect.sleep(interval);
|
|
63
91
|
}
|
|
64
92
|
yield* Effect.logWarning("Safe multisig transaction polling timeout").pipe(Effect.annotateLogs({
|
|
65
93
|
maxAttempts,
|
|
@@ -68,6 +96,9 @@ export const waitForSafeMultisigTx = Effect.fn("waitForSafeMultisigTx")(function
|
|
|
68
96
|
}));
|
|
69
97
|
return {
|
|
70
98
|
_tag: "queued",
|
|
99
|
+
confirmations: lastInfo?.confirmations ?? null,
|
|
100
|
+
confirmationsRequired: lastInfo?.confirmationsRequired ?? null,
|
|
101
|
+
lastStatus: mapStatus(lastInfo?.status),
|
|
71
102
|
onchainHash: null,
|
|
72
103
|
safeTxHash,
|
|
73
104
|
};
|
|
@@ -75,18 +106,6 @@ export const waitForSafeMultisigTx = Effect.fn("waitForSafeMultisigTx")(function
|
|
|
75
106
|
export const getSafeMultisigTxStatus = Effect.fn("getSafeMultisigTxStatus")(function* (safeTxHash) {
|
|
76
107
|
const safeApps = yield* SafeAppsService;
|
|
77
108
|
const queued = yield* safeApps.getTx(safeTxHash);
|
|
78
|
-
|
|
79
|
-
case "AWAITING_CONFIRMATIONS":
|
|
80
|
-
return "awaiting_confirmations";
|
|
81
|
-
case "AWAITING_EXECUTION":
|
|
82
|
-
return "awaiting_execution";
|
|
83
|
-
case "SUCCESS":
|
|
84
|
-
return "success";
|
|
85
|
-
case "CANCELLED":
|
|
86
|
-
case "FAILED":
|
|
87
|
-
return "failed";
|
|
88
|
-
default:
|
|
89
|
-
return "pending";
|
|
90
|
-
}
|
|
109
|
+
return mapStatus(queued.status);
|
|
91
110
|
});
|
|
92
111
|
//# sourceMappingURL=tx-lifecycle.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tx-lifecycle.js","sourceRoot":"","sources":["../../src/safe/tx-lifecycle.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Safe multisig transaction lifecycle tracking.\n *\n * Polls the Safe API to track transactions from proposal through execution.\n * Fills the gap between sendTxs() (which returns a safeTxHash) and knowing\n * when the transaction lands on-chain.\n *\n * @module safe/tx-lifecycle\n */\n\nimport { Duration, Effect, Option } from \"effect\";\nimport type { Hash, TransactionReceipt } from \"viem\";\nimport type { SafeMultisigTxLookupError } from \"./errors.js\";\nimport { SafeAppsService } from \"./service.js\";\nimport type { SafeMultisigTxInfo } from \"./types.js\";\n\n// ---------------------------------------------------------------------------\n// Configuration defaults\n// ---------------------------------------------------------------------------\n\nconst DEFAULT_POLL_INTERVAL = Duration.seconds(5);\nconst DEFAULT_MAX_WAIT = Duration.minutes(90);\nconst MIN_POLL_INTERVAL = Duration.seconds(1);\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport type SafeMultisigWaitOptions = {\n /** Polling interval (default: 5 seconds). Clamped to a minimum of 1 second. */\n readonly interval?: Duration.DurationInput;\n /** Maximum wait time (default: 90 minutes) */\n readonly maxWait?: Duration.DurationInput;\n};\n\nexport type SafeMultisigWaitResult =\n | {\n readonly _tag: \"success\";\n readonly onchainHash: Hash;\n readonly receipt: TransactionReceipt;\n readonly safeTxHash: Hash;\n }\n | { readonly _tag: \"queued\"; readonly onchainHash: null; readonly safeTxHash: Hash }\n | { readonly _tag: \"cancelled\"; readonly onchainHash: null; readonly safeTxHash: Hash }\n | {\n readonly _tag: \"failed\";\n readonly error: string;\n readonly onchainHash: null;\n readonly safeTxHash: Hash;\n };\n\nexport type SafeMultisigTxStatus =\n | \"awaiting_confirmations\"\n | \"awaiting_execution\"\n | \"pending\"\n | \"success\"\n | \"failed\";\n\n// ---------------------------------------------------------------------------\n// waitForSafeMultisigTx\n// ---------------------------------------------------------------------------\n\n/**\n * Poll a Safe multisig transaction until it reaches a terminal state or times out.\n *\n * Unlike `SafeAppsService.waitForTxReceipt` (which assumes execution will happen\n * in the current session), this utility handles the full lifecycle including\n * transactions that stay queued because other signers haven't signed yet.\n *\n * Terminal states: success (on-chain), cancelled, failed.\n * On timeout this returns a \"queued\" result with `onchainHash: null` and the\n * original `safeTxHash` so callers can persist and resume tracking later.\n *\n * @param safeTxHash - The Safe transaction hash returned by `sendTxs`\n * @param getReceipt - Caller-provided effect to fetch an on-chain receipt\n * @param options - Optional polling configuration\n */\nexport const waitForSafeMultisigTx = Effect.fn(\"waitForSafeMultisigTx\")(function* (\n safeTxHash: Hash,\n getReceipt: (hash: Hash) => Effect.Effect<TransactionReceipt, SafeMultisigTxLookupError>,\n options: SafeMultisigWaitOptions = {}\n) {\n const interval = Duration.max(\n Duration.decode(options.interval ?? DEFAULT_POLL_INTERVAL),\n MIN_POLL_INTERVAL\n );\n const maxWait = Duration.decode(options.maxWait ?? DEFAULT_MAX_WAIT);\n const maxAttempts = Math.floor(Duration.toMillis(maxWait) / Duration.toMillis(interval));\n\n const safeApps = yield* SafeAppsService;\n\n for (let attempt = 0; attempt < maxAttempts; attempt++) {\n // --- Fetch tx status, classifying errors ---\n const queuedResult = yield* safeApps.getTx(safeTxHash).pipe(\n Effect.map(Option.some),\n Effect.catchTag(\"SafeMultisigTxLookupError\", (error) => {\n if (error.retryable) {\n return Effect.logWarning(\"Retryable error polling Safe tx\").pipe(\n Effect.annotateLogs({ attempt, error: error.message, safeTxHash }),\n Effect.as(Option.none<SafeMultisigTxInfo>())\n );\n }\n // Terminal lookup error — stop polling immediately\n return Effect.fail(error);\n })\n );\n\n if (Option.isNone(queuedResult)) {\n yield* Effect.sleep(interval);\n continue;\n }\n\n const queued = queuedResult.value;\n yield* Effect.logDebug(\"Safe tx poll status\").pipe(\n Effect.annotateLogs({\n attempt,\n hash: Option.isSome(queued.onchainHash) ? queued.onchainHash.value : \"pending\",\n safeTxHash,\n status: queued.status,\n })\n );\n\n // --- Terminal states ---\n\n if (queued.status === \"CANCELLED\") {\n return {\n _tag: \"cancelled\" as const,\n onchainHash: null,\n safeTxHash,\n } satisfies SafeMultisigWaitResult;\n }\n\n if (queued.status === \"FAILED\") {\n return {\n _tag: \"failed\" as const,\n error: \"Safe transaction failed\",\n onchainHash: null,\n safeTxHash,\n } satisfies SafeMultisigWaitResult;\n }\n\n if (queued.status === \"SUCCESS\") {\n if (Option.isNone(queued.onchainHash)) {\n return {\n _tag: \"failed\" as const,\n error: \"Safe transaction succeeded but no on-chain hash available\",\n onchainHash: null,\n safeTxHash,\n } satisfies SafeMultisigWaitResult;\n }\n const txHash = queued.onchainHash.value as Hash;\n const receipt = yield* getReceipt(txHash);\n return {\n _tag: \"success\" as const,\n onchainHash: txHash,\n receipt,\n safeTxHash,\n } satisfies SafeMultisigWaitResult;\n }\n\n // Still pending — keep polling\n yield* Effect.sleep(interval);\n }\n\n // Timed out without reaching a terminal state\n yield* Effect.logWarning(\"Safe multisig transaction polling timeout\").pipe(\n Effect.annotateLogs({\n maxAttempts,\n maxWaitMs: Duration.toMillis(maxWait),\n safeTxHash,\n })\n );\n\n return {\n _tag: \"queued\" as const,\n onchainHash: null,\n safeTxHash,\n } satisfies SafeMultisigWaitResult;\n});\n\n// ---------------------------------------------------------------------------\n// getSafeMultisigTxStatus\n// ---------------------------------------------------------------------------\n\n/**\n * Fetch the current lifecycle status of a Safe multisig transaction.\n *\n * Maps the raw Safe API status string to a normalized union. Useful for\n * one-shot status checks (e.g. resuming after page reload) without starting\n * a polling loop.\n */\nexport const getSafeMultisigTxStatus = Effect.fn(\"getSafeMultisigTxStatus\")(function* (\n safeTxHash: Hash\n) {\n const safeApps = yield* SafeAppsService;\n const queued = yield* safeApps.getTx(safeTxHash);\n\n switch (queued.status) {\n case \"AWAITING_CONFIRMATIONS\":\n return \"awaiting_confirmations\";\n case \"AWAITING_EXECUTION\":\n return \"awaiting_execution\";\n case \"SUCCESS\":\n return \"success\";\n // Both map to \"failed\" — use waitForSafeMultisigTx to distinguish cancelled vs failed\n case \"CANCELLED\":\n case \"FAILED\":\n return \"failed\";\n default:\n return \"pending\";\n }\n});\n"]}
|
|
1
|
+
{"version":3,"file":"tx-lifecycle.js","sourceRoot":"","sources":["../../src/safe/tx-lifecycle.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Safe multisig transaction lifecycle tracking.\n *\n * Polls the Safe API to track transactions from proposal through execution.\n * Fills the gap between sendTxs() (which returns a safeTxHash) and knowing\n * when the transaction lands on-chain.\n *\n * @module safe/tx-lifecycle\n */\n\nimport { Duration, Effect, Option } from \"effect\";\nimport type { Hash, TransactionReceipt } from \"viem\";\nimport type { SafeMultisigTxLookupError } from \"./errors.js\";\nimport { SafeAppsService } from \"./service.js\";\nimport type { SafeMultisigTxInfo } from \"./types.js\";\n\n// ---------------------------------------------------------------------------\n// Configuration defaults\n// ---------------------------------------------------------------------------\n\nconst DEFAULT_POLL_INTERVAL = Duration.seconds(5);\nconst DEFAULT_MAX_WAIT = Duration.minutes(90);\nconst MIN_POLL_INTERVAL = Duration.seconds(1);\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport type SafeMultisigWaitOptions = {\n /** Polling interval (default: 5 seconds). Clamped to a minimum of 1 second. */\n readonly interval?: Duration.DurationInput;\n /** Maximum wait time (default: 90 minutes) */\n readonly maxWait?: Duration.DurationInput;\n};\n\nexport type SafeMultisigWaitResult =\n | {\n readonly _tag: \"success\";\n readonly onchainHash: Hash;\n readonly receipt: TransactionReceipt;\n readonly safeTxHash: Hash;\n }\n | {\n readonly _tag: \"queued\";\n readonly confirmations: number | null;\n readonly confirmationsRequired: number | null;\n readonly lastStatus: SafeMultisigTxStatus;\n readonly onchainHash: null;\n readonly safeTxHash: Hash;\n }\n | { readonly _tag: \"cancelled\"; readonly onchainHash: null; readonly safeTxHash: Hash }\n | {\n readonly _tag: \"failed\";\n readonly error: string;\n readonly onchainHash: null;\n readonly safeTxHash: Hash;\n };\n\nexport type SafeMultisigTxStatus =\n | \"awaiting_confirmations\"\n | \"awaiting_execution\"\n | \"pending\"\n | \"success\"\n | \"failed\";\n\nfunction mapStatus(raw: string | undefined): SafeMultisigTxStatus {\n switch (raw) {\n case \"AWAITING_CONFIRMATIONS\":\n return \"awaiting_confirmations\";\n case \"AWAITING_EXECUTION\":\n return \"awaiting_execution\";\n case \"SUCCESS\":\n return \"success\";\n case \"CANCELLED\":\n case \"FAILED\":\n return \"failed\";\n default:\n return \"pending\";\n }\n}\n\nfunction resolveTerminalWaitResult(\n queued: SafeMultisigTxInfo,\n safeTxHash: Hash,\n getReceipt: (hash: Hash) => Effect.Effect<TransactionReceipt, SafeMultisigTxLookupError>\n): Effect.Effect<Option.Option<SafeMultisigWaitResult>, SafeMultisigTxLookupError> {\n switch (queued.status) {\n case \"CANCELLED\":\n return Effect.succeed(\n Option.some({\n _tag: \"cancelled\" as const,\n onchainHash: null,\n safeTxHash,\n } satisfies SafeMultisigWaitResult)\n );\n case \"FAILED\":\n return Effect.succeed(\n Option.some({\n _tag: \"failed\" as const,\n error: \"Safe transaction failed\",\n onchainHash: null,\n safeTxHash,\n } satisfies SafeMultisigWaitResult)\n );\n case \"SUCCESS\": {\n if (Option.isNone(queued.onchainHash)) {\n return Effect.succeed(\n Option.some({\n _tag: \"failed\" as const,\n error: \"Safe transaction succeeded but no on-chain hash available\",\n onchainHash: null,\n safeTxHash,\n } satisfies SafeMultisigWaitResult)\n );\n }\n const txHash = queued.onchainHash.value as Hash;\n return getReceipt(txHash).pipe(\n Effect.map((receipt) =>\n Option.some({\n _tag: \"success\" as const,\n onchainHash: txHash,\n receipt,\n safeTxHash,\n } satisfies SafeMultisigWaitResult)\n )\n );\n }\n default:\n return Effect.succeed(Option.none());\n }\n}\n\n// ---------------------------------------------------------------------------\n// waitForSafeMultisigTx\n// ---------------------------------------------------------------------------\n\n/**\n * Poll a Safe multisig transaction until it reaches a terminal state or times out.\n *\n * Unlike `SafeAppsService.waitForTxReceipt` (which assumes execution will happen\n * in the current session), this utility handles the full lifecycle including\n * transactions that stay queued because other signers haven't signed yet.\n *\n * Terminal states: success (on-chain), cancelled, failed.\n * On timeout this returns a \"queued\" result with `onchainHash: null` and the\n * original `safeTxHash` so callers can persist and resume tracking later.\n *\n * @param safeTxHash - The Safe transaction hash returned by `sendTxs`\n * @param getReceipt - Caller-provided effect to fetch an on-chain receipt\n * @param options - Optional polling configuration\n */\nexport const waitForSafeMultisigTx = Effect.fn(\"waitForSafeMultisigTx\")(function* (\n safeTxHash: Hash,\n getReceipt: (hash: Hash) => Effect.Effect<TransactionReceipt, SafeMultisigTxLookupError>,\n options: SafeMultisigWaitOptions = {}\n) {\n const interval = Duration.max(\n Duration.decode(options.interval ?? DEFAULT_POLL_INTERVAL),\n MIN_POLL_INTERVAL\n );\n const maxWait = Duration.decode(options.maxWait ?? DEFAULT_MAX_WAIT);\n const maxAttempts = Math.floor(Duration.toMillis(maxWait) / Duration.toMillis(interval));\n\n const safeApps = yield* SafeAppsService;\n let lastInfo: SafeMultisigTxInfo | null = null;\n\n for (let attempt = 0; attempt < maxAttempts; attempt++) {\n // --- Fetch tx status, classifying errors ---\n const queuedResult = yield* safeApps.getTx(safeTxHash).pipe(\n Effect.map(Option.some),\n Effect.catchTag(\"SafeMultisigTxLookupError\", (error) => {\n if (error.retryable) {\n return Effect.logWarning(\"Retryable error polling Safe tx\").pipe(\n Effect.annotateLogs({ attempt, error: error.message, safeTxHash }),\n Effect.as(Option.none<SafeMultisigTxInfo>())\n );\n }\n // Terminal lookup error — stop polling immediately\n return Effect.fail(error);\n })\n );\n\n if (Option.isNone(queuedResult)) {\n if (attempt < maxAttempts - 1) {\n yield* Effect.sleep(interval);\n }\n continue;\n }\n\n const queued = queuedResult.value;\n lastInfo = queued;\n yield* Effect.logDebug(\"Safe tx poll status\").pipe(\n Effect.annotateLogs({\n attempt,\n hash: Option.isSome(queued.onchainHash) ? queued.onchainHash.value : \"pending\",\n safeTxHash,\n status: queued.status,\n })\n );\n\n const terminalResult = yield* resolveTerminalWaitResult(queued, safeTxHash, getReceipt);\n if (Option.isSome(terminalResult)) {\n return terminalResult.value;\n }\n\n // Still pending — keep polling\n if (attempt < maxAttempts - 1) {\n yield* Effect.sleep(interval);\n }\n }\n\n // Timed out without reaching a terminal state\n yield* Effect.logWarning(\"Safe multisig transaction polling timeout\").pipe(\n Effect.annotateLogs({\n maxAttempts,\n maxWaitMs: Duration.toMillis(maxWait),\n safeTxHash,\n })\n );\n\n return {\n _tag: \"queued\" as const,\n confirmations: lastInfo?.confirmations ?? null,\n confirmationsRequired: lastInfo?.confirmationsRequired ?? null,\n lastStatus: mapStatus(lastInfo?.status),\n onchainHash: null,\n safeTxHash,\n } satisfies SafeMultisigWaitResult;\n});\n\n// ---------------------------------------------------------------------------\n// getSafeMultisigTxStatus\n// ---------------------------------------------------------------------------\n\n/**\n * Fetch the current lifecycle status of a Safe multisig transaction.\n *\n * Maps the raw Safe API status string to a normalized union. Useful for\n * one-shot status checks (e.g. resuming after page reload) without starting\n * a polling loop.\n */\nexport const getSafeMultisigTxStatus = Effect.fn(\"getSafeMultisigTxStatus\")(function* (\n safeTxHash: Hash\n) {\n const safeApps = yield* SafeAppsService;\n const queued = yield* safeApps.getTx(safeTxHash);\n return mapStatus(queued.status);\n});\n"]}
|