@asseteragmbh/wallet-connector 0.2.3 → 0.2.5

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.
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/protocol.ts"],"names":[],"mappings":"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","file":"protocol.cjs","sourcesContent":["/**\n * The pop-up protocol between a partner page (the opener) and the Assetera Wallet (the pop-up) — ADR-0054 D3/D4.\n *\n * This file is the CONTRACT. The connector in this package speaks the opener side; AsseteraWalletFront's\n * `/connect` page speaks the pop-up side and imports these types from `@asseteragmbh/wallet-connector/protocol`.\n * Change it only in a backwards-compatible way, or bump `PROTOCOL_VERSION` and support both on the wallet side:\n * partner sites upgrade on their own schedule.\n *\n * ## The handshake\n *\n * 1. The partner opens `${walletUrl}${CONNECT_PATH}?origin=<partner origin>&v=<version>` from a user click.\n * 2. The pop-up checks the claimed origin against its allowlist (D4). If listed, it posts `ready` to\n * `window.opener` with `targetOrigin` = that origin. The browser delivers it ONLY if the opener really is\n * that origin, so a page cannot claim someone else's origin and still hear the answer.\n * 3. The opener waits for `ready` from the wallet origin, then sends `request`s with `targetOrigin` = the wallet\n * origin. The pop-up accepts a message only when `event.origin` is the allowed origin from step 2 AND\n * `event.source === window.opener`, and answers with `targetOrigin` = that origin. Nothing else is accepted.\n *\n * ## Sign-in hint\n *\n * A partner may tell the wallet how the client signed in at the partner, through the optional `signIn` URL\n * parameter (`SIGN_IN_HINTS`). The hint only names an identity provider: the wallet checks it against its own\n * list, may ignore it and then falls back to its own login page. The hint never carries a token, an identity or\n * any other credential.\n *\n * ## Lifetime\n *\n * The pop-up is not guaranteed to stay open. The wallet site may close the `/connect` pop-up after it connected\n * or answered a request. The provider keeps the last `accounts` and `chainId` it saw, so the partner page still\n * looks connected after the pop-up closes, and it opens the pop-up again on the next request from a user click.\n * (ADR-0054, \"the wallet tab must stay open\", no longer holds.)\n *\n * ## Onboarding\n *\n * Onboarding is a second flow on the same wallet origin, for a partner that wants the client to get an Assetera\n * Wallet (a new one, or the one the client already has). The partner opens\n * `${walletUrl}${ONBOARD_PATH}?origin=<partner origin>&v=<version>&chain=<CAIP-2 chain id>[&add=1]` from a user\n * click. The pop-up checks the claimed origin against its partner list, signs the client in through SSO, runs the\n * wizard (accept the wallet terms, create the wallet) and then posts `onboarded` (or `onboard-cancelled`) to\n * `window.opener` with `targetOrigin` = the partner origin. `add=1` asks for one more wallet even when the client\n * already has one.\n *\n * After `onboarded` the pop-up STAYS OPEN and answers `request` messages for `personal_sign` by the onboarded\n * address (plus `eth_accounts` and `eth_chainId`), with the same `response` messages as `/connect`. That is how\n * the partner gets the ownership signature it needs to register the wallet with its own compliance service: the\n * wallet itself never calls a regulated service (ADR-0059 D3), and a second `window.open` after an asynchronous\n * step would be blocked by the browser. The partner closes the pop-up when it is done.\n *\n * The opener accepts messages only when `event.origin` is the wallet origin AND `event.source` is the pop-up\n * it opened. The wizard never returns a private key or a token, only the wallet address and signatures the\n * client approved.\n *\n * ## Manage\n *\n * The partner can open `${walletUrl}${MANAGE_PATH}?origin=<partner origin>&v=<version>[&lang=<locale>][&signIn=<hint>]`\n * from a user click, so the client can see and change the wallet accounts and the wallet security (passkeys,\n * authenticator app, recovery codes). The pop-up checks the claimed origin against its partner list, posts\n * `ready` to `window.opener` (the same message as `/connect`) and then posts `security` messages. It never\n * answers `request` messages.\n *\n * ## Account names\n *\n * While a pop-up is open it may also post `accounts` messages. Each entry names one account the client has\n * already shared with THIS partner, so the partner can show a name next to the address. The wallet never sends\n * an account the partner does not already know. A manage pop-up may send the message, and an onboarding pop-up\n * may send it after it reported the address. The message is only sent while a pop-up is open, never on request\n * of the partner. It carries no token and no other data.\n */\n\nexport const PROTOCOL_VERSION = 1 as const;\n\n/** The pop-up route on the wallet origin. */\nexport const CONNECT_PATH = \"/connect\";\n\n/** The onboarding wizard route on the wallet origin. */\nexport const ONBOARD_PATH = \"/onboard\";\n\n/** The manage wallet route on the wallet origin. */\nexport const MANAGE_PATH = \"/manage\";\n\n/** The identity providers a partner can name in the `signIn` hint. */\nexport const SIGN_IN_HINTS = [\"assetera-marketplace\"] as const;\n\n/** A named identity provider the wallet may use for a silent sign-in. */\nexport type SignInHint = (typeof SIGN_IN_HINTS)[number];\n\n/** True when `value` is one of {@link SIGN_IN_HINTS}. */\nexport function isSignInHint(value: unknown): value is SignInHint {\n return (SIGN_IN_HINTS as readonly unknown[]).includes(value);\n}\n\n/** Every message carries this prefix in `type`, so both sides can ignore unrelated `postMessage` traffic. */\nexport const MESSAGE_PREFIX = \"assetera-wallet:\" as const;\n\n/**\n * The EIP-1193 methods the pop-up answers (ADR-0054 D3). Anything else is refused with 4200. Read-only chain\n * calls (`eth_call`, `eth_getBalance`, …) do not go through the pop-up; wagmi sends them to its own transports.\n */\nexport const SUPPORTED_METHODS = [\n \"eth_requestAccounts\",\n \"eth_accounts\",\n \"eth_chainId\",\n \"wallet_switchEthereumChain\",\n \"personal_sign\",\n \"eth_signTypedData_v4\",\n \"eth_sendTransaction\",\n] as const;\n\nexport type SupportedMethod = (typeof SUPPORTED_METHODS)[number];\n\nexport function isSupportedMethod(method: string): method is SupportedMethod {\n return (SUPPORTED_METHODS as readonly string[]).includes(method);\n}\n\n/** EIP-1193 / EIP-1474 error codes the pop-up uses. */\nexport const RpcErrorCode = {\n /** The client rejected the request in the pop-up. */\n UserRejected: 4001,\n /** Not connected yet (no `eth_requestAccounts` approved in this pop-up). */\n Unauthorized: 4100,\n /** The method is not in SUPPORTED_METHODS. */\n UnsupportedMethod: 4200,\n /** The pop-up closed, or the wallet session ended. */\n Disconnected: 4900,\n /** `wallet_switchEthereumChain` to a chain the wallet does not offer. */\n UnrecognizedChain: 4902,\n /** Malformed params. */\n InvalidParams: -32602,\n /** Anything unexpected inside the wallet. */\n Internal: -32603,\n} as const;\n\nexport type RpcErrorCode = (typeof RpcErrorCode)[keyof typeof RpcErrorCode];\n\nexport interface RpcError {\n code: number;\n message: string;\n data?: unknown;\n}\n\n// --- pop-up → opener ---------------------------------------------------------------------------------\n\n/** The pop-up is loaded and accepts requests from this opener. */\nexport interface ReadyMessage {\n type: `${typeof MESSAGE_PREFIX}ready`;\n v: typeof PROTOCOL_VERSION;\n /** Shown to the partner for diagnostics only; the partner must not rely on it for anything. */\n walletVersion?: string;\n}\n\n/** The answer to one `request`, matched by `id`. Exactly one of `result` / `error`. */\nexport type ResponseMessage =\n | { type: `${typeof MESSAGE_PREFIX}response`; v: typeof PROTOCOL_VERSION; id: string; result: unknown }\n | { type: `${typeof MESSAGE_PREFIX}response`; v: typeof PROTOCOL_VERSION; id: string; error: RpcError };\n\n/** EIP-1193 provider events, forwarded to the partner. */\nexport type EventMessage =\n | {\n type: `${typeof MESSAGE_PREFIX}event`;\n v: typeof PROTOCOL_VERSION;\n event: \"accountsChanged\";\n data: `0x${string}`[];\n }\n | {\n type: `${typeof MESSAGE_PREFIX}event`;\n v: typeof PROTOCOL_VERSION;\n event: \"chainChanged\";\n data: `0x${string}`;\n }\n | {\n type: `${typeof MESSAGE_PREFIX}event`;\n v: typeof PROTOCOL_VERSION;\n event: \"disconnect\";\n data: RpcError;\n };\n\n/**\n * How well the client's Assetera Wallet is protected. The values are the ones the wallet site shows:\n *\n * - `add-factor`: there is no passkey and no authenticator app yet.\n * - `save-codes`: recovery codes exist, but the client has not confirmed saving them.\n * - `create-codes`: a factor exists, but there are no recovery codes.\n * - `protected`: a factor exists and the recovery codes are saved.\n * - `unknown`: the wallet could not read the state.\n */\nexport type WalletProtection = \"unknown\" | \"add-factor\" | \"save-codes\" | \"create-codes\" | \"protected\";\n\n/** The wallet security status sent by a manage (or connect) pop-up. */\nexport interface WalletSecurityStatus {\n /** Overall result, the same values the wallet site shows. */\n protection: WalletProtection;\n /** Passkeys registered for signing (Dynamic step-up), not sign-in factors. */\n passkeys: number;\n /** Authenticator apps (TOTP) registered for signing (Dynamic step-up). */\n authenticatorApps: number;\n}\n\n/**\n * The wallet reports the security status while a pop-up is open. The partner cannot ask for it. It never\n * carries a token, an identity or a credential.\n */\nexport interface SecurityMessage {\n type: `${typeof MESSAGE_PREFIX}security`;\n v: typeof PROTOCOL_VERSION;\n data: WalletSecurityStatus;\n}\n\n/** One account of the Assetera Wallet that the client already shared with this partner, with its name. */\nexport interface WalletAccountName {\n address: `0x${string}`;\n name: string;\n}\n\n/**\n * The wallet reports the names of the accounts the client already shared with this partner. The partner cannot\n * ask for it and the wallet never names an account the partner does not already know. It never carries a token\n * or any other data.\n */\nexport interface AccountsMessage {\n type: `${typeof MESSAGE_PREFIX}accounts`;\n v: typeof PROTOCOL_VERSION;\n data: WalletAccountName[];\n}\n\n/**\n * The onboarding wizard finished: the client accepted the wallet terms and has an Assetera Wallet on `chain`.\n * `created` is `true` when the wallet was created in this run, `false` when the wizard returned the client's\n * existing wallet. The wallet is not registered with any compliance service; that is the partner's step.\n */\nexport interface OnboardedMessage {\n type: `${typeof MESSAGE_PREFIX}onboarded`;\n v: typeof PROTOCOL_VERSION;\n address: `0x${string}`;\n chain: string;\n created: boolean;\n /**\n * @deprecated Sent by wallet versions that registered the wallet themselves (before ADR-0059). Ignored.\n */\n status?: \"approved\" | \"pending\";\n}\n\n/** The onboarding wizard did not produce a wallet. `message` is optional diagnostics for the partner. */\nexport interface OnboardCancelledMessage {\n type: `${typeof MESSAGE_PREFIX}onboard-cancelled`;\n v: typeof PROTOCOL_VERSION;\n reason: \"user\" | \"refused\" | \"error\";\n message?: string;\n}\n\nexport type WalletToPartnerMessage =\n | ReadyMessage\n | ResponseMessage\n | EventMessage\n | SecurityMessage\n | AccountsMessage\n | OnboardedMessage\n | OnboardCancelledMessage;\n\n// --- opener → pop-up ---------------------------------------------------------------------------------\n\nexport interface RequestMessage {\n type: `${typeof MESSAGE_PREFIX}request`;\n v: typeof PROTOCOL_VERSION;\n /** Unique per request within one connection; the response carries it back. */\n id: string;\n method: string;\n params?: unknown[] | Record<string, unknown>;\n}\n\n/** The partner disconnects (for example the user clicked \"Disconnect\" in the partner page). */\nexport interface DisconnectMessage {\n type: `${typeof MESSAGE_PREFIX}disconnect`;\n v: typeof PROTOCOL_VERSION;\n}\n\nexport type PartnerToWalletMessage = RequestMessage | DisconnectMessage;\n\n// --- guards ------------------------------------------------------------------------------------------\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null;\n}\n\nfunction hasType(value: unknown, suffix: string): value is Record<string, unknown> {\n return isRecord(value) && value.type === `${MESSAGE_PREFIX}${suffix}` && value.v === PROTOCOL_VERSION;\n}\n\nexport function isReadyMessage(value: unknown): value is ReadyMessage {\n return hasType(value, \"ready\");\n}\n\nexport function isResponseMessage(value: unknown): value is ResponseMessage {\n return (\n hasType(value, \"response\") &&\n typeof value.id === \"string\" &&\n (\"result\" in value || (isRecord(value.error) && typeof value.error.code === \"number\"))\n );\n}\n\nexport function isEventMessage(value: unknown): value is EventMessage {\n return (\n hasType(value, \"event\") &&\n (value.event === \"accountsChanged\" || value.event === \"chainChanged\" || value.event === \"disconnect\")\n );\n}\n\nconst WALLET_PROTECTIONS: readonly WalletProtection[] = [\n \"unknown\",\n \"add-factor\",\n \"save-codes\",\n \"create-codes\",\n \"protected\",\n];\n\n/** A factor count: a whole number, zero or more. */\nfunction isCount(value: unknown): value is number {\n return typeof value === \"number\" && Number.isInteger(value) && value >= 0;\n}\n\n/** True for a `security` message with a known `protection` value and whole, non-negative factor counts. */\nexport function isSecurityMessage(value: unknown): value is SecurityMessage {\n if (!hasType(value, \"security\")) return false;\n const data = value.data;\n return (\n isRecord(data) &&\n typeof data.protection === \"string\" &&\n (WALLET_PROTECTIONS as readonly string[]).includes(data.protection) &&\n isCount(data.passkeys) &&\n isCount(data.authenticatorApps)\n );\n}\n\n/** The largest number of accounts one `accounts` message may carry. */\nconst MAX_ACCOUNT_NAMES = 50;\n\n/** True when `value` has a control character: C0, DEL or C1. */\nfunction hasControlCharacter(value: string): boolean {\n for (const character of value) {\n const code = character.codePointAt(0) ?? 0;\n if (code <= 0x1f || (code >= 0x7f && code <= 0x9f)) return true;\n }\n return false;\n}\n\n/** A name: a string whose trimmed length is 1 to 64 and that has no control characters. */\nfunction isAccountName(value: unknown): value is string {\n if (typeof value !== \"string\") return false;\n if (hasControlCharacter(value)) return false;\n const trimmed = value.trim();\n return trimmed.length >= 1 && trimmed.length <= 64;\n}\n\n/** True for an `accounts` message with at most 50 named accounts. */\nexport function isAccountsMessage(value: unknown): value is AccountsMessage {\n if (!hasType(value, \"accounts\")) return false;\n const data = value.data;\n return (\n Array.isArray(data) &&\n data.length <= MAX_ACCOUNT_NAMES &&\n data.every(\n (entry: unknown) =>\n isRecord(entry) &&\n typeof entry.address === \"string\" &&\n EVM_ADDRESS_RE.test(entry.address) &&\n isAccountName(entry.name),\n )\n );\n}\n\nexport function isRequestMessage(value: unknown): value is RequestMessage {\n return hasType(value, \"request\") && typeof value.id === \"string\" && typeof value.method === \"string\";\n}\n\nexport function isDisconnectMessage(value: unknown): value is DisconnectMessage {\n return hasType(value, \"disconnect\");\n}\n\n/** `0x` followed by exactly 40 hexadecimal digits, the shape of an EVM address. */\nconst EVM_ADDRESS_RE = /^0x[0-9a-fA-F]{40}$/;\n\nexport function isOnboardedMessage(value: unknown): value is OnboardedMessage {\n return (\n hasType(value, \"onboarded\") &&\n typeof value.address === \"string\" &&\n EVM_ADDRESS_RE.test(value.address) &&\n typeof value.chain === \"string\" &&\n // `created` is new; an older wallet sent `status` instead and always created or reused a wallet.\n (typeof value.created === \"boolean\" || value.status === \"approved\" || value.status === \"pending\")\n );\n}\n\nexport function isOnboardCancelledMessage(value: unknown): value is OnboardCancelledMessage {\n return (\n hasType(value, \"onboard-cancelled\") &&\n (value.reason === \"user\" || value.reason === \"refused\" || value.reason === \"error\")\n );\n}\n\n/** A CAIP-2 EVM chain id, for example `eip155:80002` (Polygon Amoy) or `eip155:1` (mainnet). */\nexport function isCaip2EvmChain(value: string): boolean {\n return /^eip155:\\d{1,32}$/.test(value);\n}\n\n/**\n * The URL the partner opens. `partnerOrigin` is `window.location.origin` of the partner page. `signIn` adds the\n * optional sign-in hint; it is only a hint and the wallet may ignore it.\n */\nexport function connectUrl(walletUrl: string, partnerOrigin: string, signIn?: SignInHint): string {\n const url = new URL(CONNECT_PATH, walletUrl);\n url.searchParams.set(\"origin\", partnerOrigin);\n url.searchParams.set(\"v\", String(PROTOCOL_VERSION));\n if (signIn !== undefined) url.searchParams.set(\"signIn\", signIn);\n return url.toString();\n}\n\n/**\n * The URL the partner opens for the onboarding wizard. `partnerOrigin` is `window.location.origin` of the\n * partner page; `chain` is the CAIP-2 chain id the new wallet should be created on; `locale` is passed as `lang`\n * when the partner wants a specific wizard language. `signIn` adds the optional sign-in hint; it is only a hint\n * and the wallet may ignore it.\n */\nexport function onboardUrl(\n walletUrl: string,\n partnerOrigin: string,\n options: { chain: string; locale?: string; add?: boolean; signIn?: SignInHint },\n): string {\n const url = new URL(ONBOARD_PATH, walletUrl);\n url.searchParams.set(\"origin\", partnerOrigin);\n url.searchParams.set(\"v\", String(PROTOCOL_VERSION));\n url.searchParams.set(\"chain\", options.chain);\n if (options.locale !== undefined) url.searchParams.set(\"lang\", options.locale);\n if (options.add) url.searchParams.set(\"add\", \"1\");\n if (options.signIn !== undefined) url.searchParams.set(\"signIn\", options.signIn);\n return url.toString();\n}\n\n/**\n * The URL the partner opens to manage the wallet. `partnerOrigin` is `window.location.origin` of the partner\n * page. `locale` is passed as `lang` when the partner wants a specific language. `signIn` adds the optional\n * sign-in hint; it is only a hint and the wallet may ignore it.\n */\nexport function manageUrl(\n walletUrl: string,\n partnerOrigin: string,\n options: { locale?: string; signIn?: SignInHint } = {},\n): string {\n const url = new URL(MANAGE_PATH, walletUrl);\n url.searchParams.set(\"origin\", partnerOrigin);\n url.searchParams.set(\"v\", String(PROTOCOL_VERSION));\n if (options.locale !== undefined) url.searchParams.set(\"lang\", options.locale);\n if (options.signIn !== undefined) url.searchParams.set(\"signIn\", options.signIn);\n return url.toString();\n}\n"]}
1
+ {"version":3,"sources":["../src/protocol.ts"],"names":[],"mappings":"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","file":"protocol.cjs","sourcesContent":["/**\n * The pop-up protocol between a partner page (the opener) and the Assetera Wallet (the pop-up) — ADR-0054 D3/D4.\n *\n * This file is the CONTRACT. The connector in this package speaks the opener side; AsseteraWalletFront's\n * `/connect` page speaks the pop-up side and imports these types from `@asseteragmbh/wallet-connector/protocol`.\n * Change it only in a backwards-compatible way, or bump `PROTOCOL_VERSION` and support both on the wallet side:\n * partner sites upgrade on their own schedule.\n *\n * ## The handshake\n *\n * 1. The partner opens `${walletUrl}${CONNECT_PATH}?origin=<partner origin>&v=<version>` from a user click.\n * 2. The pop-up checks the claimed origin against its allowlist (D4). If listed, it posts `ready` to\n * `window.opener` with `targetOrigin` = that origin. The browser delivers it ONLY if the opener really is\n * that origin, so a page cannot claim someone else's origin and still hear the answer.\n * 3. The opener waits for `ready` from the wallet origin, then sends `request`s with `targetOrigin` = the wallet\n * origin. The pop-up accepts a message only when `event.origin` is the allowed origin from step 2 AND\n * `event.source === window.opener`, and answers with `targetOrigin` = that origin. Nothing else is accepted.\n *\n * ## Sign-in hint\n *\n * A partner may tell the wallet how the client signed in at the partner, through the optional `signIn` URL\n * parameter (`SIGN_IN_HINTS`). The hint only names an identity provider: the wallet checks it against its own\n * list, may ignore it and then falls back to its own login page. The hint never carries a token, an identity or\n * any other credential.\n *\n * ## Lifetime\n *\n * The pop-up is not guaranteed to stay open. The wallet site may close the `/connect` pop-up after it connected\n * or answered a request. The provider keeps the last `accounts` and `chainId` it saw, so the partner page still\n * looks connected after the pop-up closes, and it opens the pop-up again on the next request from a user click.\n * A partner that needs the same pop-up across several requests of one flow can hold it open with `hold`, see\n * below. (ADR-0054, \"the wallet tab must stay open\", no longer holds.)\n *\n * ## Hold\n *\n * The partner may send `hold` to a `/connect` pop-up. `hold: true` asks the wallet to stay open between\n * requests of one multi-step flow. `hold: false` ends the hold.\n *\n * The wallet accepts `hold` only under the same rule as `request`: `event.origin` is the verified partner origin\n * AND `event.source === window.opener`.\n *\n * `hold: true` is idempotent. Each one, and each request, restarts the `HOLD_MAX_MS` cap.\n *\n * After `hold: false` (or after the cap), the normal idle close applies: the wallet may close itself a short\n * time after the last answered request when nothing is pending.\n *\n * A hold never keeps a request open and never answers anything. It only stops the idle close.\n *\n * The partner sends `hold: true` again after each `ready` of a new pop-up while its hold is active.\n *\n * `/onboard` and `/manage` ignore `hold`.\n *\n * ## Onboarding\n *\n * Onboarding is a second flow on the same wallet origin, for a partner that wants the client to get an Assetera\n * Wallet (a new one, or the one the client already has). The partner opens\n * `${walletUrl}${ONBOARD_PATH}?origin=<partner origin>&v=<version>&chain=<CAIP-2 chain id>[&add=1]` from a user\n * click. The pop-up checks the claimed origin against its partner list, signs the client in through SSO, runs the\n * wizard (accept the wallet terms, create the wallet) and then posts `onboarded` (or `onboard-cancelled`) to\n * `window.opener` with `targetOrigin` = the partner origin. `add=1` asks for one more wallet even when the client\n * already has one.\n *\n * After `onboarded` the pop-up STAYS OPEN and answers `request` messages for `personal_sign` by the onboarded\n * address (plus `eth_accounts` and `eth_chainId`), with the same `response` messages as `/connect`. That is how\n * the partner gets the ownership signature it needs to register the wallet with its own compliance service: the\n * wallet itself never calls a regulated service (ADR-0059 D3), and a second `window.open` after an asynchronous\n * step would be blocked by the browser. The partner closes the pop-up when it is done.\n *\n * The opener accepts messages only when `event.origin` is the wallet origin AND `event.source` is the pop-up\n * it opened. The wizard never returns a private key or a token, only the wallet address and signatures the\n * client approved.\n *\n * ## Manage\n *\n * The partner can open `${walletUrl}${MANAGE_PATH}?origin=<partner origin>&v=<version>[&lang=<locale>][&signIn=<hint>]`\n * from a user click, so the client can see and change the wallet accounts and the wallet security (passkeys,\n * authenticator app, recovery codes). The pop-up checks the claimed origin against its partner list, posts\n * `ready` to `window.opener` (the same message as `/connect`) and then posts `security` messages. It never\n * answers `request` messages.\n *\n * ## Account names\n *\n * While a pop-up is open it may also post `accounts` messages. Each entry names one account the client has\n * already shared with THIS partner, so the partner can show a name next to the address. The wallet never sends\n * an account the partner does not already know. A manage pop-up may send the message, and an onboarding pop-up\n * may send it after it reported the address. The message is only sent while a pop-up is open, never on request\n * of the partner. It carries no token and no other data.\n */\n\nexport const PROTOCOL_VERSION = 1 as const;\n\n/** The pop-up route on the wallet origin. */\nexport const CONNECT_PATH = \"/connect\";\n\n/** The onboarding wizard route on the wallet origin. */\nexport const ONBOARD_PATH = \"/onboard\";\n\n/** The manage wallet route on the wallet origin. */\nexport const MANAGE_PATH = \"/manage\";\n\n/** The identity providers a partner can name in the `signIn` hint. */\nexport const SIGN_IN_HINTS = [\"assetera-marketplace\"] as const;\n\n/** A named identity provider the wallet may use for a silent sign-in. */\nexport type SignInHint = (typeof SIGN_IN_HINTS)[number];\n\n/** True when `value` is one of {@link SIGN_IN_HINTS}. */\nexport function isSignInHint(value: unknown): value is SignInHint {\n return (SIGN_IN_HINTS as readonly unknown[]).includes(value);\n}\n\n/** Every message carries this prefix in `type`, so both sides can ignore unrelated `postMessage` traffic. */\nexport const MESSAGE_PREFIX = \"assetera-wallet:\" as const;\n\n/**\n * The most time the wallet keeps a held pop-up open since the last `hold: true` message or the last request.\n * After it, the normal idle close applies again, so a hold that is never released cannot keep the window open\n * forever. Five minutes.\n */\nexport const HOLD_MAX_MS = 300_000;\n\n/**\n * The EIP-1193 methods the pop-up answers (ADR-0054 D3). Anything else is refused with 4200. Read-only chain\n * calls (`eth_call`, `eth_getBalance`, …) do not go through the pop-up; wagmi sends them to its own transports.\n */\nexport const SUPPORTED_METHODS = [\n \"eth_requestAccounts\",\n \"eth_accounts\",\n \"eth_chainId\",\n \"wallet_switchEthereumChain\",\n \"personal_sign\",\n \"eth_signTypedData_v4\",\n \"eth_sendTransaction\",\n] as const;\n\nexport type SupportedMethod = (typeof SUPPORTED_METHODS)[number];\n\nexport function isSupportedMethod(method: string): method is SupportedMethod {\n return (SUPPORTED_METHODS as readonly string[]).includes(method);\n}\n\n/** EIP-1193 / EIP-1474 error codes the pop-up uses. */\nexport const RpcErrorCode = {\n /** The client rejected the request in the pop-up. */\n UserRejected: 4001,\n /** Not connected yet (no `eth_requestAccounts` approved in this pop-up). */\n Unauthorized: 4100,\n /** The method is not in SUPPORTED_METHODS. */\n UnsupportedMethod: 4200,\n /** The pop-up closed, or the wallet session ended. */\n Disconnected: 4900,\n /** `wallet_switchEthereumChain` to a chain the wallet does not offer. */\n UnrecognizedChain: 4902,\n /** Malformed params. */\n InvalidParams: -32602,\n /** Anything unexpected inside the wallet. */\n Internal: -32603,\n} as const;\n\nexport type RpcErrorCode = (typeof RpcErrorCode)[keyof typeof RpcErrorCode];\n\nexport interface RpcError {\n code: number;\n message: string;\n data?: unknown;\n}\n\n// --- pop-up → opener ---------------------------------------------------------------------------------\n\n/** The pop-up is loaded and accepts requests from this opener. */\nexport interface ReadyMessage {\n type: `${typeof MESSAGE_PREFIX}ready`;\n v: typeof PROTOCOL_VERSION;\n /** Shown to the partner for diagnostics only; the partner must not rely on it for anything. */\n walletVersion?: string;\n}\n\n/** The answer to one `request`, matched by `id`. Exactly one of `result` / `error`. */\nexport type ResponseMessage =\n | { type: `${typeof MESSAGE_PREFIX}response`; v: typeof PROTOCOL_VERSION; id: string; result: unknown }\n | { type: `${typeof MESSAGE_PREFIX}response`; v: typeof PROTOCOL_VERSION; id: string; error: RpcError };\n\n/** EIP-1193 provider events, forwarded to the partner. */\nexport type EventMessage =\n | {\n type: `${typeof MESSAGE_PREFIX}event`;\n v: typeof PROTOCOL_VERSION;\n event: \"accountsChanged\";\n data: `0x${string}`[];\n }\n | {\n type: `${typeof MESSAGE_PREFIX}event`;\n v: typeof PROTOCOL_VERSION;\n event: \"chainChanged\";\n data: `0x${string}`;\n }\n | {\n type: `${typeof MESSAGE_PREFIX}event`;\n v: typeof PROTOCOL_VERSION;\n event: \"disconnect\";\n data: RpcError;\n };\n\n/**\n * How well the client's Assetera Wallet is protected. The values are the ones the wallet site shows:\n *\n * - `add-factor`: there is no passkey and no authenticator app yet.\n * - `save-codes`: recovery codes exist, but the client has not confirmed saving them.\n * - `create-codes`: a factor exists, but there are no recovery codes.\n * - `protected`: a factor exists and the recovery codes are saved.\n * - `unknown`: the wallet could not read the state.\n */\nexport type WalletProtection = \"unknown\" | \"add-factor\" | \"save-codes\" | \"create-codes\" | \"protected\";\n\n/** The wallet security status sent by a manage (or connect) pop-up. */\nexport interface WalletSecurityStatus {\n /** Overall result, the same values the wallet site shows. */\n protection: WalletProtection;\n /** Passkeys registered for signing (Dynamic step-up), not sign-in factors. */\n passkeys: number;\n /** Authenticator apps (TOTP) registered for signing (Dynamic step-up). */\n authenticatorApps: number;\n}\n\n/**\n * The wallet reports the security status while a pop-up is open. The partner cannot ask for it. It never\n * carries a token, an identity or a credential.\n */\nexport interface SecurityMessage {\n type: `${typeof MESSAGE_PREFIX}security`;\n v: typeof PROTOCOL_VERSION;\n data: WalletSecurityStatus;\n}\n\n/** One account of the Assetera Wallet that the client already shared with this partner, with its name. */\nexport interface WalletAccountName {\n address: `0x${string}`;\n name: string;\n}\n\n/**\n * The wallet reports the names of the accounts the client already shared with this partner. The partner cannot\n * ask for it and the wallet never names an account the partner does not already know. It never carries a token\n * or any other data.\n */\nexport interface AccountsMessage {\n type: `${typeof MESSAGE_PREFIX}accounts`;\n v: typeof PROTOCOL_VERSION;\n data: WalletAccountName[];\n}\n\n/**\n * The onboarding wizard finished: the client accepted the wallet terms and has an Assetera Wallet on `chain`.\n * `created` is `true` when the wallet was created in this run, `false` when the wizard returned the client's\n * existing wallet. The wallet is not registered with any compliance service; that is the partner's step.\n */\nexport interface OnboardedMessage {\n type: `${typeof MESSAGE_PREFIX}onboarded`;\n v: typeof PROTOCOL_VERSION;\n address: `0x${string}`;\n chain: string;\n created: boolean;\n /**\n * @deprecated Sent by wallet versions that registered the wallet themselves (before ADR-0059). Ignored.\n */\n status?: \"approved\" | \"pending\";\n}\n\n/** The onboarding wizard did not produce a wallet. `message` is optional diagnostics for the partner. */\nexport interface OnboardCancelledMessage {\n type: `${typeof MESSAGE_PREFIX}onboard-cancelled`;\n v: typeof PROTOCOL_VERSION;\n reason: \"user\" | \"refused\" | \"error\";\n message?: string;\n}\n\nexport type WalletToPartnerMessage =\n | ReadyMessage\n | ResponseMessage\n | EventMessage\n | SecurityMessage\n | AccountsMessage\n | OnboardedMessage\n | OnboardCancelledMessage;\n\n// --- opener → pop-up ---------------------------------------------------------------------------------\n\nexport interface RequestMessage {\n type: `${typeof MESSAGE_PREFIX}request`;\n v: typeof PROTOCOL_VERSION;\n /** Unique per request within one connection; the response carries it back. */\n id: string;\n method: string;\n params?: unknown[] | Record<string, unknown>;\n}\n\n/** The partner disconnects (for example the user clicked \"Disconnect\" in the partner page). */\nexport interface DisconnectMessage {\n type: `${typeof MESSAGE_PREFIX}disconnect`;\n v: typeof PROTOCOL_VERSION;\n}\n\n/**\n * The partner asks the wallet to keep the pop-up open between requests of one multi-step flow, or to stop\n * holding it. The wallet accepts it only from the verified partner origin and only from `window.opener`.\n */\nexport interface HoldMessage {\n type: `${typeof MESSAGE_PREFIX}hold`;\n v: typeof PROTOCOL_VERSION;\n /** `true`: keep the pop-up open between requests. `false`: the hold ends. */\n hold: boolean;\n}\n\nexport type PartnerToWalletMessage = RequestMessage | DisconnectMessage | HoldMessage;\n\n// --- guards ------------------------------------------------------------------------------------------\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null;\n}\n\nfunction hasType(value: unknown, suffix: string): value is Record<string, unknown> {\n return isRecord(value) && value.type === `${MESSAGE_PREFIX}${suffix}` && value.v === PROTOCOL_VERSION;\n}\n\nexport function isReadyMessage(value: unknown): value is ReadyMessage {\n return hasType(value, \"ready\");\n}\n\nexport function isResponseMessage(value: unknown): value is ResponseMessage {\n return (\n hasType(value, \"response\") &&\n typeof value.id === \"string\" &&\n (\"result\" in value || (isRecord(value.error) && typeof value.error.code === \"number\"))\n );\n}\n\nexport function isEventMessage(value: unknown): value is EventMessage {\n return (\n hasType(value, \"event\") &&\n (value.event === \"accountsChanged\" || value.event === \"chainChanged\" || value.event === \"disconnect\")\n );\n}\n\nconst WALLET_PROTECTIONS: readonly WalletProtection[] = [\n \"unknown\",\n \"add-factor\",\n \"save-codes\",\n \"create-codes\",\n \"protected\",\n];\n\n/** A factor count: a whole number, zero or more. */\nfunction isCount(value: unknown): value is number {\n return typeof value === \"number\" && Number.isInteger(value) && value >= 0;\n}\n\n/** True for a `security` message with a known `protection` value and whole, non-negative factor counts. */\nexport function isSecurityMessage(value: unknown): value is SecurityMessage {\n if (!hasType(value, \"security\")) return false;\n const data = value.data;\n return (\n isRecord(data) &&\n typeof data.protection === \"string\" &&\n (WALLET_PROTECTIONS as readonly string[]).includes(data.protection) &&\n isCount(data.passkeys) &&\n isCount(data.authenticatorApps)\n );\n}\n\n/** The largest number of accounts one `accounts` message may carry. */\nconst MAX_ACCOUNT_NAMES = 50;\n\n/** True when `value` has a control character: C0, DEL or C1. */\nfunction hasControlCharacter(value: string): boolean {\n for (const character of value) {\n const code = character.codePointAt(0) ?? 0;\n if (code <= 0x1f || (code >= 0x7f && code <= 0x9f)) return true;\n }\n return false;\n}\n\n/** A name: a string whose trimmed length is 1 to 64 and that has no control characters. */\nfunction isAccountName(value: unknown): value is string {\n if (typeof value !== \"string\") return false;\n if (hasControlCharacter(value)) return false;\n const trimmed = value.trim();\n return trimmed.length >= 1 && trimmed.length <= 64;\n}\n\n/** True for an `accounts` message with at most 50 named accounts. */\nexport function isAccountsMessage(value: unknown): value is AccountsMessage {\n if (!hasType(value, \"accounts\")) return false;\n const data = value.data;\n return (\n Array.isArray(data) &&\n data.length <= MAX_ACCOUNT_NAMES &&\n data.every(\n (entry: unknown) =>\n isRecord(entry) &&\n typeof entry.address === \"string\" &&\n EVM_ADDRESS_RE.test(entry.address) &&\n isAccountName(entry.name),\n )\n );\n}\n\nexport function isRequestMessage(value: unknown): value is RequestMessage {\n return hasType(value, \"request\") && typeof value.id === \"string\" && typeof value.method === \"string\";\n}\n\nexport function isDisconnectMessage(value: unknown): value is DisconnectMessage {\n return hasType(value, \"disconnect\");\n}\n\nexport function isHoldMessage(value: unknown): value is HoldMessage {\n return hasType(value, \"hold\") && typeof value.hold === \"boolean\";\n}\n\n/** `0x` followed by exactly 40 hexadecimal digits, the shape of an EVM address. */\nconst EVM_ADDRESS_RE = /^0x[0-9a-fA-F]{40}$/;\n\nexport function isOnboardedMessage(value: unknown): value is OnboardedMessage {\n return (\n hasType(value, \"onboarded\") &&\n typeof value.address === \"string\" &&\n EVM_ADDRESS_RE.test(value.address) &&\n typeof value.chain === \"string\" &&\n // `created` is new; an older wallet sent `status` instead and always created or reused a wallet.\n (typeof value.created === \"boolean\" || value.status === \"approved\" || value.status === \"pending\")\n );\n}\n\nexport function isOnboardCancelledMessage(value: unknown): value is OnboardCancelledMessage {\n return (\n hasType(value, \"onboard-cancelled\") &&\n (value.reason === \"user\" || value.reason === \"refused\" || value.reason === \"error\")\n );\n}\n\n/** A CAIP-2 EVM chain id, for example `eip155:80002` (Polygon Amoy) or `eip155:1` (mainnet). */\nexport function isCaip2EvmChain(value: string): boolean {\n return /^eip155:\\d{1,32}$/.test(value);\n}\n\n/**\n * The URL the partner opens. `partnerOrigin` is `window.location.origin` of the partner page. `signIn` adds the\n * optional sign-in hint; it is only a hint and the wallet may ignore it.\n */\nexport function connectUrl(walletUrl: string, partnerOrigin: string, signIn?: SignInHint): string {\n const url = new URL(CONNECT_PATH, walletUrl);\n url.searchParams.set(\"origin\", partnerOrigin);\n url.searchParams.set(\"v\", String(PROTOCOL_VERSION));\n if (signIn !== undefined) url.searchParams.set(\"signIn\", signIn);\n return url.toString();\n}\n\n/**\n * The URL the partner opens for the onboarding wizard. `partnerOrigin` is `window.location.origin` of the\n * partner page; `chain` is the CAIP-2 chain id the new wallet should be created on; `locale` is passed as `lang`\n * when the partner wants a specific wizard language. `signIn` adds the optional sign-in hint; it is only a hint\n * and the wallet may ignore it.\n */\nexport function onboardUrl(\n walletUrl: string,\n partnerOrigin: string,\n options: { chain: string; locale?: string; add?: boolean; signIn?: SignInHint },\n): string {\n const url = new URL(ONBOARD_PATH, walletUrl);\n url.searchParams.set(\"origin\", partnerOrigin);\n url.searchParams.set(\"v\", String(PROTOCOL_VERSION));\n url.searchParams.set(\"chain\", options.chain);\n if (options.locale !== undefined) url.searchParams.set(\"lang\", options.locale);\n if (options.add) url.searchParams.set(\"add\", \"1\");\n if (options.signIn !== undefined) url.searchParams.set(\"signIn\", options.signIn);\n return url.toString();\n}\n\n/**\n * The URL the partner opens to manage the wallet. `partnerOrigin` is `window.location.origin` of the partner\n * page. `locale` is passed as `lang` when the partner wants a specific language. `signIn` adds the optional\n * sign-in hint; it is only a hint and the wallet may ignore it.\n */\nexport function manageUrl(\n walletUrl: string,\n partnerOrigin: string,\n options: { locale?: string; signIn?: SignInHint } = {},\n): string {\n const url = new URL(MANAGE_PATH, walletUrl);\n url.searchParams.set(\"origin\", partnerOrigin);\n url.searchParams.set(\"v\", String(PROTOCOL_VERSION));\n if (options.locale !== undefined) url.searchParams.set(\"lang\", options.locale);\n if (options.signIn !== undefined) url.searchParams.set(\"signIn\", options.signIn);\n return url.toString();\n}\n"]}
@@ -28,7 +28,27 @@
28
28
  * The pop-up is not guaranteed to stay open. The wallet site may close the `/connect` pop-up after it connected
