@x1id/resolve 0.17.0 → 0.19.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 CHANGED
@@ -415,8 +415,11 @@ holds keys. Every discriminator and account order matches the deployed program.
415
415
  ```ts
416
416
  import { fetchRecords, liveRecords, valueToAddress } from "@x1id/resolve";
417
417
 
418
- // handleAccount = the ["handle", name] PDA; registeredAt from the handle account
419
- const records = await fetchRecords(rpc, programId, handleAccount, registeredAt);
418
+ // handleAccount = the ["handle", name] PDA; registeredAt + recordsClearedAt
419
+ // both from the handle account (recordsClearedAt gates the #8139 stale-record
420
+ // rule — see fetchRecords' signature; a tokenized handle also passes its
421
+ // holderEpoch to fetchAttestations).
422
+ const records = await fetchRecords(rpc, programId, handleAccount, registeredAt, recordsClearedAt);
420
423
  for (const r of liveRecords(records)) {
421
424
  console.log(r.chain, valueToAddress(r.chain, r.value));
422
425
  }
@@ -561,6 +564,11 @@ const createIx = buildCreateRecordIx({
561
564
  programId, payer, owner, handle, record,
562
565
  coinType: 60, // ETH, per SLIP-44
563
566
  value: ethAddressBytes,
567
+ // TOKENIZED handle (the default)? pass the holder's NFT associated token
568
+ // account too, or create_record fails NotCurrentAuthority (6018). The verify
569
+ // builders below take the same `holderTokenAccount`. Omit for an untokenized
570
+ // handle. (A delegated writer passes `recordDelegate` instead.)
571
+ holderTokenAccount, // = getAssociatedTokenAddressSync(nftMint, owner)
564
572
  });
565
573
 
566
574
  // 2. build the challenge and have the wallet sign it (personal_sign)
package/dist/index.js CHANGED
@@ -234,7 +234,7 @@ export function createResolver(config) {
234
234
  // behind by a previous registration of the same name is never resolved,
235
235
  // and a stale `verified: true` is never surfaced.
236
236
  if (chain !== "X1" && chain !== "SOL") {
237
- const live = liveRecords(await fetchRecords(rpc, programBase58, pda, handle.registeredAt, handle.recordsClearedAt));
237
+ const live = liveRecords(await fetchRecords(rpc, programBase58, pda, handle.registeredAt, handle.recordsClearedAt, handle.holderEpoch));
238
238
  const record = live.find((r) => r.coinType === CHAIN_COIN_TYPE[chain]);
239
239
  if (!record) {
240
240
  throw new ResolveError("no-record-for-chain", `@${canonical} has no ${chain} record`, input);
@@ -341,7 +341,7 @@ export function createResolver(config) {
341
341
  if (chain !== "X1" && chain !== "SOL") {
342
342
  // ETH/BTC addresses live in per-chain `Record` accounts — same staleness
343
343
  // rule as a bare handle (fetchRecords → liveRecords).
344
- const live = liveRecords(await fetchRecords(rpc, programBase58, pda, handle.registeredAt, handle.recordsClearedAt));
344
+ const live = liveRecords(await fetchRecords(rpc, programBase58, pda, handle.registeredAt, handle.recordsClearedAt, handle.holderEpoch));
345
345
  const record = live.find((r) => r.coinType === CHAIN_COIN_TYPE[chain]);
346
346
  if (!record) {
347
347
  throw new ResolveError("no-record-for-chain", `${display} has no ${chain} record`, input);
@@ -645,7 +645,7 @@ export function createResolver(config) {
645
645
  throw new ResolveError("unrecognized", `per-chain records exist only for @handles, not .${parsed.namespace} domains`, input);
646
646
  }
647
647
  const { pda, handle } = await fetchHandle(parsed.canonical, input);
648
- return fetchRecords(rpc, programBase58, pda, handle.registeredAt, handle.recordsClearedAt);
648
+ return fetchRecords(rpc, programBase58, pda, handle.registeredAt, handle.recordsClearedAt, handle.holderEpoch);
649
649
  }
650
650
  async function resolve(input, opts) {
651
651
  const chain = opts?.chain ?? "X1";
@@ -83,7 +83,7 @@ export declare function splitBtcSignature(sig65: Uint8Array): {
83
83
  header: number;
84
84
  signature: Uint8Array;
85
85
  };
86
- export interface CreateRecordParams {
86
+ export interface CreateRecordParams extends RecordEditAuthorityParams {
87
87
  readonly programId: AddressLike;
88
88
  readonly payer: AddressLike;
89
89
  readonly owner: AddressLike;
@@ -171,6 +171,12 @@ export function buildCreateRecordIx(p) {
171
171
  { pubkey: toBase58(p.record, "record"), isSigner: false, isWritable: true },
172
172
  { pubkey: SYSTEM_PROGRAM, isSigner: false, isWritable: false },
173
173
  ];
174
+ // W165: create_record uses the same `require_current_authority` check as the
175
+ // verify builders, so a TOKENIZED handle (the default) needs the holder's NFT
176
+ // token account (and/or the record-delegate PDA) in the trailing accounts —
177
+ // without it create_record fails NotCurrentAuthority (6018). This was the one
178
+ // record builder missing the tail; now it matches buildVerifyRecord*Ix.
179
+ pushAuthorityTail(keys, p.programId, p);
174
180
  return { programId: toBase58(p.programId, "programId"), keys, data };
175
181
  }
176
182
  export function buildVerifyRecordEthIx(p) {
package/dist/records.d.ts CHANGED
@@ -95,7 +95,7 @@ export interface HandleRecord {
95
95
  * so a handle-only memcmp scan DOES return them — they are rejected here by
96
96
  * tag and size, not by luck.
97
97
  */
98
- export declare function decodeRecord(raw: Uint8Array, account: string, registeredAt: bigint, recordsClearedAt: bigint): HandleRecord | null;
98
+ export declare function decodeRecord(raw: Uint8Array, account: string, registeredAt: bigint, recordsClearedAt: bigint, holderEpoch?: bigint): HandleRecord | null;
99
99
  /** The records the CURRENT owner actually has — `stale` ones excluded. This
100
100
  * is the list to resolve against, display to a visitor, and count. */
101
101
  export declare function liveRecords(records: readonly HandleRecord[]): HandleRecord[];
@@ -127,4 +127,4 @@ export declare function valueToAddress(chain: Chain | null, value: Uint8Array):
127
127
  * left behind; anything that resolves, displays to a visitor, or counts
128
128
  * takes `liveRecords(...)`.
129
129
  */
130
- export declare function fetchRecords(rpc: RpcFn, programId: string, handleAccount: string, registeredAt: bigint, recordsClearedAt: bigint): Promise<HandleRecord[]>;
130
+ export declare function fetchRecords(rpc: RpcFn, programId: string, handleAccount: string, registeredAt: bigint, recordsClearedAt: bigint, holderEpoch?: bigint): Promise<HandleRecord[]>;
package/dist/records.js CHANGED
@@ -72,7 +72,17 @@ export function chainForCoinType(coinType) {
72
72
  * so a handle-only memcmp scan DOES return them — they are rejected here by
73
73
  * tag and size, not by luck.
74
74
  */
75
- export function decodeRecord(raw, account, registeredAt, recordsClearedAt) {
75
+ export function decodeRecord(raw, account, registeredAt, recordsClearedAt,
76
+ // W122: a record written before the CURRENT holder's epoch belongs to a
77
+ // previous holder and must not resolve. `holder_epoch` (H5 / PR #466) is
78
+ // bumped by a NATIVE tokenized sale (buy_handle) INSTEAD of registered_at
79
+ // (which must not move on a tokenized sale), so without this a transferred
80
+ // tokenized name kept the old holder's verified pay-to records live — a
81
+ // payment-misrouting risk. Mirrors the attestation staleness rule exactly.
82
+ // (Defaults 0n: untokenized names and callers that don't track it are
83
+ // unaffected; an EXTERNAL/plain Metaplex transfer stamps NOTHING, so it is
84
+ // NOT covered here — see the module header / docs caveat.)
85
+ holderEpoch = 0n) {
76
86
  if (raw.length !== RECORD_LEN)
77
87
  return null;
78
88
  for (let i = 0; i < 8; i++) {
@@ -95,7 +105,8 @@ export function decodeRecord(raw, account, registeredAt, recordsClearedAt) {
95
105
  const verified = raw[o] === 1;
96
106
  o += 1;
97
107
  const updatedAt = dv.getBigInt64(o, true);
98
- const stale = updatedAt < registeredAt || updatedAt < recordsClearedAt;
108
+ const epoch = registeredAt > holderEpoch ? registeredAt : holderEpoch;
109
+ const stale = updatedAt < epoch || updatedAt < recordsClearedAt;
99
110
  const chain = chainForCoinType(coinType);
100
111
  return {
101
112
  account,
@@ -164,7 +175,10 @@ function hexToBytes(hex) {
164
175
  * left behind; anything that resolves, displays to a visitor, or counts
165
176
  * takes `liveRecords(...)`.
166
177
  */
167
- export async function fetchRecords(rpc, programId, handleAccount, registeredAt, recordsClearedAt) {
178
+ export async function fetchRecords(rpc, programId, handleAccount, registeredAt, recordsClearedAt,
179
+ // W122 — pass the handle's `holderEpoch` so a native tokenized holder change
180
+ // stales the previous holder's records (see decodeRecord). Defaults 0n.
181
+ holderEpoch = 0n) {
168
182
  const res = (await rpc("getProgramAccounts", [
169
183
  programId,
170
184
  {
@@ -183,7 +197,7 @@ export async function fetchRecords(rpc, programId, handleAccount, registeredAt,
183
197
  const raw = new Uint8Array(bin.length);
184
198
  for (let i = 0; i < bin.length; i++)
185
199
  raw[i] = bin.charCodeAt(i);
186
- const r = decodeRecord(raw, a.pubkey, registeredAt, recordsClearedAt);
200
+ const r = decodeRecord(raw, a.pubkey, registeredAt, recordsClearedAt, holderEpoch);
187
201
  // Defence in depth: the memcmp filter should guarantee the handle match,
188
202
  // but a wrong offset would silently attribute someone else's record to
189
203
  // this handle. A malformed account is skipped, not fatal to the list.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@x1id/resolve",
3
- "version": "0.17.0",
3
+ "version": "0.19.0",
4
4
  "description": "Resolve @handles and X1NS names on X1. Never silently picks between namespaces.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",