@x1id/resolve 0.9.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/domainProof.d.ts +166 -0
- package/dist/domainProof.js +278 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/lease.d.ts +92 -0
- package/dist/lease.js +117 -0
- package/dist/voucher.d.ts +53 -0
- package/dist/voucher.js +56 -0
- package/package.json +1 -1
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Independent, re-verifiable domain->wallet proof via DNS-over-HTTPS (#8318,
|
|
3
|
+
* Approach C — the "auditable attestor" honesty upgrade).
|
|
4
|
+
*
|
|
5
|
+
* # Why this exists
|
|
6
|
+
*
|
|
7
|
+
* A `dns`-kind {@link import("./attestation.js").HandleAttestation} records only
|
|
8
|
+
* that x1id's admin-rotatable attestor key *signed off* on an off-chain DNS
|
|
9
|
+
* lookup ("attestation.ts" says this plainly): the chain stores the attestor's
|
|
10
|
+
* say-so, not a proof. The fully-trustless answers (an on-chain DNSSEC verifier,
|
|
11
|
+
* or a zkTLS proof — Approaches A/B in `docs/trustless-domain-proof-plan.md`)
|
|
12
|
+
* are BLOCKED on this X1 fork because the runtime does not have the crypto
|
|
13
|
+
* primitives they need active (`big_mod_exp` / `secp256r1` / `alt_bn128` are all
|
|
14
|
+
* feature-gated off). Until one of those lands, this module is the shippable
|
|
15
|
+
* middle ground: it lets ANYONE independently re-resolve the domain's DNS record
|
|
16
|
+
* and confirm the binding, so the attestor can no longer lie *undetected*. Trust
|
|
17
|
+
* moves from "the x1id attestor" to "DNS + the domain owner" — a real, cheap
|
|
18
|
+
* upgrade that needs no on-chain change.
|
|
19
|
+
*
|
|
20
|
+
* It is dependency-free and runs in both the browser and Node: a DoH query is a
|
|
21
|
+
* plain HTTPS GET, and the JSON shape ("application/dns-json", the Cloudflare /
|
|
22
|
+
* Google convention) is parsed by hand, like the rest of this package.
|
|
23
|
+
*
|
|
24
|
+
* # The TXT record convention (the interoperability contract)
|
|
25
|
+
*
|
|
26
|
+
* A domain owner proves control of `<domain>` by publishing a TXT record at the
|
|
27
|
+
* name **`_x1id.<domain>`** whose value is:
|
|
28
|
+
*
|
|
29
|
+
* x1id-wallet=<base58 wallet pubkey>
|
|
30
|
+
*
|
|
31
|
+
* - The prefix is the exported {@link TXT_BINDING_PREFIX} (`x1id-wallet=`).
|
|
32
|
+
* - `<base58 wallet pubkey>` is the 32-byte X1/SVM wallet address, base58, that
|
|
33
|
+
* the domain is bound to (the handle's authority wallet). Multiple TXT records
|
|
34
|
+
* under the same name are allowed — a domain may bind more than one wallet
|
|
35
|
+
* (e.g. during a key rotation); {@link resolveDomainBinding} returns them all.
|
|
36
|
+
* - A single TXT record split into several character-strings (RFC 1035 §3.3.14 /
|
|
37
|
+
* RFC 7208 §3.3) is concatenated with no separator before the prefix is
|
|
38
|
+
* matched, exactly as a mail SPF/DKIM verifier would.
|
|
39
|
+
*
|
|
40
|
+
* This is the contract a domain owner follows, so it is deliberately simple and
|
|
41
|
+
* stable. It mirrors the `_dnslink`/`_atproto`/`_domainkey` underscore-prefixed
|
|
42
|
+
* convention rather than putting x1id data on the apex TXT (which would collide
|
|
43
|
+
* with SPF/verification tokens from every other service).
|
|
44
|
+
*
|
|
45
|
+
* # NOTE — the paired attestor-side follow-up (NOT done here)
|
|
46
|
+
*
|
|
47
|
+
* This SDK-only change gives verifiers the re-check primitive. The *other* half
|
|
48
|
+
* of Approach C — having the attestor publish the exact `_x1id.<domain>` TXT it
|
|
49
|
+
* checked inside the attestation's `evidence_hash` preimage bundle, so a
|
|
50
|
+
* back-dated attestation whose TXT was later removed is still disprovable — is a
|
|
51
|
+
* change to the admin-api attest tooling (`tools/`), tracked as a FOLLOW-UP. See
|
|
52
|
+
* `docs/trustless-domain-proof-plan.md` §4. Until that ships,
|
|
53
|
+
* {@link verifyDomainBinding} can only confirm a binding that is *still live* in
|
|
54
|
+
* DNS; it cannot audit a removed one.
|
|
55
|
+
*/
|
|
56
|
+
import type { RpcFn } from "./accounts.js";
|
|
57
|
+
/** The canonical prefix of the `_x1id.<domain>` TXT binding value. A TXT string
|
|
58
|
+
* that (after character-string concatenation) starts with this, followed by a
|
|
59
|
+
* valid base58 32-byte address, is an x1id binding record. */
|
|
60
|
+
export declare const TXT_BINDING_PREFIX = "x1id-wallet=";
|
|
61
|
+
/** The DNS label prefixed to a domain to locate its x1id binding TXT records —
|
|
62
|
+
* the query name is `${TXT_NAME_PREFIX}.<domain>`. */
|
|
63
|
+
export declare const TXT_NAME_PREFIX = "_x1id";
|
|
64
|
+
/** Built-in DoH endpoints (both speak the `application/dns-json` shape). */
|
|
65
|
+
declare const DOH_PROVIDERS: Readonly<{
|
|
66
|
+
readonly cloudflare: "https://cloudflare-dns.com/dns-query";
|
|
67
|
+
readonly google: "https://dns.google/resolve";
|
|
68
|
+
}>;
|
|
69
|
+
export interface DomainProofOptions {
|
|
70
|
+
/** Which built-in DoH provider to use when {@link dohUrl} is not given.
|
|
71
|
+
* Default `"cloudflare"`. */
|
|
72
|
+
readonly provider?: keyof typeof DOH_PROVIDERS;
|
|
73
|
+
/** A full DoH endpoint base URL (overrides {@link provider}), e.g. a
|
|
74
|
+
* self-hosted resolver. The query string (`?name=...&type=TXT`) is appended;
|
|
75
|
+
* a URL that already has query params is respected (`&` is used). Must speak
|
|
76
|
+
* the `application/dns-json` response shape. */
|
|
77
|
+
readonly dohUrl?: string;
|
|
78
|
+
/** `fetch` override — inject one in tests (return canned DoH JSON) or to use a
|
|
79
|
+
* custom transport. Defaults to `globalThis.fetch`. */
|
|
80
|
+
readonly fetchImpl?: typeof fetch;
|
|
81
|
+
/** Abort the DoH request after this many ms. Default 6000. */
|
|
82
|
+
readonly timeoutMs?: number;
|
|
83
|
+
}
|
|
84
|
+
/** The outcome of resolving a domain's `_x1id` TXT bindings. `ok` is about the
|
|
85
|
+
* LOOKUP succeeding, not about any wallet being found: a domain that simply has
|
|
86
|
+
* no binding is `ok: true` with an empty `wallets`. `ok: false` means the DNS
|
|
87
|
+
* query itself failed (network, timeout, DoH error) and the answer is unknown —
|
|
88
|
+
* never a throw. */
|
|
89
|
+
export interface DomainBindingResult {
|
|
90
|
+
/** The DNS lookup completed and its answer was parsed. */
|
|
91
|
+
readonly ok: boolean;
|
|
92
|
+
/** The domain queried (lowercased, canonical). */
|
|
93
|
+
readonly domain: string;
|
|
94
|
+
/** Every distinct wallet the `_x1id.<domain>` TXT records bind, base58
|
|
95
|
+
* (canonical spelling), first-seen order, de-duplicated. Empty when the
|
|
96
|
+
* domain publishes no valid binding (or when `ok` is false). */
|
|
97
|
+
readonly wallets: string[];
|
|
98
|
+
/** Why the lookup failed, or a note when it succeeded with nothing usable.
|
|
99
|
+
* Absent on a clean success that found bindings. */
|
|
100
|
+
readonly reason?: string;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Resolve the wallet(s) a domain binds via its `_x1id.<domain>` TXT records,
|
|
104
|
+
* independently of x1id's attestor — a DoH GET, parsed and validated defensively.
|
|
105
|
+
*
|
|
106
|
+
* Never throws: a network failure, timeout, or malformed answer comes back as
|
|
107
|
+
* `{ ok: false, reason }`; a domain with no binding as `{ ok: true, wallets: [] }`.
|
|
108
|
+
*/
|
|
109
|
+
export declare function resolveDomainBinding(domain: string, opts?: DomainProofOptions): Promise<DomainBindingResult>;
|
|
110
|
+
/** The outcome of checking a domain against an expected wallet. */
|
|
111
|
+
export interface VerifyDomainBindingResult {
|
|
112
|
+
/** True iff the domain's `_x1id` TXT binds `expectedWallet` and the lookup
|
|
113
|
+
* succeeded. */
|
|
114
|
+
readonly ok: boolean;
|
|
115
|
+
/** Every wallet the domain binds (base58, canonical) — for context/UX even on
|
|
116
|
+
* a mismatch. Empty on a lookup failure. */
|
|
117
|
+
readonly found: string[];
|
|
118
|
+
/** Why `ok` is false: `"invalid-domain"` / `"invalid-wallet"` for bad inputs,
|
|
119
|
+
* a DoH failure reason (`"timeout"`, `"network-error"`, ...), `"not-found"`
|
|
120
|
+
* when the domain publishes no binding, or `"mismatch"` when it binds other
|
|
121
|
+
* wallets but not the expected one. Absent when `ok` is true. */
|
|
122
|
+
readonly reason?: string;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* The core "anyone can check it" primitive: does `<domain>` independently bind
|
|
126
|
+
* `expectedWallet` in DNS? Returns `{ ok, found, reason? }` and never throws.
|
|
127
|
+
*
|
|
128
|
+
* A verifier calling this trusts DNS and the domain owner — NOT x1id's attestor.
|
|
129
|
+
*/
|
|
130
|
+
export declare function verifyDomainBinding(domain: string, expectedWallet: string, opts?: DomainProofOptions): Promise<VerifyDomainBindingResult>;
|
|
131
|
+
/** The combined on-chain-attestation + live-DNS check. */
|
|
132
|
+
export interface VerifyDomainAttestationResult {
|
|
133
|
+
/** Both halves hold: x1id attested the domain (a non-stale `dns` attestation)
|
|
134
|
+
* AND DNS still independently confirms the binding. This is the strongest
|
|
135
|
+
* signal this SDK can give for a domain today. */
|
|
136
|
+
readonly ok: boolean;
|
|
137
|
+
/** The handle holds a NON-STALE `dns`-kind attestation for the current owner
|
|
138
|
+
* ("x1id attested it"). */
|
|
139
|
+
readonly attested: boolean;
|
|
140
|
+
/** DNS independently confirms the binding right now ("still current, attestor
|
|
141
|
+
* couldn't have lied"). */
|
|
142
|
+
readonly dnsConfirmed: boolean;
|
|
143
|
+
/** The wallet(s) the domain currently binds in DNS. */
|
|
144
|
+
readonly found: string[];
|
|
145
|
+
/** A note when `ok` is false — e.g. `"attestation-error"` if the chain read
|
|
146
|
+
* threw, or the {@link verifyDomainBinding} reason for the DNS half. */
|
|
147
|
+
readonly reason?: string;
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Convenience: confirm BOTH that x1id attested a domain AND that DNS still
|
|
151
|
+
* independently backs it up — so a caller learns not just "x1id says so" but
|
|
152
|
+
* "and it's verifiably still true", closing the attestor's ability to have lied.
|
|
153
|
+
*
|
|
154
|
+
* `registeredAt` / `recordsClearedAt` are the owning Handle's fields the caller
|
|
155
|
+
* already decoded (see {@link import("./accounts.js").parseHandleAccount}); they
|
|
156
|
+
* are MANDATORY here for the same reason every attestation read demands them —
|
|
157
|
+
* the staleness rule (`attested_at >= registered_at`, and `>= records_cleared_at`)
|
|
158
|
+
* decides whether an attestation vouches for the *current* owner at all. The
|
|
159
|
+
* two halves are evaluated independently, so a caller can see which one holds:
|
|
160
|
+
* a chain-read failure does not suppress the DNS answer, and vice-versa.
|
|
161
|
+
*
|
|
162
|
+
* Never throws: a failed chain read degrades to `attested: false` with
|
|
163
|
+
* `reason: "attestation-error"`, mirroring `signin.ts`'s best-effort summary.
|
|
164
|
+
*/
|
|
165
|
+
export declare function verifyDomainAttestation(rpc: RpcFn, programId: string, handleAccount: string, registeredAt: bigint, recordsClearedAt: bigint, domain: string, expectedWallet: string, opts?: DomainProofOptions): Promise<VerifyDomainAttestationResult>;
|
|
166
|
+
export {};
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Independent, re-verifiable domain->wallet proof via DNS-over-HTTPS (#8318,
|
|
3
|
+
* Approach C — the "auditable attestor" honesty upgrade).
|
|
4
|
+
*
|
|
5
|
+
* # Why this exists
|
|
6
|
+
*
|
|
7
|
+
* A `dns`-kind {@link import("./attestation.js").HandleAttestation} records only
|
|
8
|
+
* that x1id's admin-rotatable attestor key *signed off* on an off-chain DNS
|
|
9
|
+
* lookup ("attestation.ts" says this plainly): the chain stores the attestor's
|
|
10
|
+
* say-so, not a proof. The fully-trustless answers (an on-chain DNSSEC verifier,
|
|
11
|
+
* or a zkTLS proof — Approaches A/B in `docs/trustless-domain-proof-plan.md`)
|
|
12
|
+
* are BLOCKED on this X1 fork because the runtime does not have the crypto
|
|
13
|
+
* primitives they need active (`big_mod_exp` / `secp256r1` / `alt_bn128` are all
|
|
14
|
+
* feature-gated off). Until one of those lands, this module is the shippable
|
|
15
|
+
* middle ground: it lets ANYONE independently re-resolve the domain's DNS record
|
|
16
|
+
* and confirm the binding, so the attestor can no longer lie *undetected*. Trust
|
|
17
|
+
* moves from "the x1id attestor" to "DNS + the domain owner" — a real, cheap
|
|
18
|
+
* upgrade that needs no on-chain change.
|
|
19
|
+
*
|
|
20
|
+
* It is dependency-free and runs in both the browser and Node: a DoH query is a
|
|
21
|
+
* plain HTTPS GET, and the JSON shape ("application/dns-json", the Cloudflare /
|
|
22
|
+
* Google convention) is parsed by hand, like the rest of this package.
|
|
23
|
+
*
|
|
24
|
+
* # The TXT record convention (the interoperability contract)
|
|
25
|
+
*
|
|
26
|
+
* A domain owner proves control of `<domain>` by publishing a TXT record at the
|
|
27
|
+
* name **`_x1id.<domain>`** whose value is:
|
|
28
|
+
*
|
|
29
|
+
* x1id-wallet=<base58 wallet pubkey>
|
|
30
|
+
*
|
|
31
|
+
* - The prefix is the exported {@link TXT_BINDING_PREFIX} (`x1id-wallet=`).
|
|
32
|
+
* - `<base58 wallet pubkey>` is the 32-byte X1/SVM wallet address, base58, that
|
|
33
|
+
* the domain is bound to (the handle's authority wallet). Multiple TXT records
|
|
34
|
+
* under the same name are allowed — a domain may bind more than one wallet
|
|
35
|
+
* (e.g. during a key rotation); {@link resolveDomainBinding} returns them all.
|
|
36
|
+
* - A single TXT record split into several character-strings (RFC 1035 §3.3.14 /
|
|
37
|
+
* RFC 7208 §3.3) is concatenated with no separator before the prefix is
|
|
38
|
+
* matched, exactly as a mail SPF/DKIM verifier would.
|
|
39
|
+
*
|
|
40
|
+
* This is the contract a domain owner follows, so it is deliberately simple and
|
|
41
|
+
* stable. It mirrors the `_dnslink`/`_atproto`/`_domainkey` underscore-prefixed
|
|
42
|
+
* convention rather than putting x1id data on the apex TXT (which would collide
|
|
43
|
+
* with SPF/verification tokens from every other service).
|
|
44
|
+
*
|
|
45
|
+
* # NOTE — the paired attestor-side follow-up (NOT done here)
|
|
46
|
+
*
|
|
47
|
+
* This SDK-only change gives verifiers the re-check primitive. The *other* half
|
|
48
|
+
* of Approach C — having the attestor publish the exact `_x1id.<domain>` TXT it
|
|
49
|
+
* checked inside the attestation's `evidence_hash` preimage bundle, so a
|
|
50
|
+
* back-dated attestation whose TXT was later removed is still disprovable — is a
|
|
51
|
+
* change to the admin-api attest tooling (`tools/`), tracked as a FOLLOW-UP. See
|
|
52
|
+
* `docs/trustless-domain-proof-plan.md` §4. Until that ships,
|
|
53
|
+
* {@link verifyDomainBinding} can only confirm a binding that is *still live* in
|
|
54
|
+
* DNS; it cannot audit a removed one.
|
|
55
|
+
*/
|
|
56
|
+
import { decodeBase58_32, encodeBase58 } from "./base58.js";
|
|
57
|
+
import { fetchAttestations } from "./attestation.js";
|
|
58
|
+
import { attestationsPassGate, GateKind } from "./gate.js";
|
|
59
|
+
/** The canonical prefix of the `_x1id.<domain>` TXT binding value. A TXT string
|
|
60
|
+
* that (after character-string concatenation) starts with this, followed by a
|
|
61
|
+
* valid base58 32-byte address, is an x1id binding record. */
|
|
62
|
+
export const TXT_BINDING_PREFIX = "x1id-wallet=";
|
|
63
|
+
/** The DNS label prefixed to a domain to locate its x1id binding TXT records —
|
|
64
|
+
* the query name is `${TXT_NAME_PREFIX}.<domain>`. */
|
|
65
|
+
export const TXT_NAME_PREFIX = "_x1id";
|
|
66
|
+
/** DoH JSON `type` code for a TXT record (RFC 1035). */
|
|
67
|
+
const DNS_TYPE_TXT = 16;
|
|
68
|
+
/** Built-in DoH endpoints (both speak the `application/dns-json` shape). */
|
|
69
|
+
const DOH_PROVIDERS = Object.freeze({
|
|
70
|
+
cloudflare: "https://cloudflare-dns.com/dns-query",
|
|
71
|
+
google: "https://dns.google/resolve",
|
|
72
|
+
});
|
|
73
|
+
const DEFAULT_TIMEOUT_MS = 6000;
|
|
74
|
+
/** A domain is a case-insensitive dotted hostname (1..253 chars, labels 1..63 of
|
|
75
|
+
* `[a-z0-9-]`, no leading/trailing hyphen). Strict enough that the name can be
|
|
76
|
+
* safely interpolated into a query and can never smuggle a second query param. */
|
|
77
|
+
const DOMAIN_RE = /^(?=.{1,253}$)([a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)(\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)+$/;
|
|
78
|
+
function normalizeDomain(domain) {
|
|
79
|
+
if (typeof domain !== "string")
|
|
80
|
+
return null;
|
|
81
|
+
// Tolerate a trailing dot (fully-qualified form) and case; reject everything
|
|
82
|
+
// else so the name is safe to interpolate into the query.
|
|
83
|
+
const d = domain.trim().replace(/\.$/, "").toLowerCase();
|
|
84
|
+
if (!DOMAIN_RE.test(d))
|
|
85
|
+
return null;
|
|
86
|
+
return d;
|
|
87
|
+
}
|
|
88
|
+
function buildDohUrl(base, name) {
|
|
89
|
+
const q = `name=${encodeURIComponent(name)}&type=TXT`;
|
|
90
|
+
return base.includes("?") ? `${base}&${q}` : `${base}?${q}`;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Concatenate a DoH TXT `data` field into its logical string.
|
|
94
|
+
*
|
|
95
|
+
* DoH returns a TXT record's RDATA as its character-strings, each wrapped in
|
|
96
|
+
* double quotes; a record split into several character-strings comes back as
|
|
97
|
+
* several quoted runs (e.g. `"\"x1id-ver\" \"ification=...\""`). Per RFC 1035
|
|
98
|
+
* §3.3.14 they concatenate with NO separator. Escaped `\"` and `\\` inside a
|
|
99
|
+
* run are unescaped. A `data` with no quotes at all (some resolvers) is used
|
|
100
|
+
* verbatim.
|
|
101
|
+
*/
|
|
102
|
+
function unquoteTxtData(data) {
|
|
103
|
+
if (!data.includes('"'))
|
|
104
|
+
return data;
|
|
105
|
+
let out = "";
|
|
106
|
+
let inStr = false;
|
|
107
|
+
for (let i = 0; i < data.length; i++) {
|
|
108
|
+
const c = data[i];
|
|
109
|
+
if (!inStr) {
|
|
110
|
+
if (c === '"')
|
|
111
|
+
inStr = true;
|
|
112
|
+
// characters between quoted runs (whitespace) are dropped
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
if (c === "\\") {
|
|
116
|
+
// escaped char: take the next byte literally
|
|
117
|
+
const next = data[i + 1];
|
|
118
|
+
if (next !== undefined) {
|
|
119
|
+
out += next;
|
|
120
|
+
i++;
|
|
121
|
+
}
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
if (c === '"') {
|
|
125
|
+
inStr = false;
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
out += c;
|
|
129
|
+
}
|
|
130
|
+
return out;
|
|
131
|
+
}
|
|
132
|
+
/** Extract a canonical base58 wallet from one concatenated TXT string, or null
|
|
133
|
+
* if it is not a well-formed x1id binding. */
|
|
134
|
+
function walletFromTxt(txt) {
|
|
135
|
+
if (!txt.startsWith(TXT_BINDING_PREFIX))
|
|
136
|
+
return null;
|
|
137
|
+
const raw = txt.slice(TXT_BINDING_PREFIX.length).trim();
|
|
138
|
+
const bytes = decodeBase58_32(raw);
|
|
139
|
+
if (!bytes)
|
|
140
|
+
return null;
|
|
141
|
+
// Re-encode so a non-canonical base58 spelling and the caller's wallet compare
|
|
142
|
+
// byte-for-byte, exactly like the rest of the SDK does.
|
|
143
|
+
return encodeBase58(bytes);
|
|
144
|
+
}
|
|
145
|
+
async function fetchDohJson(url, fetchImpl, timeoutMs) {
|
|
146
|
+
const controller = new AbortController();
|
|
147
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
148
|
+
try {
|
|
149
|
+
const res = await fetchImpl(url, {
|
|
150
|
+
signal: controller.signal,
|
|
151
|
+
// Cloudflare's /dns-query REQUIRES this to return JSON; harmless for Google.
|
|
152
|
+
headers: { accept: "application/dns-json" },
|
|
153
|
+
});
|
|
154
|
+
if (!res.ok)
|
|
155
|
+
return { ok: false, reason: `doh-http-${res.status}` };
|
|
156
|
+
const json = (await res.json());
|
|
157
|
+
return { ok: true, json };
|
|
158
|
+
}
|
|
159
|
+
catch (e) {
|
|
160
|
+
const reason = e?.name === "AbortError" ? "timeout" : "network-error";
|
|
161
|
+
return { ok: false, reason };
|
|
162
|
+
}
|
|
163
|
+
finally {
|
|
164
|
+
clearTimeout(timer);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Resolve the wallet(s) a domain binds via its `_x1id.<domain>` TXT records,
|
|
169
|
+
* independently of x1id's attestor — a DoH GET, parsed and validated defensively.
|
|
170
|
+
*
|
|
171
|
+
* Never throws: a network failure, timeout, or malformed answer comes back as
|
|
172
|
+
* `{ ok: false, reason }`; a domain with no binding as `{ ok: true, wallets: [] }`.
|
|
173
|
+
*/
|
|
174
|
+
export async function resolveDomainBinding(domain, opts) {
|
|
175
|
+
const canonical = normalizeDomain(domain);
|
|
176
|
+
if (canonical === null) {
|
|
177
|
+
return { ok: false, domain: String(domain), wallets: [], reason: "invalid-domain" };
|
|
178
|
+
}
|
|
179
|
+
const fetchImpl = opts?.fetchImpl ?? globalThis.fetch;
|
|
180
|
+
if (typeof fetchImpl !== "function") {
|
|
181
|
+
return { ok: false, domain: canonical, wallets: [], reason: "no-fetch" };
|
|
182
|
+
}
|
|
183
|
+
const base = opts?.dohUrl ?? DOH_PROVIDERS[opts?.provider ?? "cloudflare"];
|
|
184
|
+
const url = buildDohUrl(base, `${TXT_NAME_PREFIX}.${canonical}`);
|
|
185
|
+
const timeoutMs = opts?.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
186
|
+
const res = await fetchDohJson(url, fetchImpl, timeoutMs);
|
|
187
|
+
if (!res.ok)
|
|
188
|
+
return { ok: false, domain: canonical, wallets: [], reason: res.reason };
|
|
189
|
+
const body = res.json;
|
|
190
|
+
if (typeof body !== "object" || body === null) {
|
|
191
|
+
return { ok: false, domain: canonical, wallets: [], reason: "malformed-response" };
|
|
192
|
+
}
|
|
193
|
+
const b = body;
|
|
194
|
+
const status = typeof b.Status === "number" ? b.Status : -1;
|
|
195
|
+
// 0 = NOERROR (parse answers), 3 = NXDOMAIN (the name simply doesn't exist =
|
|
196
|
+
// no binding, a clean empty result). Anything else (SERVFAIL, etc.) is an
|
|
197
|
+
// unknown answer, not a proven absence.
|
|
198
|
+
if (status === 3)
|
|
199
|
+
return { ok: true, domain: canonical, wallets: [] };
|
|
200
|
+
if (status !== 0)
|
|
201
|
+
return { ok: false, domain: canonical, wallets: [], reason: `doh-status-${status}` };
|
|
202
|
+
const answers = Array.isArray(b.Answer) ? b.Answer : [];
|
|
203
|
+
const wallets = [];
|
|
204
|
+
for (const ans of answers) {
|
|
205
|
+
if (typeof ans !== "object" || ans === null)
|
|
206
|
+
continue;
|
|
207
|
+
const a = ans;
|
|
208
|
+
if (a.type !== DNS_TYPE_TXT)
|
|
209
|
+
continue; // skip CNAME/other chained answers
|
|
210
|
+
if (typeof a.data !== "string")
|
|
211
|
+
continue;
|
|
212
|
+
const wallet = walletFromTxt(unquoteTxtData(a.data));
|
|
213
|
+
if (wallet && !wallets.includes(wallet))
|
|
214
|
+
wallets.push(wallet);
|
|
215
|
+
}
|
|
216
|
+
return { ok: true, domain: canonical, wallets };
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* The core "anyone can check it" primitive: does `<domain>` independently bind
|
|
220
|
+
* `expectedWallet` in DNS? Returns `{ ok, found, reason? }` and never throws.
|
|
221
|
+
*
|
|
222
|
+
* A verifier calling this trusts DNS and the domain owner — NOT x1id's attestor.
|
|
223
|
+
*/
|
|
224
|
+
export async function verifyDomainBinding(domain, expectedWallet, opts) {
|
|
225
|
+
const wanted = typeof expectedWallet === "string" ? decodeBase58_32(expectedWallet) : null;
|
|
226
|
+
if (!wanted)
|
|
227
|
+
return { ok: false, found: [], reason: "invalid-wallet" };
|
|
228
|
+
const wantedBase58 = encodeBase58(wanted);
|
|
229
|
+
const res = await resolveDomainBinding(domain, opts);
|
|
230
|
+
if (!res.ok)
|
|
231
|
+
return { ok: false, found: [], reason: res.reason ?? "lookup-failed" };
|
|
232
|
+
if (res.wallets.length === 0)
|
|
233
|
+
return { ok: false, found: [], reason: "not-found" };
|
|
234
|
+
if (!res.wallets.includes(wantedBase58)) {
|
|
235
|
+
return { ok: false, found: res.wallets, reason: "mismatch" };
|
|
236
|
+
}
|
|
237
|
+
return { ok: true, found: res.wallets };
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Convenience: confirm BOTH that x1id attested a domain AND that DNS still
|
|
241
|
+
* independently backs it up — so a caller learns not just "x1id says so" but
|
|
242
|
+
* "and it's verifiably still true", closing the attestor's ability to have lied.
|
|
243
|
+
*
|
|
244
|
+
* `registeredAt` / `recordsClearedAt` are the owning Handle's fields the caller
|
|
245
|
+
* already decoded (see {@link import("./accounts.js").parseHandleAccount}); they
|
|
246
|
+
* are MANDATORY here for the same reason every attestation read demands them —
|
|
247
|
+
* the staleness rule (`attested_at >= registered_at`, and `>= records_cleared_at`)
|
|
248
|
+
* decides whether an attestation vouches for the *current* owner at all. The
|
|
249
|
+
* two halves are evaluated independently, so a caller can see which one holds:
|
|
250
|
+
* a chain-read failure does not suppress the DNS answer, and vice-versa.
|
|
251
|
+
*
|
|
252
|
+
* Never throws: a failed chain read degrades to `attested: false` with
|
|
253
|
+
* `reason: "attestation-error"`, mirroring `signin.ts`'s best-effort summary.
|
|
254
|
+
*/
|
|
255
|
+
export async function verifyDomainAttestation(rpc, programId, handleAccount, registeredAt, recordsClearedAt, domain, expectedWallet, opts) {
|
|
256
|
+
// Half 1 (on-chain): a non-stale dns attestation for the current owner. A
|
|
257
|
+
// read failure must not sink the DNS half — degrade, never throw.
|
|
258
|
+
let attested = false;
|
|
259
|
+
let attestationReason;
|
|
260
|
+
try {
|
|
261
|
+
const attestations = await fetchAttestations(rpc, programId, handleAccount, registeredAt, recordsClearedAt);
|
|
262
|
+
attested = attestationsPassGate(attestations, { kind: GateKind.dns });
|
|
263
|
+
}
|
|
264
|
+
catch {
|
|
265
|
+
attestationReason = "attestation-error";
|
|
266
|
+
}
|
|
267
|
+
// Half 2 (off-chain, independent): DNS still binds the expected wallet.
|
|
268
|
+
const dns = await verifyDomainBinding(domain, expectedWallet, opts);
|
|
269
|
+
const ok = attested && dns.ok;
|
|
270
|
+
const reason = ok ? undefined : (attestationReason ?? (attested ? dns.reason : "not-attested"));
|
|
271
|
+
return {
|
|
272
|
+
ok,
|
|
273
|
+
attested,
|
|
274
|
+
dnsConfirmed: dns.ok,
|
|
275
|
+
found: dns.found,
|
|
276
|
+
...(reason !== undefined ? { reason } : {}),
|
|
277
|
+
};
|
|
278
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -41,6 +41,7 @@ export * from "./integrator.js";
|
|
|
41
41
|
export * from "./voucher.js";
|
|
42
42
|
export * from "./agent.js";
|
|
43
43
|
export * from "./register.js";
|
|
44
|
+
export * from "./lease.js";
|
|
44
45
|
export * from "./accounts.js";
|
|
45
46
|
export * from "./x402.js";
|
|
46
47
|
export * from "./recordWrite.js";
|
|
@@ -48,6 +49,7 @@ export * from "./clearRecords.js";
|
|
|
48
49
|
export * from "./commitReveal.js";
|
|
49
50
|
export * from "./pnftTransfer.js";
|
|
50
51
|
export * from "./signin.js";
|
|
52
|
+
export * from "./domainProof.js";
|
|
51
53
|
import { type Chain, type Resolved } from "./types.js";
|
|
52
54
|
import { WasmResolver } from "./wasm.js";
|
|
53
55
|
import { type HandleRecord } from "./records.js";
|
package/dist/index.js
CHANGED
|
@@ -41,6 +41,7 @@ export * from "./integrator.js";
|
|
|
41
41
|
export * from "./voucher.js";
|
|
42
42
|
export * from "./agent.js";
|
|
43
43
|
export * from "./register.js";
|
|
44
|
+
export * from "./lease.js";
|
|
44
45
|
// Was previously imported here for internal use only, never re-exported —
|
|
45
46
|
// promoted to public API 2026-09-22 because a second real package (mcp/)
|
|
46
47
|
// now needs `parseHandleAccount`/`RpcFn` directly rather than duplicating
|
|
@@ -55,6 +56,7 @@ export * from "./clearRecords.js";
|
|
|
55
56
|
export * from "./commitReveal.js";
|
|
56
57
|
export * from "./pnftTransfer.js";
|
|
57
58
|
export * from "./signin.js";
|
|
59
|
+
export * from "./domainProof.js";
|
|
58
60
|
import { ResolveError, CHAIN_COIN_TYPE } from "./types.js";
|
|
59
61
|
import { parseName } from "./parse.js";
|
|
60
62
|
import { encodeBase58, decodeBase58_32 } from "./base58.js";
|
package/dist/lease.d.ts
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lease-to-own instructions (#8150, #8320) — the SIBLING of `register` for term
|
|
3
|
+
* leases, not a replacement: the permanent one-time `register` path
|
|
4
|
+
* (register.ts) is untouched and stays the default.
|
|
5
|
+
*
|
|
6
|
+
* - `register_leased` — claim an unregistered `["handle", name]` under a lease,
|
|
7
|
+
* paying the first installment now.
|
|
8
|
+
* - `renew_lease` — pay more toward a leased handle. PERMISSIONLESS: the payer
|
|
9
|
+
* need not be the owner (anyone may keep or pay off someone else's lease,
|
|
10
|
+
* ENS-registrar-style).
|
|
11
|
+
* - `reclaim_expired_lease` — take over a leased handle whose grace period has
|
|
12
|
+
* ENDED, under a FRESH lease; the reclaimer becomes `owner`. Also
|
|
13
|
+
* permissionless. The program enforces `now >= expires_at + grace`.
|
|
14
|
+
*
|
|
15
|
+
* Hand-rolled like the rest of this package — no Anchor client, no
|
|
16
|
+
* `@solana/web3.js`. PDAs are NOT derived here (see delegate.ts) — derive
|
|
17
|
+
* `["config"]` / `["handle", name]` with your runtime's `findProgramAddress`,
|
|
18
|
+
* and read `Config.treasury` with {@link fetchConfigTreasury} (register.ts).
|
|
19
|
+
*
|
|
20
|
+
* `payment` is in lamports — the amount accepted toward the lease now. The
|
|
21
|
+
* on-chain `lease_seconds_purchased` / `lease_accept_payment` logic converts it
|
|
22
|
+
* to lease-time, capped at the permanent price (offering the full permanent
|
|
23
|
+
* price up front settles the handle permanently in one instruction, writing
|
|
24
|
+
* `expires_at = 0`). This builder does not compute that split; simulate or
|
|
25
|
+
* mirror it client-side if you need to preview before signing.
|
|
26
|
+
*/
|
|
27
|
+
import type { AddressLike, BuiltInstruction } from "./delegate.js";
|
|
28
|
+
import type { HandleTypeValue } from "./voucher.js";
|
|
29
|
+
/** `sha256("global:register_leased")[0..8]`. */
|
|
30
|
+
export declare const REGISTER_LEASED_DISCRIMINATOR: Uint8Array;
|
|
31
|
+
/** `sha256("global:renew_lease")[0..8]`. */
|
|
32
|
+
export declare const RENEW_LEASE_DISCRIMINATOR: Uint8Array;
|
|
33
|
+
/** `sha256("global:reclaim_expired_lease")[0..8]`. */
|
|
34
|
+
export declare const RECLAIM_EXPIRED_LEASE_DISCRIMINATOR: Uint8Array;
|
|
35
|
+
export interface RegisterLeasedParams {
|
|
36
|
+
/** The registry program id. */
|
|
37
|
+
readonly programId: AddressLike;
|
|
38
|
+
/** Pays the first lease installment + the `Handle` account's rent. Signer. */
|
|
39
|
+
readonly payer: AddressLike;
|
|
40
|
+
/** The handle's owner. Need not sign — may be registered on another's behalf. */
|
|
41
|
+
readonly owner: AddressLike;
|
|
42
|
+
/** The `["config"]` PDA. */
|
|
43
|
+
readonly config: AddressLike;
|
|
44
|
+
/** `Config.treasury` — read it fresh via {@link fetchConfigTreasury}. */
|
|
45
|
+
readonly treasury: AddressLike;
|
|
46
|
+
/** The `["handle", name]` PDA being claimed — must not already exist. */
|
|
47
|
+
readonly handle: AddressLike;
|
|
48
|
+
/** Canonical form (`parseName(...).canonical`), 1..=32 bytes. */
|
|
49
|
+
readonly name: string;
|
|
50
|
+
readonly handleType: HandleTypeValue;
|
|
51
|
+
/** First lease payment, in lamports. `>= permanentPrice` settles it outright. */
|
|
52
|
+
readonly payment: bigint;
|
|
53
|
+
}
|
|
54
|
+
/** Build `register_leased`. Data: `disc ‖ borsh(name) ‖ handleType(u8) ‖ payment(u64 LE)`. */
|
|
55
|
+
export declare function buildRegisterLeasedIx(p: RegisterLeasedParams): BuiltInstruction;
|
|
56
|
+
export interface RenewLeaseParams {
|
|
57
|
+
/** The registry program id. */
|
|
58
|
+
readonly programId: AddressLike;
|
|
59
|
+
/** Pays toward the lease. Signer. PERMISSIONLESS — need NOT be the owner. */
|
|
60
|
+
readonly payer: AddressLike;
|
|
61
|
+
/** The `["config"]` PDA (read-only on this path). */
|
|
62
|
+
readonly config: AddressLike;
|
|
63
|
+
/** `Config.treasury` — the program address-constrains it to this, so it can't
|
|
64
|
+
* be substituted; read it fresh via {@link fetchConfigTreasury}. */
|
|
65
|
+
readonly treasury: AddressLike;
|
|
66
|
+
/** The leased `["handle", name]` PDA being paid toward. */
|
|
67
|
+
readonly handle: AddressLike;
|
|
68
|
+
/** Lamports to accept toward the lease (capped at the permanent price on-chain). */
|
|
69
|
+
readonly payment: bigint;
|
|
70
|
+
}
|
|
71
|
+
/** Build `renew_lease`. Data: `disc ‖ payment(u64 LE)`. */
|
|
72
|
+
export declare function buildRenewLeaseIx(p: RenewLeaseParams): BuiltInstruction;
|
|
73
|
+
export interface ReclaimExpiredLeaseParams {
|
|
74
|
+
/** The registry program id. */
|
|
75
|
+
readonly programId: AddressLike;
|
|
76
|
+
/** Pays the fresh lease's first installment; becomes the new owner. Signer.
|
|
77
|
+
* PERMISSIONLESS — anyone may reclaim a grace-ended leased handle. */
|
|
78
|
+
readonly payer: AddressLike;
|
|
79
|
+
/** The new owner (typically the same as `payer`). Need not sign. */
|
|
80
|
+
readonly owner: AddressLike;
|
|
81
|
+
/** The `["config"]` PDA. */
|
|
82
|
+
readonly config: AddressLike;
|
|
83
|
+
/** `Config.treasury` — address-constrained on-chain; {@link fetchConfigTreasury}. */
|
|
84
|
+
readonly treasury: AddressLike;
|
|
85
|
+
/** The leased `["handle", name]` PDA being reclaimed — its grace must have ended. */
|
|
86
|
+
readonly handle: AddressLike;
|
|
87
|
+
readonly handleType: HandleTypeValue;
|
|
88
|
+
/** The fresh lease's first payment, in lamports. */
|
|
89
|
+
readonly payment: bigint;
|
|
90
|
+
}
|
|
91
|
+
/** Build `reclaim_expired_lease`. Data: `disc ‖ handleType(u8) ‖ payment(u64 LE)`. */
|
|
92
|
+
export declare function buildReclaimExpiredLeaseIx(p: ReclaimExpiredLeaseParams): BuiltInstruction;
|
package/dist/lease.js
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lease-to-own instructions (#8150, #8320) — the SIBLING of `register` for term
|
|
3
|
+
* leases, not a replacement: the permanent one-time `register` path
|
|
4
|
+
* (register.ts) is untouched and stays the default.
|
|
5
|
+
*
|
|
6
|
+
* - `register_leased` — claim an unregistered `["handle", name]` under a lease,
|
|
7
|
+
* paying the first installment now.
|
|
8
|
+
* - `renew_lease` — pay more toward a leased handle. PERMISSIONLESS: the payer
|
|
9
|
+
* need not be the owner (anyone may keep or pay off someone else's lease,
|
|
10
|
+
* ENS-registrar-style).
|
|
11
|
+
* - `reclaim_expired_lease` — take over a leased handle whose grace period has
|
|
12
|
+
* ENDED, under a FRESH lease; the reclaimer becomes `owner`. Also
|
|
13
|
+
* permissionless. The program enforces `now >= expires_at + grace`.
|
|
14
|
+
*
|
|
15
|
+
* Hand-rolled like the rest of this package — no Anchor client, no
|
|
16
|
+
* `@solana/web3.js`. PDAs are NOT derived here (see delegate.ts) — derive
|
|
17
|
+
* `["config"]` / `["handle", name]` with your runtime's `findProgramAddress`,
|
|
18
|
+
* and read `Config.treasury` with {@link fetchConfigTreasury} (register.ts).
|
|
19
|
+
*
|
|
20
|
+
* `payment` is in lamports — the amount accepted toward the lease now. The
|
|
21
|
+
* on-chain `lease_seconds_purchased` / `lease_accept_payment` logic converts it
|
|
22
|
+
* to lease-time, capped at the permanent price (offering the full permanent
|
|
23
|
+
* price up front settles the handle permanently in one instruction, writing
|
|
24
|
+
* `expires_at = 0`). This builder does not compute that split; simulate or
|
|
25
|
+
* mirror it client-side if you need to preview before signing.
|
|
26
|
+
*/
|
|
27
|
+
import { encodeBase58, decodeBase58_32 } from "./base58.js";
|
|
28
|
+
/** `sha256("global:register_leased")[0..8]`. */
|
|
29
|
+
export const REGISTER_LEASED_DISCRIMINATOR = Uint8Array.from([83, 192, 142, 193, 191, 53, 8, 15]);
|
|
30
|
+
/** `sha256("global:renew_lease")[0..8]`. */
|
|
31
|
+
export const RENEW_LEASE_DISCRIMINATOR = Uint8Array.from([11, 121, 141, 53, 216, 217, 123, 139]);
|
|
32
|
+
/** `sha256("global:reclaim_expired_lease")[0..8]`. */
|
|
33
|
+
export const RECLAIM_EXPIRED_LEASE_DISCRIMINATOR = Uint8Array.from([66, 80, 245, 8, 1, 21, 161, 110]);
|
|
34
|
+
const SYSTEM_PROGRAM = "11111111111111111111111111111111";
|
|
35
|
+
function toBytes32(v, what) {
|
|
36
|
+
if (typeof v === "string") {
|
|
37
|
+
const b = decodeBase58_32(v);
|
|
38
|
+
if (!b)
|
|
39
|
+
throw new Error(`${what} is not a valid base58 address`);
|
|
40
|
+
return b;
|
|
41
|
+
}
|
|
42
|
+
if (v.length !== 32)
|
|
43
|
+
throw new Error(`${what} must be exactly 32 bytes`);
|
|
44
|
+
return v;
|
|
45
|
+
}
|
|
46
|
+
function toBase58(v, what) {
|
|
47
|
+
return encodeBase58(toBytes32(v, what));
|
|
48
|
+
}
|
|
49
|
+
function assertHandleType(t) {
|
|
50
|
+
if (!Number.isInteger(t) || t < 0 || t > 3) {
|
|
51
|
+
throw new Error("handleType must be 0 (Human), 1 (Merchant), 2 (Org) or 3 (Agent)");
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
function encodeName(name) {
|
|
55
|
+
const b = new TextEncoder().encode(name);
|
|
56
|
+
if (b.length < 1 || b.length > 32)
|
|
57
|
+
throw new Error("name must be 1..=32 bytes (UTF-8)");
|
|
58
|
+
return b;
|
|
59
|
+
}
|
|
60
|
+
function u64le(n) {
|
|
61
|
+
if (n < 0n)
|
|
62
|
+
throw new Error("payment must be a non-negative u64 (lamports)");
|
|
63
|
+
const b = new Uint8Array(8);
|
|
64
|
+
new DataView(b.buffer).setBigUint64(0, n, true);
|
|
65
|
+
return b;
|
|
66
|
+
}
|
|
67
|
+
/** Build `register_leased`. Data: `disc ‖ borsh(name) ‖ handleType(u8) ‖ payment(u64 LE)`. */
|
|
68
|
+
export function buildRegisterLeasedIx(p) {
|
|
69
|
+
assertHandleType(p.handleType);
|
|
70
|
+
const nb = encodeName(p.name);
|
|
71
|
+
const data = new Uint8Array(8 + 4 + nb.length + 1 + 8);
|
|
72
|
+
data.set(REGISTER_LEASED_DISCRIMINATOR, 0);
|
|
73
|
+
new DataView(data.buffer).setUint32(8, nb.length, true);
|
|
74
|
+
data.set(nb, 12);
|
|
75
|
+
data[12 + nb.length] = p.handleType;
|
|
76
|
+
data.set(u64le(p.payment), 12 + nb.length + 1);
|
|
77
|
+
const keys = [
|
|
78
|
+
{ pubkey: toBase58(p.payer, "payer"), isSigner: true, isWritable: true },
|
|
79
|
+
{ pubkey: toBase58(p.owner, "owner"), isSigner: false, isWritable: false },
|
|
80
|
+
{ pubkey: toBase58(p.config, "config"), isSigner: false, isWritable: true },
|
|
81
|
+
{ pubkey: toBase58(p.treasury, "treasury"), isSigner: false, isWritable: true },
|
|
82
|
+
{ pubkey: toBase58(p.handle, "handle"), isSigner: false, isWritable: true },
|
|
83
|
+
{ pubkey: SYSTEM_PROGRAM, isSigner: false, isWritable: false },
|
|
84
|
+
];
|
|
85
|
+
return { programId: toBase58(p.programId, "programId"), keys, data };
|
|
86
|
+
}
|
|
87
|
+
/** Build `renew_lease`. Data: `disc ‖ payment(u64 LE)`. */
|
|
88
|
+
export function buildRenewLeaseIx(p) {
|
|
89
|
+
const data = new Uint8Array(8 + 8);
|
|
90
|
+
data.set(RENEW_LEASE_DISCRIMINATOR, 0);
|
|
91
|
+
data.set(u64le(p.payment), 8);
|
|
92
|
+
const keys = [
|
|
93
|
+
{ pubkey: toBase58(p.payer, "payer"), isSigner: true, isWritable: true },
|
|
94
|
+
{ pubkey: toBase58(p.config, "config"), isSigner: false, isWritable: false },
|
|
95
|
+
{ pubkey: toBase58(p.treasury, "treasury"), isSigner: false, isWritable: true },
|
|
96
|
+
{ pubkey: toBase58(p.handle, "handle"), isSigner: false, isWritable: true },
|
|
97
|
+
{ pubkey: SYSTEM_PROGRAM, isSigner: false, isWritable: false },
|
|
98
|
+
];
|
|
99
|
+
return { programId: toBase58(p.programId, "programId"), keys, data };
|
|
100
|
+
}
|
|
101
|
+
/** Build `reclaim_expired_lease`. Data: `disc ‖ handleType(u8) ‖ payment(u64 LE)`. */
|
|
102
|
+
export function buildReclaimExpiredLeaseIx(p) {
|
|
103
|
+
assertHandleType(p.handleType);
|
|
104
|
+
const data = new Uint8Array(8 + 1 + 8);
|
|
105
|
+
data.set(RECLAIM_EXPIRED_LEASE_DISCRIMINATOR, 0);
|
|
106
|
+
data[8] = p.handleType;
|
|
107
|
+
data.set(u64le(p.payment), 9);
|
|
108
|
+
const keys = [
|
|
109
|
+
{ pubkey: toBase58(p.payer, "payer"), isSigner: true, isWritable: true },
|
|
110
|
+
{ pubkey: toBase58(p.owner, "owner"), isSigner: false, isWritable: false },
|
|
111
|
+
{ pubkey: toBase58(p.config, "config"), isSigner: false, isWritable: true },
|
|
112
|
+
{ pubkey: toBase58(p.treasury, "treasury"), isSigner: false, isWritable: true },
|
|
113
|
+
{ pubkey: toBase58(p.handle, "handle"), isSigner: false, isWritable: true },
|
|
114
|
+
{ pubkey: SYSTEM_PROGRAM, isSigner: false, isWritable: false },
|
|
115
|
+
];
|
|
116
|
+
return { programId: toBase58(p.programId, "programId"), keys, data };
|
|
117
|
+
}
|
package/dist/voucher.d.ts
CHANGED
|
@@ -128,3 +128,56 @@ export interface RefundVoucherParams {
|
|
|
128
128
|
}
|
|
129
129
|
/** Build `refund_voucher` — payer-signed, only valid at/after `expiresAt`. */
|
|
130
130
|
export declare function buildRefundVoucherIx(p: RefundVoucherParams): BuiltInstruction;
|
|
131
|
+
/** `sha256("global:claim_voucher_tokenized")[0..8]`. */
|
|
132
|
+
export declare const CLAIM_VOUCHER_TOKENIZED_DISCRIMINATOR: Uint8Array;
|
|
133
|
+
export interface ClaimVoucherTokenizedParams {
|
|
134
|
+
readonly programId: AddressLike;
|
|
135
|
+
/** Submits + signs; fronts the tx fee (reimbursed) + the handle rent. */
|
|
136
|
+
readonly claimer: AddressLike;
|
|
137
|
+
readonly config: AddressLike;
|
|
138
|
+
readonly treasury: AddressLike;
|
|
139
|
+
readonly voucher: AddressLike;
|
|
140
|
+
/** The original voucher payer — receives rent back when the voucher closes. */
|
|
141
|
+
readonly voucherPayer: AddressLike;
|
|
142
|
+
/** Who the pNFT + ownership go to: the bound recipient, or the claimer for an
|
|
143
|
+
* open voucher. The mint's ATA + token record derive against THIS address. */
|
|
144
|
+
readonly owner: AddressLike;
|
|
145
|
+
/** The `["handle", name]` PDA. */
|
|
146
|
+
readonly handle: AddressLike;
|
|
147
|
+
readonly name: string;
|
|
148
|
+
/** `["nft_mint", handle]` PDA. */
|
|
149
|
+
readonly mint: AddressLike;
|
|
150
|
+
/** `owner`'s associated token account for `mint` (receives the frozen unit). */
|
|
151
|
+
readonly ownerTokenAccount: AddressLike;
|
|
152
|
+
/** Metaplex metadata PDA for `mint`. */
|
|
153
|
+
readonly metadata: AddressLike;
|
|
154
|
+
/** Metaplex master-edition PDA for `mint`. */
|
|
155
|
+
readonly masterEdition: AddressLike;
|
|
156
|
+
/** pNFT TokenRecord PDA for (`mint`, `ownerTokenAccount`). */
|
|
157
|
+
readonly tokenRecord: AddressLike;
|
|
158
|
+
/** The collection parent mint (`["collection_mint"]`). */
|
|
159
|
+
readonly collectionMint: AddressLike;
|
|
160
|
+
/** The collection parent metadata (writable — sized-collection bump). */
|
|
161
|
+
readonly collectionMetadata: AddressLike;
|
|
162
|
+
/** The collection parent master edition. */
|
|
163
|
+
readonly collectionMasterEdition: AddressLike;
|
|
164
|
+
/** The collection authority PDA. */
|
|
165
|
+
readonly collectionAuthority: AddressLike;
|
|
166
|
+
/** The integrator wallet — REQUIRED iff the voucher carries one; must equal
|
|
167
|
+
* the voucher's `integrator`. Omit otherwise. */
|
|
168
|
+
readonly integrator?: AddressLike;
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Build `claim_voucher_tokenized` (#8335) — the tokenized sibling of
|
|
172
|
+
* {@link buildClaimVoucherIx} and the DEFAULT path the live gift-claim flow
|
|
173
|
+
* uses: it registers the handle AND mints its tradeable capability pNFT in one
|
|
174
|
+
* instruction. The pNFT + ownership go to `owner` (the bound recipient, or the
|
|
175
|
+
* claimer for an open voucher), so `ownerTokenAccount` / `tokenRecord` derive
|
|
176
|
+
* against `owner`, not the claimer. Data: `disc ‖ borsh(name)` — the handle type
|
|
177
|
+
* comes from the voucher, not an argument.
|
|
178
|
+
*
|
|
179
|
+
* The register + 3 Metaplex CPIs exceed the 200k default compute budget —
|
|
180
|
+
* prepend a ComputeBudget `setComputeUnitLimit` instruction in the SAME
|
|
181
|
+
* transaction (the app uses ~400k) or the claim fails on-chain.
|
|
182
|
+
*/
|
|
183
|
+
export declare function buildClaimVoucherTokenizedIx(p: ClaimVoucherTokenizedParams): BuiltInstruction;
|
package/dist/voucher.js
CHANGED
|
@@ -183,3 +183,59 @@ export function buildRefundVoucherIx(p) {
|
|
|
183
183
|
data,
|
|
184
184
|
};
|
|
185
185
|
}
|
|
186
|
+
// ===================== claim_voucher_tokenized (gift NFTs, #8335) =====================
|
|
187
|
+
/** `sha256("global:claim_voucher_tokenized")[0..8]`. */
|
|
188
|
+
export const CLAIM_VOUCHER_TOKENIZED_DISCRIMINATOR = Uint8Array.from([131, 111, 174, 105, 154, 248, 191, 235]);
|
|
189
|
+
// Fixed Metaplex / SPL program ids + sysvar in the pNFT-mint tail — hardcoded,
|
|
190
|
+
// never derived (matches the app's mintNft.ts).
|
|
191
|
+
const TOKEN_PROGRAM_ID = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA";
|
|
192
|
+
const ASSOCIATED_TOKEN_PROGRAM_ID = "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL";
|
|
193
|
+
const TOKEN_METADATA_PROGRAM = "metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s";
|
|
194
|
+
const SYSVAR_INSTRUCTIONS = "Sysvar1nstructions1111111111111111111111111";
|
|
195
|
+
/**
|
|
196
|
+
* Build `claim_voucher_tokenized` (#8335) — the tokenized sibling of
|
|
197
|
+
* {@link buildClaimVoucherIx} and the DEFAULT path the live gift-claim flow
|
|
198
|
+
* uses: it registers the handle AND mints its tradeable capability pNFT in one
|
|
199
|
+
* instruction. The pNFT + ownership go to `owner` (the bound recipient, or the
|
|
200
|
+
* claimer for an open voucher), so `ownerTokenAccount` / `tokenRecord` derive
|
|
201
|
+
* against `owner`, not the claimer. Data: `disc ‖ borsh(name)` — the handle type
|
|
202
|
+
* comes from the voucher, not an argument.
|
|
203
|
+
*
|
|
204
|
+
* The register + 3 Metaplex CPIs exceed the 200k default compute budget —
|
|
205
|
+
* prepend a ComputeBudget `setComputeUnitLimit` instruction in the SAME
|
|
206
|
+
* transaction (the app uses ~400k) or the claim fails on-chain.
|
|
207
|
+
*/
|
|
208
|
+
export function buildClaimVoucherTokenizedIx(p) {
|
|
209
|
+
const name = encString(p.name);
|
|
210
|
+
const data = new Uint8Array(8 + name.length);
|
|
211
|
+
data.set(CLAIM_VOUCHER_TOKENIZED_DISCRIMINATOR, 0);
|
|
212
|
+
data.set(name, 8);
|
|
213
|
+
const keys = [
|
|
214
|
+
{ pubkey: toBase58(p.claimer, "claimer"), isSigner: true, isWritable: true },
|
|
215
|
+
{ pubkey: toBase58(p.config, "config"), isSigner: false, isWritable: true },
|
|
216
|
+
{ pubkey: toBase58(p.treasury, "treasury"), isSigner: false, isWritable: true },
|
|
217
|
+
{ pubkey: toBase58(p.voucher, "voucher"), isSigner: false, isWritable: true },
|
|
218
|
+
{ pubkey: toBase58(p.voucherPayer, "voucherPayer"), isSigner: false, isWritable: true },
|
|
219
|
+
{ pubkey: toBase58(p.owner, "owner"), isSigner: false, isWritable: false },
|
|
220
|
+
{ pubkey: toBase58(p.handle, "handle"), isSigner: false, isWritable: true },
|
|
221
|
+
// Metaplex pNFT tail — order + flags verbatim from mintNft.ts::tokenizedTailKeys.
|
|
222
|
+
{ pubkey: toBase58(p.mint, "mint"), isSigner: false, isWritable: true },
|
|
223
|
+
{ pubkey: toBase58(p.ownerTokenAccount, "ownerTokenAccount"), isSigner: false, isWritable: true },
|
|
224
|
+
{ pubkey: toBase58(p.metadata, "metadata"), isSigner: false, isWritable: true },
|
|
225
|
+
{ pubkey: toBase58(p.masterEdition, "masterEdition"), isSigner: false, isWritable: true },
|
|
226
|
+
{ pubkey: toBase58(p.tokenRecord, "tokenRecord"), isSigner: false, isWritable: true },
|
|
227
|
+
{ pubkey: toBase58(p.collectionMint, "collectionMint"), isSigner: false, isWritable: false },
|
|
228
|
+
{ pubkey: toBase58(p.collectionMetadata, "collectionMetadata"), isSigner: false, isWritable: true },
|
|
229
|
+
{ pubkey: toBase58(p.collectionMasterEdition, "collectionMasterEdition"), isSigner: false, isWritable: false },
|
|
230
|
+
{ pubkey: toBase58(p.collectionAuthority, "collectionAuthority"), isSigner: false, isWritable: false },
|
|
231
|
+
{ pubkey: TOKEN_METADATA_PROGRAM, isSigner: false, isWritable: false },
|
|
232
|
+
{ pubkey: TOKEN_PROGRAM_ID, isSigner: false, isWritable: false },
|
|
233
|
+
{ pubkey: ASSOCIATED_TOKEN_PROGRAM_ID, isSigner: false, isWritable: false },
|
|
234
|
+
{ pubkey: SYSTEM_PROGRAM, isSigner: false, isWritable: false },
|
|
235
|
+
{ pubkey: SYSVAR_INSTRUCTIONS, isSigner: false, isWritable: false },
|
|
236
|
+
];
|
|
237
|
+
if (p.integrator !== undefined) {
|
|
238
|
+
keys.push({ pubkey: toBase58(p.integrator, "integrator"), isSigner: false, isWritable: true });
|
|
239
|
+
}
|
|
240
|
+
return { programId: toBase58(p.programId, "programId"), keys, data };
|
|
241
|
+
}
|