29
29
  * or answered a request. The provider keeps the last `accounts` and `chainId` it saw, so the partner page still
30
30
  * looks connected after the pop-up closes, and it opens the pop-up again on the next request from a user click.
31
- * (ADR-0054, "the wallet tab must stay open", no longer holds.)
31
+ * A partner that needs the same pop-up across several requests of one flow can hold it open with `hold`, see
32
+ * below. (ADR-0054, "the wallet tab must stay open", no longer holds.)
33
+ *
34
+ * ## Hold
35
+ *
36
+ * The partner may send `hold` to a `/connect` pop-up. `hold: true` asks the wallet to stay open between
37
+ * requests of one multi-step flow. `hold: false` ends the hold.
38
+ *
39
+ * The wallet accepts `hold` only under the same rule as `request`: `event.origin` is the verified partner origin
40
+ * AND `event.source === window.opener`.
41
+ *
42
+ * `hold: true` is idempotent. Each one, and each request, restarts the `HOLD_MAX_MS` cap.
43
+ *
44
+ * After `hold: false` (or after the cap), the normal idle close applies: the wallet may close itself a short
45
+ * time after the last answered request when nothing is pending.
46
+ *
47
+ * A hold never keeps a request open and never answers anything. It only stops the idle close.
48
+ *
49
+ * The partner sends `hold: true` again after each `ready` of a new pop-up while its hold is active.
50
+ *
51
+ * `/onboard` and `/manage` ignore `hold`.
32
52
  *
