Files
hanzo.ai/lib/auth/wallet.ts
zeekay a46eaec8fb sign-in: the wallet never holds the page, counts only once it signs
A wallet whose prompt never answers no longer locks the sign-in: the
other ways in stay open while it waits, any other press supersedes it,
and after a minute it is let go and said so. signup_submitted is sent
once the wallet has signed, not on the press. A second factor's header
names the way in that holds it (the wallet, the number), never an
address typed beside it. The gate's challenge names hanzo.ai, as IAM
v1.34.171 does for a page at one of the client's registered origins.

/login still sends no verify method: that needs @hanzo/iam 0.21.38
(loginWithWallet's `method`, hanzo-js/iam branch wallet-method).
2026-10-27 01:23:17 -07:00

76 lines
2.8 KiB
TypeScript

/**
* A wallet, as the sign-in panel and Settings see it. The sign-in itself is
* @hanzo/iam's `loginWithWallet`: IAM's challenge, the EIP-4361 text rendered
* from it, `personal_sign` by the browser's own wallet, and IAM's verify, which
* answers as its login does. This file answers the three questions the page asks
* around that call, and is pure, so each is tested without a browser.
*/
/** The EIP-1193 request surface `loginWithWallet` signs through. */
type Provider = { request?: unknown }
/**
* Whether this browser holds an Ethereum wallet the sign-in can ask: the injected
* EIP-1193 provider (`window.ethereum`) `loginWithWallet` signs with when it is
* handed none. No wallet, no button: one that could not finish is worse than none.
*/
export function injected(w: { ethereum?: Provider } | undefined = typeof window === 'undefined' ? undefined : (window as { ethereum?: Provider })): boolean {
return typeof w?.ethereum?.request === 'function'
}
/**
* Whether the wallet declined to sign: the person said no in its prompt, which
* EIP-1193 states as error 4001. Said in the page's words, not the wallet's.
*/
export function declined(err: unknown): boolean {
return typeof err === 'object' && err !== null && (err as { code?: unknown }).code === 4001
}
/**
* How long the page waits on the wallet, in ms: its prompt open, the person
* unlocking it and reading the sign-in. A wallet that never answers (a prompt
* that never opened, an extension that hung) is let go after this.
*/
export const WAIT = 60_000
/** The wallet did not answer within `WAIT`. */
export class Silent extends Error {
constructor() {
super('the wallet did not answer')
this.name = 'Silent'
}
}
/** `p`, or a {@link Silent} once `ms` pass without it settling. */
export function within<T>(p: Promise<T>, ms: number = WAIT): Promise<T> {
return new Promise<T>((done, fail) => {
const late = setTimeout(() => fail(new Silent()), ms)
p.then(
(v) => {
clearTimeout(late)
done(v)
},
(err: unknown) => {
clearTimeout(late)
fail(err)
},
)
})
}
/** One wallet that signs an account in, as IAM names it: its chain family and its address. */
export type Wallet = { chain: string; address: string }
/**
* The wallets an access token says sign its account in: IAM's `wallets` claim, in
* IAM's order. Anything that is not a chain and an address is not one.
*/
export function wallets(claims: Record<string, unknown> | null | undefined): Wallet[] {
const set = claims?.wallets
if (!Array.isArray(set)) return []
return set.flatMap((w) => {
const r = (w ?? {}) as Record<string, unknown>
return typeof r.chain === 'string' && r.chain && typeof r.address === 'string' && r.address ? [{ chain: r.chain, address: r.address }] : []
})
}