@x1id/resolve 0.3.0 → 0.7.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/LICENSE +21 -0
- package/README.md +128 -1
- package/dist/accounts.d.ts +7 -0
- package/dist/accounts.js +8 -1
- package/dist/agent.d.ts +186 -0
- package/dist/agent.js +213 -0
- package/dist/attestation.d.ts +20 -8
- package/dist/attestation.js +22 -10
- package/dist/clearRecords.d.ts +60 -0
- package/dist/clearRecords.js +69 -0
- package/dist/commitReveal.d.ts +187 -0
- package/dist/commitReveal.js +245 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +17 -2
- package/dist/pnftTransfer.d.ts +151 -0
- package/dist/pnftTransfer.js +183 -0
- package/dist/recordWrite.d.ts +136 -0
- package/dist/recordWrite.js +228 -0
- package/dist/records.d.ts +24 -7
- package/dist/records.js +26 -9
- package/dist/register.d.ts +119 -0
- package/dist/register.js +183 -0
- package/dist/subname.d.ts +5 -0
- package/dist/subname.js +6 -1
- package/dist/textRecords.d.ts +18 -7
- package/dist/textRecords.js +20 -9
- package/dist/wasm.d.ts +24 -0
- package/dist/wasm.js +45 -0
- package/dist/x402.d.ts +107 -0
- package/dist/x402.js +76 -0
- package/package.json +45 -9
- package/schema/agent-manifest.json +91 -0
- package/wasm/x1_resolve_wasm.wasm +0 -0
package/dist/attestation.js
CHANGED
|
@@ -50,6 +50,15 @@
|
|
|
50
50
|
* documented blind spot is shared with `Handle.owner`/`Primary`/
|
|
51
51
|
* `RecordDelegate`: a bearer-NFT marketplace trade bumps no epoch, so the
|
|
52
52
|
* attestation keeps reading live until the attestor re-reviews or revokes.)
|
|
53
|
+
*
|
|
54
|
+
* # `records_cleared_at` (#8139) — the SECOND, independent staleness rule
|
|
55
|
+
*
|
|
56
|
+
* `clear_records` lets an owner bulk-invalidate every record RIGHT NOW,
|
|
57
|
+
* without a transfer — its own doc comment (lib.rs) names attestations
|
|
58
|
+
* explicitly alongside records/text-records as sharing this epoch rule. The
|
|
59
|
+
* same functions therefore also demand the handle's `recordsClearedAt` (0 if
|
|
60
|
+
* never cleared, from `ParsedHandle`), and an attestation is `stale` when
|
|
61
|
+
* EITHER `attested_at < registeredAt` OR `attested_at < recordsClearedAt`.
|
|
53
62
|
*/
|
|
54
63
|
import { encodeBase58 } from "./base58.js";
|
|
55
64
|
import { decodeBase58_32 } from "./base58.js";
|
|
@@ -123,13 +132,15 @@ export function attestationKindName(kind) {
|
|
|
123
132
|
* Decode one Attestation account.
|
|
124
133
|
*
|
|
125
134
|
* `registeredAt` is the owning Handle's `registered_at`, decoded by the
|
|
126
|
-
* caller from the Handle account — it decides `stale`.
|
|
127
|
-
*
|
|
135
|
+
* caller from the Handle account — it decides `stale`. `recordsClearedAt` is
|
|
136
|
+
* that same Handle's `recordsClearedAt` (0 if never cleared, #8139), a
|
|
137
|
+
* SECOND independent staleness anchor. There is intentionally no overload
|
|
138
|
+
* without either (see the module docs).
|
|
128
139
|
*
|
|
129
140
|
* Returns null for anything that is not an Attestation: wrong length or
|
|
130
141
|
* wrong discriminator.
|
|
131
142
|
*/
|
|
132
|
-
export function decodeAttestation(raw, account, registeredAt) {
|
|
143
|
+
export function decodeAttestation(raw, account, registeredAt, recordsClearedAt) {
|
|
133
144
|
if (raw.length !== ATTESTATION_LEN)
|
|
134
145
|
return null;
|
|
135
146
|
for (let i = 0; i < 8; i++) {
|
|
@@ -155,7 +166,7 @@ export function decodeAttestation(raw, account, registeredAt) {
|
|
|
155
166
|
evidenceHash,
|
|
156
167
|
attestedAt,
|
|
157
168
|
attestor,
|
|
158
|
-
stale: attestedAt < registeredAt,
|
|
169
|
+
stale: attestedAt < registeredAt || attestedAt < recordsClearedAt,
|
|
159
170
|
};
|
|
160
171
|
}
|
|
161
172
|
/** The attestations that vouch for the CURRENT owner — `stale` ones
|
|
@@ -181,12 +192,13 @@ export function isHandleVerified(attestations) {
|
|
|
181
192
|
*
|
|
182
193
|
* `registeredAt` is `Handle.registered_at` as decoded from the Handle
|
|
183
194
|
* account the caller already has — the staleness rule needs it, and there
|
|
184
|
-
* is no variant of this function without it.
|
|
185
|
-
*
|
|
186
|
-
*
|
|
187
|
-
*
|
|
195
|
+
* is no variant of this function without it. `recordsClearedAt` is that same
|
|
196
|
+
* Handle's `recordsClearedAt` (0 if never cleared, #8139) — pass it through.
|
|
197
|
+
* Every attestation is returned, stale ones flagged, so an owner surface can
|
|
198
|
+
* show what a previous registration left behind; anything that renders a
|
|
199
|
+
* verified badge takes {@link isHandleVerified} / {@link liveAttestations}.
|
|
188
200
|
*/
|
|
189
|
-
export async function fetchAttestations(rpc, programId, handleAccount, registeredAt) {
|
|
201
|
+
export async function fetchAttestations(rpc, programId, handleAccount, registeredAt, recordsClearedAt) {
|
|
190
202
|
const res = (await rpc("getProgramAccounts", [
|
|
191
203
|
programId,
|
|
192
204
|
{
|
|
@@ -205,7 +217,7 @@ export async function fetchAttestations(rpc, programId, handleAccount, registere
|
|
|
205
217
|
const raw = new Uint8Array(bin.length);
|
|
206
218
|
for (let i = 0; i < bin.length; i++)
|
|
207
219
|
raw[i] = bin.charCodeAt(i);
|
|
208
|
-
const decoded = decodeAttestation(raw, a.pubkey, registeredAt);
|
|
220
|
+
const decoded = decodeAttestation(raw, a.pubkey, registeredAt, recordsClearedAt);
|
|
209
221
|
// Defence in depth: the memcmp filter should guarantee the handle
|
|
210
222
|
// match, but a wrong offset would silently attribute someone else's
|
|
211
223
|
// attestation to this handle. A malformed account is skipped, not fatal.
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `clear_records` (#8139): bulk-invalidate every record/text-record/
|
|
3
|
+
* attestation on a handle RIGHT NOW, without a transfer, by bumping
|
|
4
|
+
* `Handle.records_cleared_at` to the current time. The accounts
|
|
5
|
+
* (`accounts.ts` `ParsedHandle.recordsClearedAt`) and read-side staleness
|
|
6
|
+
* rule (every decoder in `records.ts` / `textRecords.ts` / `attestation.ts`:
|
|
7
|
+
* `updated_at < recordsClearedAt` is stale, alongside the `registeredAt`
|
|
8
|
+
* epoch rule) have existed since #8139 shipped; this module is the one
|
|
9
|
+
* missing piece — the instruction builder to actually trigger a clear
|
|
10
|
+
* (WP #8212).
|
|
11
|
+
*
|
|
12
|
+
* Hand-rolled like the rest of this package — no Anchor client, no
|
|
13
|
+
* `@solana/web3.js` import (it stays an optional peer). The builder returns
|
|
14
|
+
* the same transport-neutral {@link BuiltInstruction} `delegate.ts` uses.
|
|
15
|
+
*
|
|
16
|
+
* # Authority: STRICT current-authority, no delegate
|
|
17
|
+
*
|
|
18
|
+
* `clear_records` is gated by `require_current_authority` in lib.rs — the
|
|
19
|
+
* SAME strict check `lock_handle`/`initiate_unlock`/`complete_unlock` use
|
|
20
|
+
* (see `lock.ts`'s module docs), not the record-editing delegate model
|
|
21
|
+
* (`require_record_edit_authority`, `recordWrite.ts`'s
|
|
22
|
+
* `RecordEditAuthorityParams`). A handle's active `RecordDelegate` can add,
|
|
23
|
+
* edit, and verify individual records, but CANNOT bulk-invalidate all of
|
|
24
|
+
* them — that stays an owner/NFT-holder-only action, on purpose: nuking
|
|
25
|
+
* every record is a much bigger blast radius than editing one. There is
|
|
26
|
+
* deliberately no `recordDelegate` param here.
|
|
27
|
+
*
|
|
28
|
+
* A TOKENIZED handle's current holder appends their ATA for the handle's
|
|
29
|
+
* NFT mint as the sole remaining account (`holderTokenAccount`), exactly
|
|
30
|
+
* like `lock.ts`'s `LockParams`/`UnlockParams`; omit it for an untokenized
|
|
31
|
+
* handle.
|
|
32
|
+
*/
|
|
33
|
+
import type { AddressLike, BuiltInstruction } from "./delegate.js";
|
|
34
|
+
/** Anchor instruction discriminator: `sha256("global:clear_records")[0..8]`.
|
|
35
|
+
* Pinned (this SDK is zero-dependency and cannot assume WebCrypto SHA-256
|
|
36
|
+
* everywhere it runs); asserted against a re-derivation in the test suite
|
|
37
|
+
* so a typo can never silently pass. */
|
|
38
|
+
export declare const CLEAR_RECORDS_DISCRIMINATOR: Uint8Array;
|
|
39
|
+
export interface ClearRecordsParams {
|
|
40
|
+
/** The registry program id. */
|
|
41
|
+
readonly programId: AddressLike;
|
|
42
|
+
/** The handle's CURRENT authority (untokenized owner, or NFT holder).
|
|
43
|
+
* Signer, writable — also pays the one-time account grow through
|
|
44
|
+
* `records_cleared_at`'s extension region on a handle that has never
|
|
45
|
+
* been grown that far. */
|
|
46
|
+
readonly owner: AddressLike;
|
|
47
|
+
/** The `["handle", name]` PDA. */
|
|
48
|
+
readonly handle: AddressLike;
|
|
49
|
+
/** TOKENIZED handles only: the holder's associated token account for the
|
|
50
|
+
* handle's NFT mint, appended as the strict check's remaining-account
|
|
51
|
+
* proof. Omit for an untokenized handle. */
|
|
52
|
+
readonly holderTokenAccount?: AddressLike;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Build `clear_records` — bump `Handle.records_cleared_at` to now, so every
|
|
56
|
+
* record/text-record/attestation with an `updated_at` before this instant
|
|
57
|
+
* reads as stale everywhere in this SDK, without touching any of those
|
|
58
|
+
* accounts individually. Idempotent (re-clearing just re-stamps the clock).
|
|
59
|
+
*/
|
|
60
|
+
export declare function buildClearRecordsIx(p: ClearRecordsParams): BuiltInstruction;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `clear_records` (#8139): bulk-invalidate every record/text-record/
|
|
3
|
+
* attestation on a handle RIGHT NOW, without a transfer, by bumping
|
|
4
|
+
* `Handle.records_cleared_at` to the current time. The accounts
|
|
5
|
+
* (`accounts.ts` `ParsedHandle.recordsClearedAt`) and read-side staleness
|
|
6
|
+
* rule (every decoder in `records.ts` / `textRecords.ts` / `attestation.ts`:
|
|
7
|
+
* `updated_at < recordsClearedAt` is stale, alongside the `registeredAt`
|
|
8
|
+
* epoch rule) have existed since #8139 shipped; this module is the one
|
|
9
|
+
* missing piece — the instruction builder to actually trigger a clear
|
|
10
|
+
* (WP #8212).
|
|
11
|
+
*
|
|
12
|
+
* Hand-rolled like the rest of this package — no Anchor client, no
|
|
13
|
+
* `@solana/web3.js` import (it stays an optional peer). The builder returns
|
|
14
|
+
* the same transport-neutral {@link BuiltInstruction} `delegate.ts` uses.
|
|
15
|
+
*
|
|
16
|
+
* # Authority: STRICT current-authority, no delegate
|
|
17
|
+
*
|
|
18
|
+
* `clear_records` is gated by `require_current_authority` in lib.rs — the
|
|
19
|
+
* SAME strict check `lock_handle`/`initiate_unlock`/`complete_unlock` use
|
|
20
|
+
* (see `lock.ts`'s module docs), not the record-editing delegate model
|
|
21
|
+
* (`require_record_edit_authority`, `recordWrite.ts`'s
|
|
22
|
+
* `RecordEditAuthorityParams`). A handle's active `RecordDelegate` can add,
|
|
23
|
+
* edit, and verify individual records, but CANNOT bulk-invalidate all of
|
|
24
|
+
* them — that stays an owner/NFT-holder-only action, on purpose: nuking
|
|
25
|
+
* every record is a much bigger blast radius than editing one. There is
|
|
26
|
+
* deliberately no `recordDelegate` param here.
|
|
27
|
+
*
|
|
28
|
+
* A TOKENIZED handle's current holder appends their ATA for the handle's
|
|
29
|
+
* NFT mint as the sole remaining account (`holderTokenAccount`), exactly
|
|
30
|
+
* like `lock.ts`'s `LockParams`/`UnlockParams`; omit it for an untokenized
|
|
31
|
+
* handle.
|
|
32
|
+
*/
|
|
33
|
+
import { encodeBase58, decodeBase58_32 } from "./base58.js";
|
|
34
|
+
/** Anchor instruction discriminator: `sha256("global:clear_records")[0..8]`.
|
|
35
|
+
* Pinned (this SDK is zero-dependency and cannot assume WebCrypto SHA-256
|
|
36
|
+
* everywhere it runs); asserted against a re-derivation in the test suite
|
|
37
|
+
* so a typo can never silently pass. */
|
|
38
|
+
export const CLEAR_RECORDS_DISCRIMINATOR = Uint8Array.from([
|
|
39
|
+
150, 232, 238, 45, 8, 105, 50, 153,
|
|
40
|
+
]);
|
|
41
|
+
const SYSTEM_PROGRAM = "11111111111111111111111111111111";
|
|
42
|
+
function toBase58(v, what) {
|
|
43
|
+
if (typeof v === "string") {
|
|
44
|
+
const b = decodeBase58_32(v);
|
|
45
|
+
if (!b)
|
|
46
|
+
throw new Error(`${what} is not a valid base58 address`);
|
|
47
|
+
return encodeBase58(b);
|
|
48
|
+
}
|
|
49
|
+
if (v.length !== 32)
|
|
50
|
+
throw new Error(`${what} must be exactly 32 bytes`);
|
|
51
|
+
return encodeBase58(v);
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Build `clear_records` — bump `Handle.records_cleared_at` to now, so every
|
|
55
|
+
* record/text-record/attestation with an `updated_at` before this instant
|
|
56
|
+
* reads as stale everywhere in this SDK, without touching any of those
|
|
57
|
+
* accounts individually. Idempotent (re-clearing just re-stamps the clock).
|
|
58
|
+
*/
|
|
59
|
+
export function buildClearRecordsIx(p) {
|
|
60
|
+
const keys = [
|
|
61
|
+
{ pubkey: toBase58(p.owner, "owner"), isSigner: true, isWritable: true },
|
|
62
|
+
{ pubkey: toBase58(p.handle, "handle"), isSigner: false, isWritable: true },
|
|
63
|
+
{ pubkey: SYSTEM_PROGRAM, isSigner: false, isWritable: false },
|
|
64
|
+
];
|
|
65
|
+
if (p.holderTokenAccount !== undefined) {
|
|
66
|
+
keys.push({ pubkey: toBase58(p.holderTokenAccount, "holderTokenAccount"), isSigner: false, isWritable: false });
|
|
67
|
+
}
|
|
68
|
+
return { programId: toBase58(p.programId, "programId"), keys, data: CLEAR_RECORDS_DISCRIMINATOR.slice() };
|
|
69
|
+
}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Commit-reveal registration (#8134 / #8199): instruction builders and
|
|
3
|
+
* account decoding for `commit` / `register_revealed` / `cancel_commitment`
|
|
4
|
+
* — the anti-front-running path. `register` (register.ts) stays the plain
|
|
5
|
+
* single-step sibling; this module adds the two-step flow: stake out a
|
|
6
|
+
* hash, wait `Config.min_commitment_age_secs`, then reveal.
|
|
7
|
+
*
|
|
8
|
+
* Hand-rolled like the rest of this package — no Anchor client, no
|
|
9
|
+
* `@solana/web3.js` import. PDAs are NOT derived here (see delegate.ts's
|
|
10
|
+
* module docs for why) — derive `["commitment", hash]` (seed constant:
|
|
11
|
+
* {@link COMMITMENT_SEED}) with your runtime's canonical
|
|
12
|
+
* `findProgramAddress`, e.g. web3.js:
|
|
13
|
+
*
|
|
14
|
+
* ```ts
|
|
15
|
+
* PublicKey.findProgramAddressSync(
|
|
16
|
+
* [Buffer.from(COMMITMENT_SEED), Buffer.from(hash)],
|
|
17
|
+
* programId,
|
|
18
|
+
* );
|
|
19
|
+
* ```
|
|
20
|
+
*
|
|
21
|
+
* # The commitment hash
|
|
22
|
+
*
|
|
23
|
+
* `sha256(name_bytes ‖ owner_pubkey ‖ handle_type_byte ‖ secret[32] ‖
|
|
24
|
+
* program_id_bytes)` — the exact preimage order of the program's own
|
|
25
|
+
* `commitment_hash` (lib.rs), byte-verified against the deployed program by
|
|
26
|
+
* the E2E reveal phase (tools/scripts/e2e/journey-audit.ts). sha256, NOT
|
|
27
|
+
* keccak, for the same documented reason as `text_key_hash`: off-chain
|
|
28
|
+
* derivation is plain WebCrypto with no extra dependency — so
|
|
29
|
+
* {@link commitmentHash} is async, mirroring textRecords.ts's
|
|
30
|
+
* `hashTextKey`. The program id in the preimage is the SVM analog of
|
|
31
|
+
* ENS/ArcNS's `chainid + controller` domain separation: a commitment cannot
|
|
32
|
+
* replay against a different deployment.
|
|
33
|
+
*
|
|
34
|
+
* # Lifecycle (mirror of lib.rs)
|
|
35
|
+
*
|
|
36
|
+
* - `commit(commitment_hash)` `init`s `["commitment", hash]`, recording only
|
|
37
|
+
* `payer` + `committed_at` — the name is never on chain until reveal.
|
|
38
|
+
* Anyone may pay (the security is the secret preimage, not who pays).
|
|
39
|
+
* - `register_revealed(name, handle_type, secret)` recomputes the hash
|
|
40
|
+
* on-chain from the now-public fields; the supplied `commitment` account
|
|
41
|
+
* must sit at exactly the PDA that hash derives (`CommitmentMismatch`,
|
|
42
|
+
* 6077, otherwise). Valid only inside the window: at least
|
|
43
|
+
* `min_commitment_age_secs` after `committed_at` (`CommitmentTooNew`,
|
|
44
|
+
* 6074) and strictly before `max_commitment_age_secs` (`CommitmentTooOld`,
|
|
45
|
+
* 6075). Consuming closes the commitment — rent back. Pricing, integrator
|
|
46
|
+
* split and the `Handle` write are identical to `register` (both call the
|
|
47
|
+
* program's `register_core`); the result is a PERMANENT registration.
|
|
48
|
+
* - `cancel_commitment()` closes an unconsumed commitment, payer-only
|
|
49
|
+
* (`NotCommitmentPayer`, 6080), allowed at ANY age — the escape hatch for
|
|
50
|
+
* a lost secret or an abandoned flow, so rent is never stranded.
|
|
51
|
+
*
|
|
52
|
+
* A SECRET THAT IS LOST STRANDS THE COMMITMENT: without it the hash cannot
|
|
53
|
+
* be recomputed, so the commitment can only ever be `cancel_commitment`ed
|
|
54
|
+
* (which at least needs its address, discoverable via
|
|
55
|
+
* `getProgramAccounts` on the payer). Persist the secret client-side the
|
|
56
|
+
* moment it is generated, before `commit` is even sent.
|
|
57
|
+
*/
|
|
58
|
+
import type { AddressLike, BuiltInstruction } from "./delegate.js";
|
|
59
|
+
import type { RpcFn } from "./accounts.js";
|
|
60
|
+
import type { HandleTypeValue } from "./voucher.js";
|
|
61
|
+
/** Seed prefix of the commitment PDA: `["commitment", commitmentHash]`. */
|
|
62
|
+
export declare const COMMITMENT_SEED = "commitment";
|
|
63
|
+
/** Anchor instruction discriminator: `sha256("global:commit")[0..8]`. */
|
|
64
|
+
export declare const COMMIT_DISCRIMINATOR: Uint8Array;
|
|
65
|
+
/** Anchor instruction discriminator: `sha256("global:register_revealed")[0..8]`. */
|
|
66
|
+
export declare const REGISTER_REVEALED_DISCRIMINATOR: Uint8Array;
|
|
67
|
+
/** Anchor instruction discriminator: `sha256("global:cancel_commitment")[0..8]`. */
|
|
68
|
+
export declare const CANCEL_COMMITMENT_DISCRIMINATOR: Uint8Array;
|
|
69
|
+
/** Anchor account discriminator: `sha256("account:Commitment")[0..8]`. */
|
|
70
|
+
export declare const COMMITMENT_DISCRIMINATOR: Uint8Array;
|
|
71
|
+
/** `Commitment` account size: disc(8) payer(32) committed_at(8) bump(1). */
|
|
72
|
+
export declare const COMMITMENT_LEN = 49;
|
|
73
|
+
export interface CommitmentHashParams {
|
|
74
|
+
/** Canonical form (`parseName(...).canonical`), 1..=32 bytes. */
|
|
75
|
+
readonly name: string;
|
|
76
|
+
/** The owner the reveal will register the handle to — must be the SAME
|
|
77
|
+
* key later passed as `register_revealed`'s `owner`, or the reveal fails
|
|
78
|
+
* closed (`CommitmentMismatch`). */
|
|
79
|
+
readonly owner: AddressLike;
|
|
80
|
+
readonly handleType: HandleTypeValue;
|
|
81
|
+
/** 32 bytes of fresh CSPRNG output (`crypto.getRandomValues(new
|
|
82
|
+
* Uint8Array(32))`). Persist it — see the module docs. */
|
|
83
|
+
readonly secret: Uint8Array;
|
|
84
|
+
/** The registry program id — part of the preimage (domain separation). */
|
|
85
|
+
readonly programId: AddressLike;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* The 32-byte commitment hash — `sha256(name ‖ owner ‖ handle_type ‖ secret
|
|
89
|
+
* ‖ program_id)`, the program's own preimage order (see the module docs).
|
|
90
|
+
* WebCrypto (`crypto.subtle`), so it is async — mirrors `hashTextKey`.
|
|
91
|
+
*/
|
|
92
|
+
export declare function commitmentHash(p: CommitmentHashParams): Promise<Uint8Array>;
|
|
93
|
+
export interface CommitParams {
|
|
94
|
+
/** The registry program id. */
|
|
95
|
+
readonly programId: AddressLike;
|
|
96
|
+
/** Pays the commitment's rent; recorded as the only key allowed to
|
|
97
|
+
* `cancel_commitment`. Signer. */
|
|
98
|
+
readonly payer: AddressLike;
|
|
99
|
+
/** The `["commitment", commitmentHash]` PDA — derive it with your
|
|
100
|
+
* runtime's canonical `findProgramAddress` (see the module docs). */
|
|
101
|
+
readonly commitment: AddressLike;
|
|
102
|
+
/** The 32-byte hash from {@link commitmentHash}. */
|
|
103
|
+
readonly commitmentHash: Uint8Array;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Build `commit` — stake out the hash. Only the hash goes on chain; the
|
|
107
|
+
* name stays private until reveal. Account order matches `Commit<'info>`
|
|
108
|
+
* (lib.rs) exactly: payer, commitment, system_program.
|
|
109
|
+
*/
|
|
110
|
+
export declare function buildCommitIx(p: CommitParams): BuiltInstruction;
|
|
111
|
+
export interface RegisterRevealedParams {
|
|
112
|
+
/** The registry program id. */
|
|
113
|
+
readonly programId: AddressLike;
|
|
114
|
+
/** Pays the registration fee and the `Handle` account's rent. Signer.
|
|
115
|
+
* Need NOT be the commitment's payer — the binding is the hash preimage,
|
|
116
|
+
* and the consumed commitment's rent refunds to this payer. */
|
|
117
|
+
readonly payer: AddressLike;
|
|
118
|
+
/** The handle's owner. Need not sign, but MUST be the key the commitment
|
|
119
|
+
* hashed (see {@link CommitmentHashParams.owner}). */
|
|
120
|
+
readonly owner: AddressLike;
|
|
121
|
+
/** The `["config"]` PDA. */
|
|
122
|
+
readonly config: AddressLike;
|
|
123
|
+
/** `Config.treasury` — read it fresh via `fetchConfigTreasury`
|
|
124
|
+
* (register.ts); it's admin-rotatable. */
|
|
125
|
+
readonly treasury: AddressLike;
|
|
126
|
+
/** The `["handle", name]` PDA being claimed — must not already exist. */
|
|
127
|
+
readonly handle: AddressLike;
|
|
128
|
+
/** The `["commitment", commitmentHash]` PDA the matching `commit`
|
|
129
|
+
* created. */
|
|
130
|
+
readonly commitment: AddressLike;
|
|
131
|
+
/** Canonical form (`parseName(...).canonical`), 1..=32 bytes. */
|
|
132
|
+
readonly name: string;
|
|
133
|
+
readonly handleType: HandleTypeValue;
|
|
134
|
+
/** The exact 32-byte secret the commitment hashed. */
|
|
135
|
+
readonly secret: Uint8Array;
|
|
136
|
+
/** OPTIONAL revenue-share pair — same contract as `register`'s (#7397):
|
|
137
|
+
* supply BOTH or NEITHER. */
|
|
138
|
+
readonly integrator?: AddressLike;
|
|
139
|
+
/** The `["integrator", integrator]` allowlist PDA. */
|
|
140
|
+
readonly integratorAllowlist?: AddressLike;
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Build `register_revealed` — the reveal step. Price is computed on-chain
|
|
144
|
+
* exactly like `register`'s (see register.ts's module docs); simulate first
|
|
145
|
+
* to show the payer a price. Account order matches
|
|
146
|
+
* `RegisterRevealed<'info>` (lib.rs) exactly: payer, owner, config,
|
|
147
|
+
* treasury, handle, commitment, system_program [, integrator,
|
|
148
|
+
* integrator_allowlist].
|
|
149
|
+
*/
|
|
150
|
+
export declare function buildRegisterRevealedIx(p: RegisterRevealedParams): BuiltInstruction;
|
|
151
|
+
export interface CancelCommitmentParams {
|
|
152
|
+
/** The registry program id. */
|
|
153
|
+
readonly programId: AddressLike;
|
|
154
|
+
/** MUST be the commitment's recorded payer (`NotCommitmentPayer`, 6080,
|
|
155
|
+
* otherwise). Signer; receives the rent. */
|
|
156
|
+
readonly payer: AddressLike;
|
|
157
|
+
/** The commitment account to close. No name/secret needed — half the
|
|
158
|
+
* point of canceling is that the preimage may be lost. */
|
|
159
|
+
readonly commitment: AddressLike;
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Build `cancel_commitment` — reclaim an unconsumed commitment's rent,
|
|
163
|
+
* payer-only, allowed at any age. Account order matches
|
|
164
|
+
* `CancelCommitment<'info>` (lib.rs) exactly: payer, commitment.
|
|
165
|
+
*/
|
|
166
|
+
export declare function buildCancelCommitmentIx(p: CancelCommitmentParams): BuiltInstruction;
|
|
167
|
+
/** Decoded `Commitment` account. The hash itself is never stored — the
|
|
168
|
+
* account's ADDRESS is the hash's proof (it is the PDA seed). */
|
|
169
|
+
export interface Commitment {
|
|
170
|
+
/** Who paid `commit`'s rent — the only key allowed to cancel (base58). */
|
|
171
|
+
readonly payer: string;
|
|
172
|
+
/** Unix seconds when `commit` created this account — what the
|
|
173
|
+
* min/max-age window is checked against. */
|
|
174
|
+
readonly committedAt: bigint;
|
|
175
|
+
readonly bump: number;
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Decode a `Commitment` account's raw data, or null if not one (wrong
|
|
179
|
+
* discriminator or too short).
|
|
180
|
+
*/
|
|
181
|
+
export declare function decodeCommitment(data: Uint8Array): Commitment | null;
|
|
182
|
+
/**
|
|
183
|
+
* Fetch + decode one `Commitment` account, or null when it doesn't exist
|
|
184
|
+
* (never committed, already consumed by a reveal, or canceled — all leave
|
|
185
|
+
* no account) or isn't shaped like a `Commitment`.
|
|
186
|
+
*/
|
|
187
|
+
export declare function fetchCommitment(rpc: RpcFn, commitmentAccount: string): Promise<Commitment | null>;
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Commit-reveal registration (#8134 / #8199): instruction builders and
|
|
3
|
+
* account decoding for `commit` / `register_revealed` / `cancel_commitment`
|
|
4
|
+
* — the anti-front-running path. `register` (register.ts) stays the plain
|
|
5
|
+
* single-step sibling; this module adds the two-step flow: stake out a
|
|
6
|
+
* hash, wait `Config.min_commitment_age_secs`, then reveal.
|
|
7
|
+
*
|
|
8
|
+
* Hand-rolled like the rest of this package — no Anchor client, no
|
|
9
|
+
* `@solana/web3.js` import. PDAs are NOT derived here (see delegate.ts's
|
|
10
|
+
* module docs for why) — derive `["commitment", hash]` (seed constant:
|
|
11
|
+
* {@link COMMITMENT_SEED}) with your runtime's canonical
|
|
12
|
+
* `findProgramAddress`, e.g. web3.js:
|
|
13
|
+
*
|
|
14
|
+
* ```ts
|
|
15
|
+
* PublicKey.findProgramAddressSync(
|
|
16
|
+
* [Buffer.from(COMMITMENT_SEED), Buffer.from(hash)],
|
|
17
|
+
* programId,
|
|
18
|
+
* );
|
|
19
|
+
* ```
|
|
20
|
+
*
|
|
21
|
+
* # The commitment hash
|
|
22
|
+
*
|
|
23
|
+
* `sha256(name_bytes ‖ owner_pubkey ‖ handle_type_byte ‖ secret[32] ‖
|
|
24
|
+
* program_id_bytes)` — the exact preimage order of the program's own
|
|
25
|
+
* `commitment_hash` (lib.rs), byte-verified against the deployed program by
|
|
26
|
+
* the E2E reveal phase (tools/scripts/e2e/journey-audit.ts). sha256, NOT
|
|
27
|
+
* keccak, for the same documented reason as `text_key_hash`: off-chain
|
|
28
|
+
* derivation is plain WebCrypto with no extra dependency — so
|
|
29
|
+
* {@link commitmentHash} is async, mirroring textRecords.ts's
|
|
30
|
+
* `hashTextKey`. The program id in the preimage is the SVM analog of
|
|
31
|
+
* ENS/ArcNS's `chainid + controller` domain separation: a commitment cannot
|
|
32
|
+
* replay against a different deployment.
|
|
33
|
+
*
|
|
34
|
+
* # Lifecycle (mirror of lib.rs)
|
|
35
|
+
*
|
|
36
|
+
* - `commit(commitment_hash)` `init`s `["commitment", hash]`, recording only
|
|
37
|
+
* `payer` + `committed_at` — the name is never on chain until reveal.
|
|
38
|
+
* Anyone may pay (the security is the secret preimage, not who pays).
|
|
39
|
+
* - `register_revealed(name, handle_type, secret)` recomputes the hash
|
|
40
|
+
* on-chain from the now-public fields; the supplied `commitment` account
|
|
41
|
+
* must sit at exactly the PDA that hash derives (`CommitmentMismatch`,
|
|
42
|
+
* 6077, otherwise). Valid only inside the window: at least
|
|
43
|
+
* `min_commitment_age_secs` after `committed_at` (`CommitmentTooNew`,
|
|
44
|
+
* 6074) and strictly before `max_commitment_age_secs` (`CommitmentTooOld`,
|
|
45
|
+
* 6075). Consuming closes the commitment — rent back. Pricing, integrator
|
|
46
|
+
* split and the `Handle` write are identical to `register` (both call the
|
|
47
|
+
* program's `register_core`); the result is a PERMANENT registration.
|
|
48
|
+
* - `cancel_commitment()` closes an unconsumed commitment, payer-only
|
|
49
|
+
* (`NotCommitmentPayer`, 6080), allowed at ANY age — the escape hatch for
|
|
50
|
+
* a lost secret or an abandoned flow, so rent is never stranded.
|
|
51
|
+
*
|
|
52
|
+
* A SECRET THAT IS LOST STRANDS THE COMMITMENT: without it the hash cannot
|
|
53
|
+
* be recomputed, so the commitment can only ever be `cancel_commitment`ed
|
|
54
|
+
* (which at least needs its address, discoverable via
|
|
55
|
+
* `getProgramAccounts` on the payer). Persist the secret client-side the
|
|
56
|
+
* moment it is generated, before `commit` is even sent.
|
|
57
|
+
*/
|
|
58
|
+
import { encodeBase58, decodeBase58_32 } from "./base58.js";
|
|
59
|
+
/** Seed prefix of the commitment PDA: `["commitment", commitmentHash]`. */
|
|
60
|
+
export const COMMITMENT_SEED = "commitment";
|
|
61
|
+
/** Anchor instruction discriminator: `sha256("global:commit")[0..8]`. */
|
|
62
|
+
export const COMMIT_DISCRIMINATOR = Uint8Array.from([
|
|
63
|
+
223, 140, 142, 165, 229, 208, 156, 74,
|
|
64
|
+
]);
|
|
65
|
+
/** Anchor instruction discriminator: `sha256("global:register_revealed")[0..8]`. */
|
|
66
|
+
export const REGISTER_REVEALED_DISCRIMINATOR = Uint8Array.from([
|
|
67
|
+
175, 112, 193, 148, 139, 78, 15, 86,
|
|
68
|
+
]);
|
|
69
|
+
/** Anchor instruction discriminator: `sha256("global:cancel_commitment")[0..8]`. */
|
|
70
|
+
export const CANCEL_COMMITMENT_DISCRIMINATOR = Uint8Array.from([
|
|
71
|
+
36, 39, 70, 137, 71, 179, 88, 232,
|
|
72
|
+
]);
|
|
73
|
+
/** Anchor account discriminator: `sha256("account:Commitment")[0..8]`. */
|
|
74
|
+
export const COMMITMENT_DISCRIMINATOR = Uint8Array.from([
|
|
75
|
+
61, 112, 129, 128, 24, 147, 77, 87,
|
|
76
|
+
]);
|
|
77
|
+
/** `Commitment` account size: disc(8) payer(32) committed_at(8) bump(1). */
|
|
78
|
+
export const COMMITMENT_LEN = 49;
|
|
79
|
+
const SYSTEM_PROGRAM = "11111111111111111111111111111111";
|
|
80
|
+
function toBytes32(v, what) {
|
|
81
|
+
if (typeof v === "string") {
|
|
82
|
+
const b = decodeBase58_32(v);
|
|
83
|
+
if (!b)
|
|
84
|
+
throw new Error(`${what} is not a valid base58 address`);
|
|
85
|
+
return b;
|
|
86
|
+
}
|
|
87
|
+
if (v.length !== 32)
|
|
88
|
+
throw new Error(`${what} must be exactly 32 bytes`);
|
|
89
|
+
return v;
|
|
90
|
+
}
|
|
91
|
+
function toBase58(v, what) {
|
|
92
|
+
return encodeBase58(toBytes32(v, what));
|
|
93
|
+
}
|
|
94
|
+
function checkHandleType(handleType) {
|
|
95
|
+
if (!Number.isInteger(handleType) || handleType < 0 || handleType > 3) {
|
|
96
|
+
throw new Error("handleType must be 0 (Human), 1 (Merchant), 2 (Org) or 3 (Agent)");
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
function checkSecret(secret) {
|
|
100
|
+
if (secret.length !== 32)
|
|
101
|
+
throw new Error("secret must be exactly 32 bytes");
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* The 32-byte commitment hash — `sha256(name ‖ owner ‖ handle_type ‖ secret
|
|
105
|
+
* ‖ program_id)`, the program's own preimage order (see the module docs).
|
|
106
|
+
* WebCrypto (`crypto.subtle`), so it is async — mirrors `hashTextKey`.
|
|
107
|
+
*/
|
|
108
|
+
export async function commitmentHash(p) {
|
|
109
|
+
checkHandleType(p.handleType);
|
|
110
|
+
checkSecret(p.secret);
|
|
111
|
+
const nameBytes = new TextEncoder().encode(p.name);
|
|
112
|
+
if (nameBytes.length < 1 || nameBytes.length > 32) {
|
|
113
|
+
throw new Error("name must be 1..=32 bytes (UTF-8)");
|
|
114
|
+
}
|
|
115
|
+
const owner = toBytes32(p.owner, "owner");
|
|
116
|
+
const program = toBytes32(p.programId, "programId");
|
|
117
|
+
const preimage = new Uint8Array(nameBytes.length + 32 + 1 + 32 + 32);
|
|
118
|
+
let o = 0;
|
|
119
|
+
preimage.set(nameBytes, o);
|
|
120
|
+
o += nameBytes.length;
|
|
121
|
+
preimage.set(owner, o);
|
|
122
|
+
o += 32;
|
|
123
|
+
preimage[o] = p.handleType;
|
|
124
|
+
o += 1;
|
|
125
|
+
preimage.set(p.secret, o);
|
|
126
|
+
o += 32;
|
|
127
|
+
preimage.set(program, o);
|
|
128
|
+
const digest = await crypto.subtle.digest("SHA-256", preimage);
|
|
129
|
+
return new Uint8Array(digest);
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Build `commit` — stake out the hash. Only the hash goes on chain; the
|
|
133
|
+
* name stays private until reveal. Account order matches `Commit<'info>`
|
|
134
|
+
* (lib.rs) exactly: payer, commitment, system_program.
|
|
135
|
+
*/
|
|
136
|
+
export function buildCommitIx(p) {
|
|
137
|
+
if (p.commitmentHash.length !== 32) {
|
|
138
|
+
throw new Error("commitmentHash must be exactly 32 bytes (a sha256 digest)");
|
|
139
|
+
}
|
|
140
|
+
const data = new Uint8Array(8 + 32);
|
|
141
|
+
data.set(COMMIT_DISCRIMINATOR, 0);
|
|
142
|
+
data.set(p.commitmentHash, 8);
|
|
143
|
+
const keys = [
|
|
144
|
+
{ pubkey: toBase58(p.payer, "payer"), isSigner: true, isWritable: true },
|
|
145
|
+
{ pubkey: toBase58(p.commitment, "commitment"), isSigner: false, isWritable: true },
|
|
146
|
+
{ pubkey: SYSTEM_PROGRAM, isSigner: false, isWritable: false },
|
|
147
|
+
];
|
|
148
|
+
return { programId: toBase58(p.programId, "programId"), keys, data };
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Build `register_revealed` — the reveal step. Price is computed on-chain
|
|
152
|
+
* exactly like `register`'s (see register.ts's module docs); simulate first
|
|
153
|
+
* to show the payer a price. Account order matches
|
|
154
|
+
* `RegisterRevealed<'info>` (lib.rs) exactly: payer, owner, config,
|
|
155
|
+
* treasury, handle, commitment, system_program [, integrator,
|
|
156
|
+
* integrator_allowlist].
|
|
157
|
+
*/
|
|
158
|
+
export function buildRegisterRevealedIx(p) {
|
|
159
|
+
checkHandleType(p.handleType);
|
|
160
|
+
checkSecret(p.secret);
|
|
161
|
+
const nameBytes = new TextEncoder().encode(p.name);
|
|
162
|
+
if (nameBytes.length < 1 || nameBytes.length > 32) {
|
|
163
|
+
throw new Error("name must be 1..=32 bytes (UTF-8)");
|
|
164
|
+
}
|
|
165
|
+
const data = new Uint8Array(8 + 4 + nameBytes.length + 1 + 32);
|
|
166
|
+
data.set(REGISTER_REVEALED_DISCRIMINATOR, 0);
|
|
167
|
+
new DataView(data.buffer).setUint32(8, nameBytes.length, true);
|
|
168
|
+
data.set(nameBytes, 12);
|
|
169
|
+
data[12 + nameBytes.length] = p.handleType;
|
|
170
|
+
data.set(p.secret, 12 + nameBytes.length + 1);
|
|
171
|
+
const hasIntegrator = p.integrator !== undefined || p.integratorAllowlist !== undefined;
|
|
172
|
+
if (hasIntegrator && (p.integrator === undefined || p.integratorAllowlist === undefined)) {
|
|
173
|
+
throw new Error("integrator and integratorAllowlist must both be supplied, or neither");
|
|
174
|
+
}
|
|
175
|
+
const keys = [
|
|
176
|
+
{ pubkey: toBase58(p.payer, "payer"), isSigner: true, isWritable: true },
|
|
177
|
+
{ pubkey: toBase58(p.owner, "owner"), isSigner: false, isWritable: false },
|
|
178
|
+
{ pubkey: toBase58(p.config, "config"), isSigner: false, isWritable: true },
|
|
179
|
+
{ pubkey: toBase58(p.treasury, "treasury"), isSigner: false, isWritable: true },
|
|
180
|
+
{ pubkey: toBase58(p.handle, "handle"), isSigner: false, isWritable: true },
|
|
181
|
+
{ pubkey: toBase58(p.commitment, "commitment"), isSigner: false, isWritable: true },
|
|
182
|
+
{ pubkey: SYSTEM_PROGRAM, isSigner: false, isWritable: false },
|
|
183
|
+
];
|
|
184
|
+
if (hasIntegrator) {
|
|
185
|
+
keys.push({ pubkey: toBase58(p.integrator, "integrator"), isSigner: false, isWritable: true }, {
|
|
186
|
+
pubkey: toBase58(p.integratorAllowlist, "integratorAllowlist"),
|
|
187
|
+
isSigner: false,
|
|
188
|
+
isWritable: false,
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
return { programId: toBase58(p.programId, "programId"), keys, data };
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Build `cancel_commitment` — reclaim an unconsumed commitment's rent,
|
|
195
|
+
* payer-only, allowed at any age. Account order matches
|
|
196
|
+
* `CancelCommitment<'info>` (lib.rs) exactly: payer, commitment.
|
|
197
|
+
*/
|
|
198
|
+
export function buildCancelCommitmentIx(p) {
|
|
199
|
+
const data = new Uint8Array(8);
|
|
200
|
+
data.set(CANCEL_COMMITMENT_DISCRIMINATOR, 0);
|
|
201
|
+
return {
|
|
202
|
+
programId: toBase58(p.programId, "programId"),
|
|
203
|
+
keys: [
|
|
204
|
+
{ pubkey: toBase58(p.payer, "payer"), isSigner: true, isWritable: true },
|
|
205
|
+
{ pubkey: toBase58(p.commitment, "commitment"), isSigner: false, isWritable: true },
|
|
206
|
+
],
|
|
207
|
+
data,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Decode a `Commitment` account's raw data, or null if not one (wrong
|
|
212
|
+
* discriminator or too short).
|
|
213
|
+
*/
|
|
214
|
+
export function decodeCommitment(data) {
|
|
215
|
+
if (data.length < COMMITMENT_LEN)
|
|
216
|
+
return null;
|
|
217
|
+
for (let i = 0; i < 8; i++)
|
|
218
|
+
if (data[i] !== COMMITMENT_DISCRIMINATOR[i])
|
|
219
|
+
return null;
|
|
220
|
+
const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
|
|
221
|
+
return {
|
|
222
|
+
payer: encodeBase58(data.slice(8, 40)),
|
|
223
|
+
committedAt: view.getBigInt64(40, true),
|
|
224
|
+
bump: data[48],
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Fetch + decode one `Commitment` account, or null when it doesn't exist
|
|
229
|
+
* (never committed, already consumed by a reveal, or canceled — all leave
|
|
230
|
+
* no account) or isn't shaped like a `Commitment`.
|
|
231
|
+
*/
|
|
232
|
+
export async function fetchCommitment(rpc, commitmentAccount) {
|
|
233
|
+
const res = (await rpc("getAccountInfo", [
|
|
234
|
+
commitmentAccount,
|
|
235
|
+
{ encoding: "base64", commitment: "confirmed" },
|
|
236
|
+
]));
|
|
237
|
+
const data = res?.value?.data?.[0];
|
|
238
|
+
if (!data)
|
|
239
|
+
return null;
|
|
240
|
+
const bin = atob(data);
|
|
241
|
+
const raw = new Uint8Array(bin.length);
|
|
242
|
+
for (let i = 0; i < bin.length; i++)
|
|
243
|
+
raw[i] = bin.charCodeAt(i);
|
|
244
|
+
return decodeCommitment(raw);
|
|
245
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -38,6 +38,14 @@ export * from "./textRecords.js";
|
|
|
38
38
|
export * from "./lock.js";
|
|
39
39
|
export * from "./integrator.js";
|
|
40
40
|
export * from "./voucher.js";
|
|
41
|
+
export * from "./agent.js";
|
|
42
|
+
export * from "./register.js";
|
|
43
|
+
export * from "./accounts.js";
|
|
44
|
+
export * from "./x402.js";
|
|
45
|
+
export * from "./recordWrite.js";
|
|
46
|
+
export * from "./clearRecords.js";
|
|
47
|
+
export * from "./commitReveal.js";
|
|
48
|
+
export * from "./pnftTransfer.js";
|
|
41
49
|
import { type Chain, type Resolved } from "./types.js";
|
|
42
50
|
import { WasmResolver } from "./wasm.js";
|
|
43
51
|
import { type HandleRecord } from "./records.js";
|