33
53
  * ## Onboarding
34
54
  *
@@ -81,6 +101,12 @@ type SignInHint = (typeof SIGN_IN_HINTS)[number];
81
101
  declare function isSignInHint(value: unknown): value is SignInHint;
82
102
  /** Every message carries this prefix in `type`, so both sides can ignore unrelated `postMessage` traffic. */
83
103
  declare const MESSAGE_PREFIX: "assetera-wallet:";
104
+ /**
105
+ * The most time the wallet keeps a held pop-up open since the last `hold: true` message or the last request.
106
+ * After it, the normal idle close applies again, so a hold that is never released cannot keep the window open
107
+ * forever. Five minutes.
108
+ */
109
+ declare const HOLD_MAX_MS = 300000;
84
110
  /**
85
111
  * The EIP-1193 methods the pop-up answers (ADR-0054 D3). Anything else is refused with 4200. Read-only chain
86
112
  * calls (`eth_call`, `eth_getBalance`, …) do not go through the pop-up; wagmi sends them to its own transports.
@@ -227,7 +253,17 @@ interface DisconnectMessage {
227
253
  type: `${typeof MESSAGE_PREFIX}disconnect`;
228
254
  v: typeof PROTOCOL_VERSION;
229
255
  }
230
- type PartnerToWalletMessage = RequestMessage | DisconnectMessage;
256
+ /**
257
+ * The partner asks the wallet to keep the pop-up open between requests of one multi-step flow, or to stop
258
+ * holding it. The wallet accepts it only from the verified partner origin and only from `window.opener`.
259
+ */
260
+ interface HoldMessage {
261
+ type: `${typeof MESSAGE_PREFIX}hold`;
262
+ v: typeof PROTOCOL_VERSION;
263
+ /** `true`: keep the pop-up open between requests. `false`: the hold ends. */
264
+ hold: boolean;
265
+ }
266
+ type PartnerToWalletMessage = RequestMessage | DisconnectMessage | HoldMessage;
231
267
  declare function isReadyMessage(value: unknown): value is ReadyMessage;
