@x1id/resolve 0.22.0 → 0.43.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +71 -0
- package/dist/accounts.d.ts +6 -0
- package/dist/accounts.js +6 -1
- package/dist/agent.d.ts +184 -8
- package/dist/agent.js +351 -3
- package/dist/agentPolicy.d.ts +238 -0
- package/dist/agentPolicy.js +364 -0
- package/dist/attestation.d.ts +25 -0
- package/dist/attestation.js +25 -0
- package/dist/checkout.d.ts +216 -0
- package/dist/checkout.js +327 -0
- package/dist/credentials.d.ts +466 -0
- package/dist/credentials.js +669 -0
- package/dist/gate.d.ts +94 -4
- package/dist/gate.js +206 -7
- package/dist/gateClient.d.ts +12 -0
- package/dist/giftLink.d.ts +272 -0
- package/dist/giftLink.js +452 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +33 -0
- package/dist/loginButton.d.ts +69 -0
- package/dist/loginButton.js +207 -0
- package/dist/logo.d.ts +1 -0
- package/dist/logo.js +8 -0
- package/dist/messaging.d.ts +439 -0
- package/dist/messaging.js +979 -0
- package/dist/mintNft.d.ts +247 -0
- package/dist/mintNft.js +328 -0
- package/dist/nsGate.d.ts +98 -0
- package/dist/nsGate.js +172 -0
- package/dist/oidc.d.ts +272 -0
- package/dist/oidc.js +641 -0
- package/dist/pay.d.ts +513 -0
- package/dist/pay.js +756 -0
- package/dist/payment.d.ts +9 -2
- package/dist/payment.js +9 -2
- package/dist/recovery.d.ts +186 -0
- package/dist/recovery.js +276 -0
- package/dist/recoveryNft.d.ts +183 -0
- package/dist/recoveryNft.js +291 -0
- package/dist/shield.d.ts +373 -0
- package/dist/shield.js +695 -0
- package/dist/subscription.d.ts +439 -0
- package/dist/subscription.js +642 -0
- package/dist/textRecords.d.ts +52 -6
- package/dist/textRecords.js +80 -8
- package/dist/types.d.ts +3 -0
- package/dist/x402Receipt.d.ts +246 -0
- package/dist/x402Receipt.js +374 -0
- package/package.json +18 -1
- package/schema/agent-manifest.json +91 -8
package/dist/payment.d.ts
CHANGED
|
@@ -5,8 +5,15 @@
|
|
|
5
5
|
* A handle's per-chain address `Record` can be written by a record-editing
|
|
6
6
|
* DELEGATE, not just the owner — so an UNVERIFIED record must never receive
|
|
7
7
|
* money: a malicious delegate could silently redirect payments. On-chain
|
|
8
|
-
* VERIFICATION
|
|
9
|
-
*
|
|
8
|
+
* VERIFICATION is the guard: `verify_record_svm`/`_eth`/`_btc` need the
|
|
9
|
+
* recorded address's own proof (co-signature / signed challenge) AND the
|
|
10
|
+
* handle's CURRENT AUTHORITY (owner, or pNFT holder) as the signer. A
|
|
11
|
+
* delegate cannot verify (`DelegateCannotVerify`, 6137, Sentinel
|
|
12
|
+
* x1p-007-04), and every delegate write leaves the record unverified, so a
|
|
13
|
+
* delegate alone can never produce a record this rule pays. (Before that
|
|
14
|
+
* program upgrade a delegate COULD verify a key it controlled; a record a
|
|
15
|
+
* delegate verified before the upgrade stays verified until the owner
|
|
16
|
+
* updates, clears or re-verifies it.) The rule every pay surface shares is:
|
|
10
17
|
*
|
|
11
18
|
* 1. a LIVE, VERIFIED record for the payment chain → that record's address
|
|
12
19
|
* (`basis: "verified-record"`). Staleness is judged first: a record from
|
package/dist/payment.js
CHANGED
|
@@ -5,8 +5,15 @@
|
|
|
5
5
|
* A handle's per-chain address `Record` can be written by a record-editing
|
|
6
6
|
* DELEGATE, not just the owner — so an UNVERIFIED record must never receive
|
|
7
7
|
* money: a malicious delegate could silently redirect payments. On-chain
|
|
8
|
-
* VERIFICATION
|
|
9
|
-
*
|
|
8
|
+
* VERIFICATION is the guard: `verify_record_svm`/`_eth`/`_btc` need the
|
|
9
|
+
* recorded address's own proof (co-signature / signed challenge) AND the
|
|
10
|
+
* handle's CURRENT AUTHORITY (owner, or pNFT holder) as the signer. A
|
|
11
|
+
* delegate cannot verify (`DelegateCannotVerify`, 6137, Sentinel
|
|
12
|
+
* x1p-007-04), and every delegate write leaves the record unverified, so a
|
|
13
|
+
* delegate alone can never produce a record this rule pays. (Before that
|
|
14
|
+
* program upgrade a delegate COULD verify a key it controlled; a record a
|
|
15
|
+
* delegate verified before the upgrade stays verified until the owner
|
|
16
|
+
* updates, clears or re-verifies it.) The rule every pay surface shares is:
|
|
10
17
|
*
|
|
11
18
|
* 1. a LIVE, VERIFIED record for the payment chain → that record's address
|
|
12
19
|
* (`basis: "verified-record"`). Staleness is judged first: a record from
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Social recovery (Recovery 2, WP 8868): instruction builders for the four
|
|
3
|
+
* recovery instructions and a state reader with time remaining.
|
|
4
|
+
*
|
|
5
|
+
* set_recovery(Option<Pubkey>) owner — set / clear the recovery key;
|
|
6
|
+
* also CLEARS any pending recovery
|
|
7
|
+
* initiate_recovery(target) recovery — start the timelock toward `target`
|
|
8
|
+
* cancel_recovery owner — stop a pending recovery (allowed
|
|
9
|
+
* even while the name is locked)
|
|
10
|
+
* complete_recovery recovery — once the timelock elapsed, owner
|
|
11
|
+
* becomes the target
|
|
12
|
+
*
|
|
13
|
+
* The lock family (`lock_handle`, `initiate_unlock`, `cancel_unlock`,
|
|
14
|
+
* `complete_unlock`) already lives in `lock.ts`; {@link fetchRecoveryState}
|
|
15
|
+
* reports both, because a lock also freezes `set_recovery` /
|
|
16
|
+
* `initiate_recovery` / `complete_recovery`.
|
|
17
|
+
*
|
|
18
|
+
* Hand-rolled like the rest of this package — no Anchor client, no
|
|
19
|
+
* `@solana/web3.js` import. Builders return the transport-neutral
|
|
20
|
+
* {@link BuiltInstruction} (`delegate.ts`); PDAs come from an injected
|
|
21
|
+
* {@link PdaFinder} (`namespacePricing.ts`).
|
|
22
|
+
*
|
|
23
|
+
* # Wire rules mirrored from programs/x1-handles/src/lib.rs
|
|
24
|
+
*
|
|
25
|
+
* - `OwnerOnly { owner: Signer, handle: mut (has_one = owner) }` backs
|
|
26
|
+
* `set_recovery` / `cancel_recovery`; `Recover { recovery: Signer,
|
|
27
|
+
* handle: mut, config, auction }` backs `initiate_recovery` /
|
|
28
|
+
* `complete_recovery` (the `auction` PDA `["auction", handle]` is checked
|
|
29
|
+
* for a live bid; it need not exist).
|
|
30
|
+
* - The WHOLE family is refused on a TOKENIZED handle (`HandleIsTokenized`),
|
|
31
|
+
* and scoped `name.tld` handles are always tokenized — so no remaining
|
|
32
|
+
* accounts are ever needed (the bare PDA check never reads them).
|
|
33
|
+
* - `complete_recovery` requires `now - recovery_initiated_at >=
|
|
34
|
+
* max(Config.recovery_timelock_secs, MIN_RECOVERY_TIMELOCK_SECS)`; a lock
|
|
35
|
+
* (`HandleLocked`) or a bid-carrying auction (`AuctionHasBids`) also refuse
|
|
36
|
+
* it, and `initiate_recovery`.
|
|
37
|
+
* - Completion KEEPS the recovery key (audit L-1); every other ownership
|
|
38
|
+
* change clears it.
|
|
39
|
+
*/
|
|
40
|
+
import { type RpcFn } from "./accounts.js";
|
|
41
|
+
import type { AddressLike, BuiltInstruction } from "./delegate.js";
|
|
42
|
+
import type { PdaFinder } from "./namespacePricing.js";
|
|
43
|
+
/** Anchor discriminators, `sha256("global:<name>")[0..8]`, pinned and
|
|
44
|
+
* re-derived in the test suite. */
|
|
45
|
+
export declare const SET_RECOVERY_DISCRIMINATOR: Uint8Array;
|
|
46
|
+
export declare const INITIATE_RECOVERY_DISCRIMINATOR: Uint8Array;
|
|
47
|
+
export declare const CANCEL_RECOVERY_DISCRIMINATOR: Uint8Array;
|
|
48
|
+
export declare const COMPLETE_RECOVERY_DISCRIMINATOR: Uint8Array;
|
|
49
|
+
/** `sha256("account:Handle")[0..8]` / `sha256("account:Config")[0..8]`. */
|
|
50
|
+
export declare const HANDLE_ACCOUNT_DISCRIMINATOR: Uint8Array;
|
|
51
|
+
export declare const CONFIG_ACCOUNT_DISCRIMINATOR: Uint8Array;
|
|
52
|
+
/** lib.rs `RECOVERY_TIMELOCK_SECS` — the default `Config.recovery_timelock_secs`. */
|
|
53
|
+
export declare const DEFAULT_RECOVERY_TIMELOCK_SECS: number;
|
|
54
|
+
/** lib.rs `MIN_RECOVERY_TIMELOCK_SECS` — `complete_recovery`'s hard floor. */
|
|
55
|
+
export declare const MIN_RECOVERY_TIMELOCK_SECS: number;
|
|
56
|
+
/** `Config.recovery_timelock_secs` (i64 LE) offset: disc(8) admin(32)
|
|
57
|
+
* treasury(32) launch_ts(8) tiers(80) nft_tiers(80) total_handles_sold(8)
|
|
58
|
+
* nfts_minted(8) = 256. Present on a V2+ Config (>= 265 bytes). */
|
|
59
|
+
export declare const CONFIG_RECOVERY_TIMELOCK_OFFSET = 256;
|
|
60
|
+
/** `["config"]` and `["auction", handle]` — the two extra accounts `Recover`
|
|
61
|
+
* needs. */
|
|
62
|
+
export declare function deriveRecoveryAccounts(find: PdaFinder, programId: AddressLike, handle: AddressLike): Promise<{
|
|
63
|
+
readonly config: string;
|
|
64
|
+
readonly auction: string;
|
|
65
|
+
}>;
|
|
66
|
+
export interface SetRecoveryParams {
|
|
67
|
+
readonly programId: AddressLike;
|
|
68
|
+
/** The handle's owner. Signer. */
|
|
69
|
+
readonly owner: AddressLike;
|
|
70
|
+
/** The `["handle", name]` PDA. */
|
|
71
|
+
readonly handle: AddressLike;
|
|
72
|
+
/** The new recovery key, or `null` to clear it. Setting (or clearing) the
|
|
73
|
+
* key also cancels any pending recovery. */
|
|
74
|
+
readonly recovery: AddressLike | null;
|
|
75
|
+
}
|
|
76
|
+
/** `set_recovery(Option<Pubkey>)` — data: disc ‖ 0 | (1 ‖ pubkey). */
|
|
77
|
+
export declare function buildSetRecoveryIx(p: SetRecoveryParams): BuiltInstruction;
|
|
78
|
+
export interface CancelRecoveryParams {
|
|
79
|
+
readonly programId: AddressLike;
|
|
80
|
+
/** The handle's owner. Signer. */
|
|
81
|
+
readonly owner: AddressLike;
|
|
82
|
+
readonly handle: AddressLike;
|
|
83
|
+
}
|
|
84
|
+
/** `cancel_recovery` — the owner stops a pending recovery. Data: disc only. */
|
|
85
|
+
export declare function buildCancelRecoveryIx(p: CancelRecoveryParams): BuiltInstruction;
|
|
86
|
+
export interface RecoverParams {
|
|
87
|
+
readonly programId: AddressLike;
|
|
88
|
+
/** The configured recovery key (`Handle.recovery`). Signer. */
|
|
89
|
+
readonly recovery: AddressLike;
|
|
90
|
+
readonly handle: AddressLike;
|
|
91
|
+
/** The `["config"]` PDA ({@link deriveRecoveryAccounts}). */
|
|
92
|
+
readonly config: AddressLike;
|
|
93
|
+
/** The `["auction", handle]` PDA — passed even when no auction exists. */
|
|
94
|
+
readonly auction: AddressLike;
|
|
95
|
+
}
|
|
96
|
+
export interface InitiateRecoveryParams extends RecoverParams {
|
|
97
|
+
/** Who will own the handle once the recovery completes. */
|
|
98
|
+
readonly target: AddressLike;
|
|
99
|
+
}
|
|
100
|
+
/** `initiate_recovery(target)` — data: disc ‖ target(32). Starts the timelock;
|
|
101
|
+
* the owner can cancel until {@link RecoveryState.pending}`.completesAt`. */
|
|
102
|
+
export declare function buildInitiateRecoveryIx(p: InitiateRecoveryParams): BuiltInstruction;
|
|
103
|
+
/** `complete_recovery` — data: disc only. Valid once the timelock elapsed
|
|
104
|
+
* ({@link RecoveryState.pending}`.completable`). */
|
|
105
|
+
export declare function buildCompleteRecoveryIx(p: RecoverParams): BuiltInstruction;
|
|
106
|
+
/** The recovery fields of a `Handle` account. */
|
|
107
|
+
export interface HandleRecoveryFields {
|
|
108
|
+
readonly owner: string;
|
|
109
|
+
/** The configured recovery key, or null. */
|
|
110
|
+
readonly recovery: string | null;
|
|
111
|
+
/** Unix seconds; 0n = no recovery pending. */
|
|
112
|
+
readonly recoveryInitiatedAt: bigint;
|
|
113
|
+
readonly recoveryTarget: string | null;
|
|
114
|
+
readonly registeredAt: bigint;
|
|
115
|
+
readonly tokenized: boolean;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Decode the recovery fields of a `Handle` account's raw data (sequential:
|
|
119
|
+
* the two `Option<Pubkey>` fields shift what follows them). Null when the
|
|
120
|
+
* data is not a Handle (wrong discriminator / too short).
|
|
121
|
+
*
|
|
122
|
+
* disc(8) name(32) name_len(1) owner(32)@41 handle_type(1)
|
|
123
|
+
* recovery: Option<Pubkey> recovery_initiated_at: i64
|
|
124
|
+
* recovery_target: Option<Pubkey> registered_at: i64 bump(1)
|
|
125
|
+
* [157: nft tag(1) mint(32)]
|
|
126
|
+
*/
|
|
127
|
+
export declare function decodeHandleRecovery(data: Uint8Array): HandleRecoveryFields | null;
|
|
128
|
+
/** `Config.recovery_timelock_secs`, or null for a pre-V2 Config / not a Config. */
|
|
129
|
+
export declare function readConfigRecoveryTimelock(data: Uint8Array): bigint | null;
|
|
130
|
+
/** The timelock `complete_recovery` enforces: `max(config ?? default, floor)`. */
|
|
131
|
+
export declare function effectiveRecoveryTimelock(configSecs: bigint | null): bigint;
|
|
132
|
+
/** When a pending recovery can complete (null = none pending). Same clamp as
|
|
133
|
+
* the program, so a UI never shows an ETA earlier than it will allow. */
|
|
134
|
+
export declare function recoveryCompletesAt(recoveryInitiatedAt: bigint, configSecs: bigint | null): bigint | null;
|
|
135
|
+
export interface RecoveryState {
|
|
136
|
+
readonly handle: string;
|
|
137
|
+
readonly owner: string;
|
|
138
|
+
/** Recovery is refused on chain for a tokenized handle. */
|
|
139
|
+
readonly tokenized: boolean;
|
|
140
|
+
readonly recoveryKey: string | null;
|
|
141
|
+
/** `!tokenized && !locked && recoveryKey !== null`: initiate is possible. */
|
|
142
|
+
readonly recoveryAvailable: boolean;
|
|
143
|
+
readonly pending: {
|
|
144
|
+
readonly target: string | null;
|
|
145
|
+
readonly initiatedAt: bigint;
|
|
146
|
+
readonly completesAt: bigint;
|
|
147
|
+
/** Seconds until `complete_recovery` can land (0n once it can). */
|
|
148
|
+
readonly secondsRemaining: bigint;
|
|
149
|
+
/** Timelock elapsed AND not locked. A live bid-carrying auction would
|
|
150
|
+
* still refuse it (`AuctionHasBids`). */
|
|
151
|
+
readonly completable: boolean;
|
|
152
|
+
} | null;
|
|
153
|
+
readonly locked: boolean;
|
|
154
|
+
readonly pendingUnlock: {
|
|
155
|
+
readonly initiatedAt: bigint;
|
|
156
|
+
readonly unlocksAt: bigint;
|
|
157
|
+
readonly secondsRemaining: bigint;
|
|
158
|
+
readonly completable: boolean;
|
|
159
|
+
} | null;
|
|
160
|
+
/** The effective timelock (seconds) in force now. */
|
|
161
|
+
readonly timelockSecs: bigint;
|
|
162
|
+
readonly nowSecs: bigint;
|
|
163
|
+
}
|
|
164
|
+
/** Pure: the state from raw Handle data + the Config timelock + now. */
|
|
165
|
+
export declare function computeRecoveryState(handle: AddressLike, handleData: Uint8Array, configTimelockSecs: bigint | null, nowSecs: bigint): RecoveryState | null;
|
|
166
|
+
export interface FetchRecoveryStateOptions {
|
|
167
|
+
readonly programId: AddressLike;
|
|
168
|
+
/** The `["handle", name]` PDA (derive it with the resolver's WASM
|
|
169
|
+
* `deriveHandleAccount`, or `makeScopedHandleDeriver` for name.tld). */
|
|
170
|
+
readonly handle: AddressLike;
|
|
171
|
+
/** The `["config"]` PDA; or pass `find` to derive it. */
|
|
172
|
+
readonly config?: AddressLike;
|
|
173
|
+
readonly find?: PdaFinder;
|
|
174
|
+
/** Unix seconds; defaults to the local clock. Pass the cluster time
|
|
175
|
+
* (`getBlockTime`) when it matters to the second. */
|
|
176
|
+
readonly nowSecs?: bigint;
|
|
177
|
+
readonly commitment?: string;
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Read a handle's recovery + lock state from chain (the authority — the
|
|
181
|
+
* indexer's `GET /recovery` is a convenience view): the Handle account and
|
|
182
|
+
* the Config timelock, in two `getAccountInfo` calls. Null when the handle
|
|
183
|
+
* does not exist. Throws when the account exists but is not a registry
|
|
184
|
+
* Handle.
|
|
185
|
+
*/
|
|
186
|
+
export declare function fetchRecoveryState(rpc: RpcFn, opts: FetchRecoveryStateOptions): Promise<RecoveryState | null>;
|
package/dist/recovery.js
ADDED
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Social recovery (Recovery 2, WP 8868): instruction builders for the four
|
|
3
|
+
* recovery instructions and a state reader with time remaining.
|
|
4
|
+
*
|
|
5
|
+
* set_recovery(Option<Pubkey>) owner — set / clear the recovery key;
|
|
6
|
+
* also CLEARS any pending recovery
|
|
7
|
+
* initiate_recovery(target) recovery — start the timelock toward `target`
|
|
8
|
+
* cancel_recovery owner — stop a pending recovery (allowed
|
|
9
|
+
* even while the name is locked)
|
|
10
|
+
* complete_recovery recovery — once the timelock elapsed, owner
|
|
11
|
+
* becomes the target
|
|
12
|
+
*
|
|
13
|
+
* The lock family (`lock_handle`, `initiate_unlock`, `cancel_unlock`,
|
|
14
|
+
* `complete_unlock`) already lives in `lock.ts`; {@link fetchRecoveryState}
|
|
15
|
+
* reports both, because a lock also freezes `set_recovery` /
|
|
16
|
+
* `initiate_recovery` / `complete_recovery`.
|
|
17
|
+
*
|
|
18
|
+
* Hand-rolled like the rest of this package — no Anchor client, no
|
|
19
|
+
* `@solana/web3.js` import. Builders return the transport-neutral
|
|
20
|
+
* {@link BuiltInstruction} (`delegate.ts`); PDAs come from an injected
|
|
21
|
+
* {@link PdaFinder} (`namespacePricing.ts`).
|
|
22
|
+
*
|
|
23
|
+
* # Wire rules mirrored from programs/x1-handles/src/lib.rs
|
|
24
|
+
*
|
|
25
|
+
* - `OwnerOnly { owner: Signer, handle: mut (has_one = owner) }` backs
|
|
26
|
+
* `set_recovery` / `cancel_recovery`; `Recover { recovery: Signer,
|
|
27
|
+
* handle: mut, config, auction }` backs `initiate_recovery` /
|
|
28
|
+
* `complete_recovery` (the `auction` PDA `["auction", handle]` is checked
|
|
29
|
+
* for a live bid; it need not exist).
|
|
30
|
+
* - The WHOLE family is refused on a TOKENIZED handle (`HandleIsTokenized`),
|
|
31
|
+
* and scoped `name.tld` handles are always tokenized — so no remaining
|
|
32
|
+
* accounts are ever needed (the bare PDA check never reads them).
|
|
33
|
+
* - `complete_recovery` requires `now - recovery_initiated_at >=
|
|
34
|
+
* max(Config.recovery_timelock_secs, MIN_RECOVERY_TIMELOCK_SECS)`; a lock
|
|
35
|
+
* (`HandleLocked`) or a bid-carrying auction (`AuctionHasBids`) also refuse
|
|
36
|
+
* it, and `initiate_recovery`.
|
|
37
|
+
* - Completion KEEPS the recovery key (audit L-1); every other ownership
|
|
38
|
+
* change clears it.
|
|
39
|
+
*/
|
|
40
|
+
import { encodeBase58, decodeBase58_32 } from "./base58.js";
|
|
41
|
+
import { bytesEqual, parseHandleAccount } from "./accounts.js";
|
|
42
|
+
import { readLockState, unlockAvailableAt } from "./lock.js";
|
|
43
|
+
/** Anchor discriminators, `sha256("global:<name>")[0..8]`, pinned and
|
|
44
|
+
* re-derived in the test suite. */
|
|
45
|
+
export const SET_RECOVERY_DISCRIMINATOR = Uint8Array.from([70, 35, 195, 148, 6, 140, 128, 124]);
|
|
46
|
+
export const INITIATE_RECOVERY_DISCRIMINATOR = Uint8Array.from([132, 148, 60, 74, 49, 178, 235, 187]);
|
|
47
|
+
export const CANCEL_RECOVERY_DISCRIMINATOR = Uint8Array.from([176, 23, 203, 37, 121, 251, 227, 83]);
|
|
48
|
+
export const COMPLETE_RECOVERY_DISCRIMINATOR = Uint8Array.from([195, 236, 5, 5, 131, 32, 196, 110]);
|
|
49
|
+
/** `sha256("account:Handle")[0..8]` / `sha256("account:Config")[0..8]`. */
|
|
50
|
+
export const HANDLE_ACCOUNT_DISCRIMINATOR = Uint8Array.from([150, 96, 143, 54, 64, 147, 63, 63]);
|
|
51
|
+
export const CONFIG_ACCOUNT_DISCRIMINATOR = Uint8Array.from([155, 12, 170, 224, 30, 250, 204, 130]);
|
|
52
|
+
/** lib.rs `RECOVERY_TIMELOCK_SECS` — the default `Config.recovery_timelock_secs`. */
|
|
53
|
+
export const DEFAULT_RECOVERY_TIMELOCK_SECS = 7 * 24 * 60 * 60;
|
|
54
|
+
/** lib.rs `MIN_RECOVERY_TIMELOCK_SECS` — `complete_recovery`'s hard floor. */
|
|
55
|
+
export const MIN_RECOVERY_TIMELOCK_SECS = 24 * 60 * 60;
|
|
56
|
+
/** `Config.recovery_timelock_secs` (i64 LE) offset: disc(8) admin(32)
|
|
57
|
+
* treasury(32) launch_ts(8) tiers(80) nft_tiers(80) total_handles_sold(8)
|
|
58
|
+
* nfts_minted(8) = 256. Present on a V2+ Config (>= 265 bytes). */
|
|
59
|
+
export const CONFIG_RECOVERY_TIMELOCK_OFFSET = 256;
|
|
60
|
+
function toBase58(v, what) {
|
|
61
|
+
if (typeof v === "string") {
|
|
62
|
+
const b = decodeBase58_32(v);
|
|
63
|
+
if (!b)
|
|
64
|
+
throw new Error(`${what} is not a valid base58 address`);
|
|
65
|
+
return encodeBase58(b);
|
|
66
|
+
}
|
|
67
|
+
if (v.length !== 32)
|
|
68
|
+
throw new Error(`${what} must be exactly 32 bytes`);
|
|
69
|
+
return encodeBase58(v);
|
|
70
|
+
}
|
|
71
|
+
function toBytes(v, what) {
|
|
72
|
+
if (typeof v === "string") {
|
|
73
|
+
const b = decodeBase58_32(v);
|
|
74
|
+
if (!b)
|
|
75
|
+
throw new Error(`${what} is not a valid base58 address`);
|
|
76
|
+
return b;
|
|
77
|
+
}
|
|
78
|
+
if (v.length !== 32)
|
|
79
|
+
throw new Error(`${what} must be exactly 32 bytes`);
|
|
80
|
+
return v;
|
|
81
|
+
}
|
|
82
|
+
function concat(...parts) {
|
|
83
|
+
const out = new Uint8Array(parts.reduce((n, p) => n + p.length, 0));
|
|
84
|
+
let o = 0;
|
|
85
|
+
for (const p of parts) {
|
|
86
|
+
out.set(p, o);
|
|
87
|
+
o += p.length;
|
|
88
|
+
}
|
|
89
|
+
return out;
|
|
90
|
+
}
|
|
91
|
+
// ---------------------------------------------------------------------------
|
|
92
|
+
// PDAs
|
|
93
|
+
// ---------------------------------------------------------------------------
|
|
94
|
+
const TE = new TextEncoder();
|
|
95
|
+
/** `["config"]` and `["auction", handle]` — the two extra accounts `Recover`
|
|
96
|
+
* needs. */
|
|
97
|
+
export async function deriveRecoveryAccounts(find, programId, handle) {
|
|
98
|
+
const program = toBase58(programId, "programId");
|
|
99
|
+
const [config, auction] = await Promise.all([
|
|
100
|
+
find([TE.encode("config")], program),
|
|
101
|
+
find([TE.encode("auction"), toBytes(handle, "handle")], program),
|
|
102
|
+
]);
|
|
103
|
+
return { config, auction };
|
|
104
|
+
}
|
|
105
|
+
/** `set_recovery(Option<Pubkey>)` — data: disc ‖ 0 | (1 ‖ pubkey). */
|
|
106
|
+
export function buildSetRecoveryIx(p) {
|
|
107
|
+
const arg = p.recovery === null ? Uint8Array.of(0) : concat(Uint8Array.of(1), toBytes(p.recovery, "recovery"));
|
|
108
|
+
return {
|
|
109
|
+
programId: toBase58(p.programId, "programId"),
|
|
110
|
+
keys: [
|
|
111
|
+
{ pubkey: toBase58(p.owner, "owner"), isSigner: true, isWritable: false },
|
|
112
|
+
{ pubkey: toBase58(p.handle, "handle"), isSigner: false, isWritable: true },
|
|
113
|
+
],
|
|
114
|
+
data: concat(SET_RECOVERY_DISCRIMINATOR, arg),
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
/** `cancel_recovery` — the owner stops a pending recovery. Data: disc only. */
|
|
118
|
+
export function buildCancelRecoveryIx(p) {
|
|
119
|
+
return {
|
|
120
|
+
programId: toBase58(p.programId, "programId"),
|
|
121
|
+
keys: [
|
|
122
|
+
{ pubkey: toBase58(p.owner, "owner"), isSigner: true, isWritable: false },
|
|
123
|
+
{ pubkey: toBase58(p.handle, "handle"), isSigner: false, isWritable: true },
|
|
124
|
+
],
|
|
125
|
+
data: CANCEL_RECOVERY_DISCRIMINATOR.slice(),
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
function recoverKeys(p) {
|
|
129
|
+
return [
|
|
130
|
+
{ pubkey: toBase58(p.recovery, "recovery"), isSigner: true, isWritable: false },
|
|
131
|
+
{ pubkey: toBase58(p.handle, "handle"), isSigner: false, isWritable: true },
|
|
132
|
+
{ pubkey: toBase58(p.config, "config"), isSigner: false, isWritable: false },
|
|
133
|
+
{ pubkey: toBase58(p.auction, "auction"), isSigner: false, isWritable: false },
|
|
134
|
+
];
|
|
135
|
+
}
|
|
136
|
+
/** `initiate_recovery(target)` — data: disc ‖ target(32). Starts the timelock;
|
|
137
|
+
* the owner can cancel until {@link RecoveryState.pending}`.completesAt`. */
|
|
138
|
+
export function buildInitiateRecoveryIx(p) {
|
|
139
|
+
return {
|
|
140
|
+
programId: toBase58(p.programId, "programId"),
|
|
141
|
+
keys: recoverKeys(p),
|
|
142
|
+
data: concat(INITIATE_RECOVERY_DISCRIMINATOR, toBytes(p.target, "target")),
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
/** `complete_recovery` — data: disc only. Valid once the timelock elapsed
|
|
146
|
+
* ({@link RecoveryState.pending}`.completable`). */
|
|
147
|
+
export function buildCompleteRecoveryIx(p) {
|
|
148
|
+
return {
|
|
149
|
+
programId: toBase58(p.programId, "programId"),
|
|
150
|
+
keys: recoverKeys(p),
|
|
151
|
+
data: COMPLETE_RECOVERY_DISCRIMINATOR.slice(),
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Decode the recovery fields of a `Handle` account's raw data (sequential:
|
|
156
|
+
* the two `Option<Pubkey>` fields shift what follows them). Null when the
|
|
157
|
+
* data is not a Handle (wrong discriminator / too short).
|
|
158
|
+
*
|
|
159
|
+
* disc(8) name(32) name_len(1) owner(32)@41 handle_type(1)
|
|
160
|
+
* recovery: Option<Pubkey> recovery_initiated_at: i64
|
|
161
|
+
* recovery_target: Option<Pubkey> registered_at: i64 bump(1)
|
|
162
|
+
* [157: nft tag(1) mint(32)]
|
|
163
|
+
*/
|
|
164
|
+
export function decodeHandleRecovery(data) {
|
|
165
|
+
if (data.length < 8 || !bytesEqual(data.subarray(0, 8), HANDLE_ACCOUNT_DISCRIMINATOR))
|
|
166
|
+
return null;
|
|
167
|
+
// The ONE sequential Handle decoder (accounts.ts) — never a second offset guess.
|
|
168
|
+
const h = parseHandleAccount(data);
|
|
169
|
+
if (!h)
|
|
170
|
+
return null;
|
|
171
|
+
return {
|
|
172
|
+
owner: encodeBase58(h.owner),
|
|
173
|
+
recovery: h.recovery ? encodeBase58(h.recovery) : null,
|
|
174
|
+
recoveryInitiatedAt: h.recoveryInitiatedAt,
|
|
175
|
+
recoveryTarget: h.recoveryTarget ? encodeBase58(h.recoveryTarget) : null,
|
|
176
|
+
registeredAt: h.registeredAt,
|
|
177
|
+
tokenized: h.nftMint !== null,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
/** `Config.recovery_timelock_secs`, or null for a pre-V2 Config / not a Config. */
|
|
181
|
+
export function readConfigRecoveryTimelock(data) {
|
|
182
|
+
if (!bytesEqual(data.subarray(0, 8), CONFIG_ACCOUNT_DISCRIMINATOR))
|
|
183
|
+
return null;
|
|
184
|
+
if (data.length < CONFIG_RECOVERY_TIMELOCK_OFFSET + 9)
|
|
185
|
+
return null;
|
|
186
|
+
return new DataView(data.buffer, data.byteOffset, data.byteLength).getBigInt64(CONFIG_RECOVERY_TIMELOCK_OFFSET, true);
|
|
187
|
+
}
|
|
188
|
+
/** The timelock `complete_recovery` enforces: `max(config ?? default, floor)`. */
|
|
189
|
+
export function effectiveRecoveryTimelock(configSecs) {
|
|
190
|
+
const v = configSecs ?? BigInt(DEFAULT_RECOVERY_TIMELOCK_SECS);
|
|
191
|
+
const floor = BigInt(MIN_RECOVERY_TIMELOCK_SECS);
|
|
192
|
+
return v > floor ? v : floor;
|
|
193
|
+
}
|
|
194
|
+
/** When a pending recovery can complete (null = none pending). Same clamp as
|
|
195
|
+
* the program, so a UI never shows an ETA earlier than it will allow. */
|
|
196
|
+
export function recoveryCompletesAt(recoveryInitiatedAt, configSecs) {
|
|
197
|
+
if (recoveryInitiatedAt === 0n)
|
|
198
|
+
return null;
|
|
199
|
+
return recoveryInitiatedAt + effectiveRecoveryTimelock(configSecs);
|
|
200
|
+
}
|
|
201
|
+
/** Pure: the state from raw Handle data + the Config timelock + now. */
|
|
202
|
+
export function computeRecoveryState(handle, handleData, configTimelockSecs, nowSecs) {
|
|
203
|
+
const f = decodeHandleRecovery(handleData);
|
|
204
|
+
if (!f)
|
|
205
|
+
return null;
|
|
206
|
+
const lock = readLockState(handleData);
|
|
207
|
+
const timelock = effectiveRecoveryTimelock(configTimelockSecs);
|
|
208
|
+
const completesAt = recoveryCompletesAt(f.recoveryInitiatedAt, configTimelockSecs);
|
|
209
|
+
// The unlock floor (lock.ts MIN_UNLOCK_TIMELOCK_SECS) equals the recovery
|
|
210
|
+
// floor today; use lock.ts's own helper so they can never drift apart.
|
|
211
|
+
const unlocksAt = lock.locked ? unlockAvailableAt(lock.unlockInitiatedAt, configTimelockSecs ?? BigInt(DEFAULT_RECOVERY_TIMELOCK_SECS)) : null;
|
|
212
|
+
const left = (t) => (t > nowSecs ? t - nowSecs : 0n);
|
|
213
|
+
return {
|
|
214
|
+
handle: toBase58(handle, "handle"),
|
|
215
|
+
owner: f.owner,
|
|
216
|
+
tokenized: f.tokenized,
|
|
217
|
+
recoveryKey: f.recovery,
|
|
218
|
+
recoveryAvailable: !f.tokenized && !lock.locked && f.recovery !== null,
|
|
219
|
+
pending: completesAt === null
|
|
220
|
+
? null
|
|
221
|
+
: {
|
|
222
|
+
target: f.recoveryTarget,
|
|
223
|
+
initiatedAt: f.recoveryInitiatedAt,
|
|
224
|
+
completesAt,
|
|
225
|
+
secondsRemaining: left(completesAt),
|
|
226
|
+
completable: nowSecs >= completesAt && !lock.locked && !f.tokenized,
|
|
227
|
+
},
|
|
228
|
+
locked: lock.locked,
|
|
229
|
+
pendingUnlock: unlocksAt === null
|
|
230
|
+
? null
|
|
231
|
+
: { initiatedAt: lock.unlockInitiatedAt, unlocksAt, secondsRemaining: left(unlocksAt), completable: nowSecs >= unlocksAt },
|
|
232
|
+
timelockSecs: timelock,
|
|
233
|
+
nowSecs,
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
async function readAccount(rpc, address, commitment) {
|
|
237
|
+
const res = (await rpc("getAccountInfo", [address, { encoding: "base64", commitment }]));
|
|
238
|
+
const v = res?.value;
|
|
239
|
+
if (!v)
|
|
240
|
+
return null;
|
|
241
|
+
const bin = atob(v.data[0]);
|
|
242
|
+
const data = new Uint8Array(bin.length);
|
|
243
|
+
for (let i = 0; i < bin.length; i++)
|
|
244
|
+
data[i] = bin.charCodeAt(i);
|
|
245
|
+
return { data, owner: v.owner };
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* Read a handle's recovery + lock state from chain (the authority — the
|
|
249
|
+
* indexer's `GET /recovery` is a convenience view): the Handle account and
|
|
250
|
+
* the Config timelock, in two `getAccountInfo` calls. Null when the handle
|
|
251
|
+
* does not exist. Throws when the account exists but is not a registry
|
|
252
|
+
* Handle.
|
|
253
|
+
*/
|
|
254
|
+
export async function fetchRecoveryState(rpc, opts) {
|
|
255
|
+
const programId = toBase58(opts.programId, "programId");
|
|
256
|
+
const handle = toBase58(opts.handle, "handle");
|
|
257
|
+
let config;
|
|
258
|
+
if (opts.config !== undefined)
|
|
259
|
+
config = toBase58(opts.config, "config");
|
|
260
|
+
else if (opts.find)
|
|
261
|
+
config = await opts.find([TE.encode("config")], programId);
|
|
262
|
+
else
|
|
263
|
+
throw new Error("fetchRecoveryState needs `config` or `find`");
|
|
264
|
+
const commitment = opts.commitment ?? "confirmed";
|
|
265
|
+
const [h, c] = await Promise.all([readAccount(rpc, handle, commitment), readAccount(rpc, config, commitment)]);
|
|
266
|
+
if (!h)
|
|
267
|
+
return null;
|
|
268
|
+
if (h.owner !== programId)
|
|
269
|
+
throw new Error("handle account is not owned by the registry program");
|
|
270
|
+
const timelock = c && c.owner === programId ? readConfigRecoveryTimelock(c.data) : null;
|
|
271
|
+
const now = opts.nowSecs ?? BigInt(Math.floor(Date.now() / 1000));
|
|
272
|
+
const s = computeRecoveryState(handle, h.data, timelock, now);
|
|
273
|
+
if (!s)
|
|
274
|
+
throw new Error("account is not an x1id Handle");
|
|
275
|
+
return s;
|
|
276
|
+
}
|