232
268
  declare function isResponseMessage(value: unknown): value is ResponseMessage;
233
269
  declare function isEventMessage(value: unknown): value is EventMessage;
@@ -237,6 +273,7 @@ declare function isSecurityMessage(value: unknown): value is SecurityMessage;
237
273
  declare function isAccountsMessage(value: unknown): value is AccountsMessage;
238
274
  declare function isRequestMessage(value: unknown): value is RequestMessage;
239
275
  declare function isDisconnectMessage(value: unknown): value is DisconnectMessage;
276
+ declare function isHoldMessage(value: unknown): value is HoldMessage;
240
277
  declare function isOnboardedMessage(value: unknown): value is OnboardedMessage;
241
278
  declare function isOnboardCancelledMessage(value: unknown): value is OnboardCancelledMessage;
242
279
  /** A CAIP-2 EVM chain id, for example `eip155:80002` (Polygon Amoy) or `eip155:1` (mainnet). */
@@ -268,4 +305,4 @@ declare function manageUrl(walletUrl: string, partnerOrigin: string, options?: {
268
305
  signIn?: SignInHint;
269
306
  }): string;
270
307
 
271
- export { type AccountsMessage, CONNECT_PATH, type DisconnectMessage, type EventMessage, MANAGE_PATH, MESSAGE_PREFIX, ONBOARD_PATH, type OnboardCancelledMessage, type OnboardedMessage, PROTOCOL_VERSION, type PartnerToWalletMessage, type ReadyMessage, type RequestMessage, type ResponseMessage, type RpcError, RpcErrorCode, SIGN_IN_HINTS, SUPPORTED_METHODS, type SecurityMessage, type SignInHint, type SupportedMethod, type WalletAccountName, type WalletProtection, type WalletSecurityStatus, type WalletToPartnerMessage, connectUrl, isAccountsMessage, isCaip2EvmChain, isDisconnectMessage, isEventMessage, isOnboardCancelledMessage, isOnboardedMessage, isReadyMessage, isRequestMessage, isResponseMessage, isSecurityMessage, isSignInHint, isSupportedMethod, manageUrl, onboardUrl };
308
+ export { type AccountsMessage, CONNECT_PATH, type DisconnectMessage, type EventMessage, HOLD_MAX_MS, type HoldMessage, MANAGE_PATH, MESSAGE_PREFIX, ONBOARD_PATH, type OnboardCancelledMessage, type OnboardedMessage, PROTOCOL_VERSION, type PartnerToWalletMessage, type ReadyMessage, type RequestMessage, type ResponseMessage, type RpcError, RpcErrorCode, SIGN_IN_HINTS, SUPPORTED_METHODS, type SecurityMessage, type SignInHint, type SupportedMethod, type WalletAccountName, type WalletProtection, type WalletSecurityStatus, type WalletToPartnerMessage, connectUrl, isAccountsMessage, isCaip2EvmChain, isDisconnectMessage, isEventMessage, isHoldMessage, isOnboardCancelledMessage, isOnboardedMessage, isReadyMessage, isRequestMessage, isResponseMessage, isSecurityMessage, isSignInHint, isSupportedMethod, manageUrl, onboardUrl };
@@ -28,7 +28,27 @@
28
28
  * The pop-up is not guaranteed to stay open. The wallet site may close the `/connect` pop-up after it connected
29
29
  * or answered a request. The provider keeps the last `accounts` and `chainId` it saw, so the partner page still
30
30
  * looks connected after the pop-up closes, and it opens the pop-up again on the next request from a user click.
31
- * (ADR-0054, "the wallet tab must stay open", no longer holds.)
31
+ * A partner that needs the same pop-up across several requests of one flow can hold it open with `hold`, see
32
+ * below. (ADR-0054, "the wallet tab must stay open", no longer holds.)
33
+ *
34
+ * ## Hold
35
+ *
36
+ * The partner may send `hold` to a `/connect` pop-up. `hold: true` asks the wallet to stay open between
37
+ * requests of one multi-step flow. `hold: false` ends the hold.
38
+ *
39
+ * The wallet accepts `hold` only under the same rule as `request`: `event.origin` is the verified partner origin
40
+ * AND `event.source === window.opener`.
41
+ *
42
+ * `hold: true` is idempotent. Each one, and each request, restarts the `HOLD_MAX_MS` cap.
43
+ *
44
+ * After `hold: false` (or after the cap), the normal idle close applies: the wallet may close itself a short
45
+ * time after the last answered request when nothing is pending.
46
+ *
47
+ * A hold never keeps a request open and never answers anything. It only stops the idle close.
48
+ *
49
+ * The partner sends `hold: true` again after each `ready` of a new pop-up while its hold is active.
50
+ *
51
+ * `/onboard` and `/manage` ignore `hold`.
32
52
  *
33
53
  * ## Onboarding
34
54
  *
@@ -81,6 +101,12 @@ type SignInHint = (typeof SIGN_IN_HINTS)[number];
81
101
  declare function isSignInHint(value: unknown): value is SignInHint;
82
102
  /** Every message carries this prefix in `type`, so both sides can ignore unrelated `postMessage` traffic. */
83
103
  declare const MESSAGE_PREFIX: "assetera-wallet:";
104
+ /**
105
+ * The most time the wallet keeps a held pop-up open since the last `hold: true` message or the last request.
106
+ * After it, the normal idle close applies again, so a hold that is never released cannot keep the window open
107
+ * forever. Five minutes.
108
+ */
109
+ declare const HOLD_MAX_MS = 300000;
84
110
  /**
85
111
  * The EIP-1193 methods the pop-up answers (ADR-0054 D3). Anything else is refused with 4200. Read-only chain
86
112
  * calls (`eth_call`, `eth_getBalance`, …) do not go through the pop-up; wagmi sends them to its own transports.
@@ -227,7 +253,17 @@ interface DisconnectMessage {
227
253
  type: `${typeof MESSAGE_PREFIX}disconnect`;
228
254
  v: typeof PROTOCOL_VERSION;
229
255
  }
230
- type PartnerToWalletMessage = RequestMessage | DisconnectMessage;
256
+ /**
257
+ * The partner asks the wallet to keep the pop-up open between requests of one multi-step flow, or to stop
258
+ * holding it. The wallet accepts it only from the verified partner origin and only from `window.opener`.
259
+ */
260
+ interface HoldMessage {
261
+ type: `${typeof MESSAGE_PREFIX}hold`;
262
+ v: typeof PROTOCOL_VERSION;
263
+ /** `true`: keep the pop-up open between requests. `false`: the hold ends. */
264
+ hold: boolean;
265
+ }
266
+ type PartnerToWalletMessage = RequestMessage | DisconnectMessage | HoldMessage;
231
267
  declare function isReadyMessage(value: unknown): value is ReadyMessage;
232
268
  declare function isResponseMessage(value: unknown): value is ResponseMessage;
233
269
  declare function isEventMessage(value: unknown): value is EventMessage;
@@ -237,6 +273,7 @@ declare function isSecurityMessage(value: unknown): value is SecurityMessage;
237
273
  declare function isAccountsMessage(value: unknown): value is AccountsMessage;
238
274
  declare function isRequestMessage(value: unknown): value is RequestMessage;
239
275
  declare function isDisconnectMessage(value: unknown): value is DisconnectMessage;
276
+ declare function isHoldMessage(value: unknown): value is HoldMessage;
240
277
  declare function isOnboardedMessage(value: unknown): value is OnboardedMessage;
241
278
  declare function isOnboardCancelledMessage(value: unknown): value is OnboardCancelledMessage;
242
279
  /** A CAIP-2 EVM chain id, for example `eip155:80002` (Polygon Amoy) or `eip155:1` (mainnet). */
@@ -268,4 +305,4 @@ declare function manageUrl(walletUrl: string, partnerOrigin: string, options?: {
268
305
  signIn?: SignInHint;
269
306
  }): string;
270
307
 
271
- export { type AccountsMessage, CONNECT_PATH, type DisconnectMessage, type EventMessage, MANAGE_PATH, MESSAGE_PREFIX, ONBOARD_PATH, type OnboardCancelledMessage, type OnboardedMessage, PROTOCOL_VERSION, type PartnerToWalletMessage, type ReadyMessage, type RequestMessage, type ResponseMessage, type RpcError, RpcErrorCode, SIGN_IN_HINTS, SUPPORTED_METHODS, type SecurityMessage, type SignInHint, type SupportedMethod, type WalletAccountName, type WalletProtection, type WalletSecurityStatus, type WalletToPartnerMessage, connectUrl, isAccountsMessage, isCaip2EvmChain, isDisconnectMessage, isEventMessage, isOnboardCancelledMessage, isOnboardedMessage, isReadyMessage, isRequestMessage, isResponseMessage, isSecurityMessage, isSignInHint, isSupportedMethod, manageUrl, onboardUrl };
308
+ export { type AccountsMessage, CONNECT_PATH, type DisconnectMessage, type EventMessage, HOLD_MAX_MS, type HoldMessage, MANAGE_PATH, MESSAGE_PREFIX, ONBOARD_PATH, type OnboardCancelledMessage, type OnboardedMessage, PROTOCOL_VERSION, type PartnerToWalletMessage, type ReadyMessage, type RequestMessage, type ResponseMessage, type RpcError, RpcErrorCode, SIGN_IN_HINTS, SUPPORTED_METHODS, type SecurityMessage, type SignInHint, type SupportedMethod, type WalletAccountName, type WalletProtection, type WalletSecurityStatus, type WalletToPartnerMessage, connectUrl, isAccountsMessage, isCaip2EvmChain, isDisconnectMessage, isEventMessage, isHoldMessage, isOnboardCancelledMessage, isOnboardedMessage, isReadyMessage, isRequestMessage, isResponseMessage, isSecurityMessage, isSignInHint, isSupportedMethod, manageUrl, onboardUrl };
package/dist/protocol.js CHANGED
@@ -1,3 +1,3 @@
1
- export { CONNECT_PATH, MANAGE_PATH, MESSAGE_PREFIX, ONBOARD_PATH, PROTOCOL_VERSION, RpcErrorCode, SIGN_IN_HINTS, SUPPORTED_METHODS, connectUrl, isAccountsMessage, isCaip2EvmChain, isDisconnectMessage, isEventMessage, isOnboardCancelledMessage, isOnboardedMessage, isReadyMessage, isRequestMessage, isResponseMessage, isSecurityMessage, isSignInHint, isSupportedMethod, manageUrl, onboardUrl } from './chunk-ZEOGKINA.js';
1
+ export { CONNECT_PATH, HOLD_MAX_MS, MANAGE_PATH, MESSAGE_PREFIX, ONBOARD_PATH, PROTOCOL_VERSION, RpcErrorCode, SIGN_IN_HINTS, SUPPORTED_METHODS, connectUrl, isAccountsMessage, isCaip2EvmChain, isDisconnectMessage, isEventMessage, isHoldMessage, isOnboardCancelledMessage, isOnboardedMessage, isReadyMessage, isRequestMessage, isResponseMessage, isSecurityMessage, isSignInHint, isSupportedMethod, manageUrl, onboardUrl } from './chunk-Z7QSH4N2.js';
2
2
  //# sourceMappingURL=protocol.js.map
3
3
  //# sourceMappingURL=protocol.js.map
@@ -40,18 +40,46 @@ interface AsseteraWalletProvider {
40
40
  * Open the pop-up NOW, inside the caller's click, for a signature that is sent later. Use it when the app must
41
41
  * wait for something (a firm quote, a fee) between the click and the signing request: without it, the request
42
42
  * would open the pop-up after an `await`, outside the click, and the browser would block it. A later
43
- * `request` reuses the pop-up. Synchronous; it never throws.
43
+ * `request` reuses the pop-up. While no request has followed yet, the handle also holds the pop-up open (like
44
+ * {@link holdPopup}), so a pop-up that is already counting down its idle close does not vanish during the
45
+ * wait. The hold ends when the first request sent after this call settles (resolved or rejected), or on
46
+ * `release()`, whichever comes first. Synchronous; it never throws.
44
47
  */
45
48
  prepareSigning(): PreparedSigning;
49
+ /**
50
+ * Open the pop-up NOW, inside the caller's click, and keep it open until `release()`.
51
+ *
52
+ * Use it when one user click starts several wallet requests with waits between them (approve a token, wait
53
+ * for the approval to mine, sign the terms, send the transaction). The wallet normally closes the pop-up a
54
+ * short time after the last answered request, and a request sent after that would open a new pop-up outside
55
+ * a click, which the browser blocks. `holdPopup()` asks the wallet to keep the current pop-up open between
56
+ * those requests. Call `release()` in a `finally` block.
57
+ *
58
+ * The hold is a request to the wallet, not a guarantee: an older wallet ignores the message, and the wallet
59
+ * caps a hold at `HOLD_MAX_MS` (5 minutes) since the last hold or request. Keep a fallback for a closed
60
+ * pop-up. Synchronous; it never throws.
61
+ */
62
+ holdPopup(): PopupHold;
46
63
  }
47
64
  /** The handle {@link AsseteraWalletProvider.prepareSigning} returns. */
48
65
  interface PreparedSigning {
49
66
  /** `false` when the browser blocked the pop-up. The caller should then ask for another click. */
50
67
  readonly opened: boolean;
51
68
  /**
52
- * Give the pop-up back when no request followed (the quote failed, the client cancelled). It closes the
53
- * pop-up only when THIS call opened it and no request was sent since; otherwise it does nothing. Safe to call
54
- * more than once.
69
+ * Give the pop-up back when no request followed (the quote failed, the client cancelled). It ends the hold
70
+ * this handle started and posts `hold: false` when no other hold is active. It closes the pop-up only when
71
+ * THIS call opened it and no request was sent since; otherwise it does nothing. Safe to call more than once.
72
+ */
73
+ release(): void;
74
+ }
75
+ /** The handle {@link AsseteraWalletProvider.holdPopup} returns. */
76
+ interface PopupHold {
77
+ /** `false` when the browser blocked the pop-up. The caller should then ask for another click. */
78
+ readonly opened: boolean;
79
+ /**
80
+ * End the hold. When no other hold is active the provider posts `hold: false` to the current pop-up. It
81
+ * closes the pop-up only when this call opened it and no request was sent since; otherwise it does nothing.
82
+ * Safe to call more than once.
55
83
  */
56
84
  release(): void;
57
85
  }
@@ -83,4 +111,4 @@ declare class ProviderRpcError extends Error {
83
111
  */
84
112
  declare function createAsseteraWalletProvider(options: AsseteraWalletProviderOptions): AsseteraWalletProvider;
85
113
 
86
- export { type AsseteraWalletProvider as A, type PreparedSigning as P, type RequestArguments as R, type AsseteraWalletProviderOptions as a, type ProviderEvent as b, type ProviderListener as c, ProviderRpcError as d, createAsseteraWalletProvider as e };
114
+ export { type AsseteraWalletProvider as A, type PopupHold as P, type RequestArguments as R, type AsseteraWalletProviderOptions as a, type PreparedSigning as b, type ProviderEvent as c, type ProviderListener as d, ProviderRpcError as e, createAsseteraWalletProvider as f };
@@ -40,18 +40,46 @@ interface AsseteraWalletProvider {
40
40
  * Open the pop-up NOW, inside the caller's click, for a signature that is sent later. Use it when the app must
41
41
  * wait for something (a firm quote, a fee) between the click and the signing request: without it, the request
42
42
  * would open the pop-up after an `await`, outside the click, and the browser would block it. A later
43
- * `request` reuses the pop-up. Synchronous; it never throws.
43
+ * `request` reuses the pop-up. While no request has followed yet, the handle also holds the pop-up open (like
44
+ * {@link holdPopup}), so a pop-up that is already counting down its idle close does not vanish during the
45
+ * wait. The hold ends when the first request sent after this call settles (resolved or rejected), or on
46
+ * `release()`, whichever comes first. Synchronous; it never throws.
44
47
  */
45
48
  prepareSigning(): PreparedSigning;
49
+ /**
50
+ * Open the pop-up NOW, inside the caller's click, and keep it open until `release()`.
51
+ *
52
+ * Use it when one user click starts several wallet requests with waits between them (approve a token, wait
53
+ * for the approval to mine, sign the terms, send the transaction). The wallet normally closes the pop-up a
54
+ * short time after the last answered request, and a request sent after that would open a new pop-up outside
55
+ * a click, which the browser blocks. `holdPopup()` asks the wallet to keep the current pop-up open between
56
+ * those requests. Call `release()` in a `finally` block.
57
+ *
58
+ * The hold is a request to the wallet, not a guarantee: an older wallet ignores the message, and the wallet
59
+ * caps a hold at `HOLD_MAX_MS` (5 minutes) since the last hold or request. Keep a fallback for a closed
60
+ * pop-up. Synchronous; it never throws.
61
+ */
62
+ holdPopup(): PopupHold;
46
63
  }
47
64
  /** The handle {@link AsseteraWalletProvider.prepareSigning} returns. */
48
65
  interface PreparedSigning {
49
66
  /** `false` when the browser blocked the pop-up. The caller should then ask for another click. */
50
67
  readonly opened: boolean;
51
68
  /**
52
- * Give the pop-up back when no request followed (the quote failed, the client cancelled). It closes the
53
- * pop-up only when THIS call opened it and no request was sent since; otherwise it does nothing. Safe to call
54
- * more than once.
69
+ * Give the pop-up back when no request followed (the quote failed, the client cancelled). It ends the hold
70
+ * this handle started and posts `hold: false` when no other hold is active. It closes the pop-up only when
71
+ * THIS call opened it and no request was sent since; otherwise it does nothing. Safe to call more than once.
72
+ */
73
+ release(): void;
74
+ }
75
+ /** The handle {@link AsseteraWalletProvider.holdPopup} returns. */
76
+ interface PopupHold {
77
+ /** `false` when the browser blocked the pop-up. The caller should then ask for another click. */
78
+ readonly opened: boolean;
79
+ /**
80
+ * End the hold. When no other hold is active the provider posts `hold: false` to the current pop-up. It
81
+ * closes the pop-up only when this call opened it and no request was sent since; otherwise it does nothing.
82
+ * Safe to call more than once.
55
83
  */
56
84
  release(): void;
57
85
  }
@@ -83,4 +111,4 @@ declare class ProviderRpcError extends Error {
83
111
  */
84
112
  declare function createAsseteraWalletProvider(options: AsseteraWalletProviderOptions): AsseteraWalletProvider;
85
113
 
86
- export { type AsseteraWalletProvider as A, type PreparedSigning as P, type RequestArguments as R, type AsseteraWalletProviderOptions as a, type ProviderEvent as b, type ProviderListener as c, ProviderRpcError as d, createAsseteraWalletProvider as e };
114
+ export { type AsseteraWalletProvider as A, type PopupHold as P, type RequestArguments as R, type AsseteraWalletProviderOptions as a, type PreparedSigning as b, type ProviderEvent as c, type ProviderListener as d, ProviderRpcError as e, createAsseteraWalletProvider as f };
@@ -140,6 +140,9 @@ function createAsseteraWalletProvider(options) {
140
140
  const listeners = /* @__PURE__ */ new Map();
141
141
  let closeTimer = null;
142
142
  let requestsSent = 0;
143
+ let popupReady = false;
144
+ const holds = /* @__PURE__ */ new Set();
145
+ const preparedHolds = /* @__PURE__ */ new Map();
143
146
  function currentChainId() {
144
147
  return chainIdHex ?? toHexChainId(defaultChainId);
145
148
  }
@@ -174,6 +177,34 @@ function createAsseteraWalletProvider(options) {
174
177
  function removeEventListener(event, listener) {
175
178
  listeners.get(event)?.delete(listener);
176
179
  }
180
+ function postHold(hold) {
181
+ const active = popup;
182
+ if (!active || active.closed || !popupReady) return;
183
+ const message = {
184
+ type: `${MESSAGE_PREFIX}hold`,
185
+ v: PROTOCOL_VERSION,
186
+ hold
187
+ };
188
+ active.postMessage(message, walletOrigin);
189
+ }
190
+ function addHold(token) {
191
+ const wasEmpty = holds.size === 0;
192
+ holds.add(token);
193
+ if (wasEmpty) postHold(true);
194
+ }
195
+ function removeHold(token) {
196
+ if (!holds.delete(token)) return false;
197
+ return holds.size === 0;
198
+ }
199
+ function endPreparedHolds(sequence) {
200
+ let emptied = false;
201
+ for (const [token, registeredAt] of preparedHolds) {
202
+ if (registeredAt >= sequence) continue;
203
+ preparedHolds.delete(token);
204
+ if (removeHold(token)) emptied = true;
205
+ }
206
+ if (emptied) postHold(false);
207
+ }
177
208
  function stopClosePolling() {
178
209
  if (closeTimer !== null) {
179
210
  clearInterval(closeTimer);
@@ -187,7 +218,10 @@ function createAsseteraWalletProvider(options) {
187
218
  }, CLOSE_POLL_MS);
188
219
  }
189
220
  function rejectAllPending(error) {
190
- for (const request2 of pending.values()) request2.reject(error);
221
+ for (const request2 of pending.values()) {
222
+ request2.reject(error);
223
+ endPreparedHolds(request2.sequence);
224
+ }
191
225
  pending.clear();
192
226
  }
193
227
  function handlePopupClosed() {
@@ -195,6 +229,7 @@ function createAsseteraWalletProvider(options) {
195
229
  stopClosePolling();
196
230
  const closedPopup = popup;
197
231
  popup = null;
232
+ popupReady = false;
198
233
  const closedReady = ready;
199
234
  ready = createDeferred();
200
235
  const error = disconnectedError();
@@ -217,12 +252,13 @@ function createAsseteraWalletProvider(options) {
217
252
  );
218
253
  if (!opened) return null;
219
254
  popup = opened;
255
+ popupReady = false;
220
256
  ready = createDeferred();
221
257
  walletDisconnected = false;
222
258
  startClosePolling();
223
259
  return opened;
224
260
  }
225
- function sendRequest(method, params) {
261
+ function sendRequest(method, params, sequence) {
226
262
  const active = popup;
227
263
  if (!active || active.closed) return Promise.reject(disconnectedError());
228
264
  const id = crypto.randomUUID();
@@ -234,7 +270,7 @@ function createAsseteraWalletProvider(options) {
234
270
  ...params === void 0 ? {} : { params }
235
271
  };
236
272
  return new Promise((resolve, reject) => {
237
- pending.set(id, { resolve, reject });
273
+ pending.set(id, { resolve, reject, sequence });
238
274
  active.postMessage(message, walletOrigin);
239
275
  });
240
276
  }
@@ -242,6 +278,7 @@ function createAsseteraWalletProvider(options) {
242
278
  const request2 = pending.get(message.id);
243
279
  if (!request2) return;
244
280
  pending.delete(message.id);
281
+ endPreparedHolds(request2.sequence);
245
282
  if ("error" in message) {
246
283
  request2.reject(new ProviderRpcError(message.error.code, message.error.message, message.error.data));
247
284
  } else {
@@ -282,6 +319,8 @@ function createAsseteraWalletProvider(options) {
282
319
  if (popup === null || event.source !== popup) return;
283
320
  const data = event.data;
284
321
  if (isReadyMessage(data)) {
322
+ popupReady = true;
323
+ if (holds.size > 0) postHold(true);
285
324
  ready.resolve(void 0);
286
325
  return;
287
326
  }
@@ -321,9 +360,10 @@ function createAsseteraWalletProvider(options) {
321
360
  if (method === "eth_chainId") return Promise.resolve(currentChainId());
322
361
  if (method === "eth_requestAccounts") {
323
362
  requestsSent += 1;
363
+ const sequence2 = requestsSent;
324
364
  const active2 = openPopup();
325
365
  if (!active2) return Promise.reject(blockedError());
326
- return ready.promise.then(() => sendRequest("eth_requestAccounts", params)).then((result) => {
366
+ return ready.promise.then(() => sendRequest("eth_requestAccounts", params, sequence2)).then((result) => {
327
367
  accounts = Array.isArray(result) ? result : [];
328
368
  emit("accountsChanged", [...accounts]);
329
369
  emit("connect", { chainId: currentChainId() });
@@ -339,10 +379,11 @@ function createAsseteraWalletProvider(options) {
339
379
  );
340
380
  }
341
381
  requestsSent += 1;
382
+ const sequence = requestsSent;
342
383
  const active = openPopup();
343
384
  if (!active) return Promise.reject(blockedError());
344
385
  if (SIGNING_METHODS.has(method)) active.focus();
345
- return ready.promise.then(() => sendRequest(method, params));
386
+ return ready.promise.then(() => sendRequest(method, params, sequence));
346
387
  }
347
388
  function disconnect() {
348
389
  const active = popup;
@@ -356,6 +397,9 @@ function createAsseteraWalletProvider(options) {
356
397
  }
357
398
  stopClosePolling();
358
399
  popup = null;
400
+ popupReady = false;
401
+ holds.clear();
402
+ preparedHolds.clear();
359
403
  const closedReady = ready;
360
404
  ready = createDeferred();
361
405
  closedReady.reject(disconnectedError("Disconnected from the Assetera Wallet."));
@@ -371,16 +415,49 @@ function createAsseteraWalletProvider(options) {
371
415
  if (!active) return { opened: false, release: () => {
372
416
  } };
373
417
  const sentAtPrepare = requestsSent;
418
+ const token = /* @__PURE__ */ Symbol("assetera-wallet-prepare");
419
+ preparedHolds.set(token, sentAtPrepare);
420
+ addHold(token);
421
+ let released = false;
422
+ return {
423
+ opened: true,
424
+ release() {
425
+ if (released) return;
426
+ released = true;
427
+ preparedHolds.delete(token);
428
+ const emptied = removeHold(token);
429
+ const canClose = emptied && !existed && popup === active && !active.closed && requestsSent === sentAtPrepare && pending.size === 0;
430
+ if (canClose) {
431
+ active.close();
432
+ handlePopupClosed();
433
+ return;
434
+ }
435
+ if (emptied) postHold(false);
436
+ }
437
+ };
438
+ }
439
+ function holdPopup() {
440
+ const existed = popup !== null && !popup.closed;
441
+ const active = typeof window === "undefined" ? null : openPopup();
442
+ if (!active) return { opened: false, release: () => {
443
+ } };
444
+ const token = /* @__PURE__ */ Symbol("assetera-wallet-hold");
445
+ const sentAtHold = requestsSent;
446
+ addHold(token);
374
447
  let released = false;
375
448
  return {
376
449
  opened: true,
377
450
  release() {
378
451
  if (released) return;
379
452
  released = true;
380
- if (existed || popup !== active || active.closed) return;
381
- if (requestsSent !== sentAtPrepare || pending.size > 0) return;
382
- active.close();
383
- handlePopupClosed();
453
+ const emptied = removeHold(token);
454
+ const canClose = emptied && !existed && popup === active && !active.closed && requestsSent === sentAtHold && pending.size === 0;
455
+ if (canClose) {
456
+ active.close();
457
+ handlePopupClosed();
458
+ return;
459
+ }
460
+ if (emptied) postHold(false);
384
461
  }
385
462
  };
386
463
  }
@@ -403,7 +480,8 @@ function createAsseteraWalletProvider(options) {
403
480
  disconnect,
404
481
  lastSecurityStatus,
405
482
  lastAccountNames,
406
- prepareSigning
483
+ prepareSigning,
484
+ holdPopup
407
485
  };
408
486
  }
409
487