@x1id/resolve 0.19.2 → 0.22.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 +83 -1
- package/dist/adminConfig.d.ts +17 -3
- package/dist/adminConfig.js +24 -1
- package/dist/connect.d.ts +20 -8
- package/dist/connect.js +34 -10
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/namespacePricing.d.ts +207 -0
- package/dist/namespacePricing.js +378 -0
- package/dist/sha256.d.ts +10 -0
- package/dist/sha256.js +77 -0
- package/dist/siws.d.ts +310 -0
- package/dist/siws.js +607 -0
- package/dist/voucher.d.ts +27 -9
- package/dist/voucher.js +37 -16
- package/dist/x402.d.ts +70 -6
- package/dist/x402.js +158 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -605,8 +605,10 @@ const requirements = buildX402Requirements({
|
|
|
605
605
|
buyer: buyerAddress, // x1id v1 is self-sponsored: buyer pays their own fee, no facilitator
|
|
606
606
|
network: X1_MAINNET_NETWORK, // defaults to mainnet; a server on another cluster
|
|
607
607
|
// should pass x402NetworkForGenesis(await getGenesisHash)
|
|
608
|
-
|
|
608
|
+
reference: invoiceReference, // a unique base58 reference per invoice (the payment carries it)
|
|
609
609
|
});
|
|
610
|
+
// Buyer side for api.x1id.io: sign x402RedeemMessage(reference) with the paying
|
|
611
|
+
// wallet and send it as payload.payerSig next to payload.signature.
|
|
610
612
|
```
|
|
611
613
|
|
|
612
614
|
This package only builds the requirements object — it does not build, sign,
|
|
@@ -620,6 +622,86 @@ of this zero-dependency package). Use the MCP server's `prepare_x402_payment`
|
|
|
620
622
|
> `confirmed` gets a `402 "payment not yet confirmed; finalize it and retry"`.
|
|
621
623
|
> Poll the signature to `finalized` before the retry.
|
|
622
624
|
|
|
625
|
+
## Sign in with Solana
|
|
626
|
+
|
|
627
|
+
Sign a user in with any [Wallet Standard](https://github.com/wallet-standard/wallet-standard)
|
|
628
|
+
wallet that implements `solana:signIn` (SIWS), against the hosted X1ID Connect
|
|
629
|
+
API. You get back a Bearer session proving **wallet control**; the wallet's
|
|
630
|
+
primary `@handle`, if it has one, comes along as metadata.
|
|
631
|
+
|
|
632
|
+
```ts
|
|
633
|
+
import { signInWithSolana, SiwsError } from "@x1id/resolve";
|
|
634
|
+
|
|
635
|
+
try {
|
|
636
|
+
const session = await signInWithSolana({
|
|
637
|
+
wallet, // a Wallet Standard wallet object (e.g. from getWallets().get())
|
|
638
|
+
apiKey: "x1id_…", // your integrator key from developers.x1id.io
|
|
639
|
+
// domain defaults to location.host. It must be on your deployment's SIWS allowlist.
|
|
640
|
+
statement: "Sign in to Example.",
|
|
641
|
+
});
|
|
642
|
+
// session.token (send as `Authorization: Bearer …`), session.address, session.handle
|
|
643
|
+
} catch (e) {
|
|
644
|
+
if (e instanceof SiwsError) {
|
|
645
|
+
// e.code: "wallet-unsupported" | "user-rejected" | "domain-not-allowed"
|
|
646
|
+
// | "nonce-expired" | "invalid-params" | "wallet-timeout" | "wallet-error" | "network" | "api-error"
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
```
|
|
650
|
+
|
|
651
|
+
### With fallbacks: `signInWithWallet`
|
|
652
|
+
|
|
653
|
+
Some wallets lack `solana:signIn` or implement it badly, and a Ledger refuses
|
|
654
|
+
message signing. `signInWithWallet` takes the same arguments and tries, in the
|
|
655
|
+
developers.x1id.io order:
|
|
656
|
+
|
|
657
|
+
1. SIWS (`solana:signIn`);
|
|
658
|
+
2. a signed, **never-sent** memo transaction (`solana:signTransaction`) whose
|
|
659
|
+
only instruction is an SPL-Memo carrying the server's
|
|
660
|
+
`x1id-verify:<audience>:<nonce>`;
|
|
661
|
+
3. `solana:signMessage` over the server's
|
|
662
|
+
`x1id-connect:v2:wallet-control:<audience>:<address>:<nonce>`.
|
|
663
|
+
|
|
664
|
+
```ts
|
|
665
|
+
import { signInWithWallet, X1ID_MAINNET_SIGNIN_FEE_PAYER } from "@x1id/resolve";
|
|
666
|
+
|
|
667
|
+
const { method, ...session } = await signInWithWallet({
|
|
668
|
+
wallet,
|
|
669
|
+
apiKey: "x1id_…",
|
|
670
|
+
rpcUrl: "https://rpc.mainnet.x1.xyz", // optional: recent blockhash for the memo tx
|
|
671
|
+
signinFeePayer: X1ID_MAINNET_SIGNIN_FEE_PAYER, // optional: no-fee layout (api.x1id.io mainnet)
|
|
672
|
+
});
|
|
673
|
+
```
|
|
674
|
+
|
|
675
|
+
A user rejection or a wallet timeout stops the chain, so the user is never
|
|
676
|
+
prompted twice after saying no. The audience (your key id) is part of every
|
|
677
|
+
signed string, and the SDK signs the server's strings verbatim. It refuses to
|
|
678
|
+
sign a memo or challenge that is not bound to an audience. Without
|
|
679
|
+
`signinFeePayer` the user is the memo transaction's fee payer, which every
|
|
680
|
+
deployment accepts. The transaction is never sent either way. Outside a
|
|
681
|
+
browser, pass `domain` or set `methods: ["memo-tx", "sign-message"]`.
|
|
682
|
+
|
|
683
|
+
The steps are also exported on their own, for wallets you drive yourself:
|
|
684
|
+
|
|
685
|
+
| function | does |
|
|
686
|
+
|---|---|
|
|
687
|
+
| `requestSignInChallenge({ apiKey, wallet, apiBase? })` | `POST /v1/auth/challenge {wallet, mode:"signin"}` → `{ nonce, expires_at, … }` |
|
|
688
|
+
| `buildSignInInput({ domain?, address, nonce, statement?, uri?, issuedAt? })` | the `solana:signIn` input: domain, address, statement, uri, version `"1"`, nonce, issuedAt |
|
|
689
|
+
| `verifySolanaSignIn({ apiKey, nonce, signedMessage, signature, apiBase? })` | `POST /v1/auth/verify-signin` (signed bytes as base64, signature as base58) → session |
|
|
690
|
+
| `buildSignInMessageText(input)` | the SIWS message text a wallet renders from that input |
|
|
691
|
+
| `requestConnectChallenge({ apiKey, wallet, mode? })` | challenge for `"signin"`, `"tx"` (adds `memo`), or signMessage (no mode) |
|
|
692
|
+
| `buildSignInMemoTransaction({ user, memo, blockhash?, feePayer? })` | the unsigned memo-only legacy transaction (no web3.js) |
|
|
693
|
+
| `verifySignInTransaction({ apiKey, nonce, signedTransaction })` | `POST /v1/auth/verify-tx` |
|
|
694
|
+
| `verifySignInMessage({ apiKey, nonce, signature, signer })` | `POST /v1/auth/verify` |
|
|
695
|
+
|
|
696
|
+
The server checks that line 1 is `<domain> wants you to sign in with your Solana account:`
|
|
697
|
+
with an allowed domain, that line 2 is the challenged wallet, that a `Nonce:` line
|
|
698
|
+
carries the single-use challenge nonce, and the ed25519 signature (raw bytes or
|
|
699
|
+
the SIMD-0048 off-chain envelope). A nonce is single-use and expires, so on
|
|
700
|
+
`nonce-expired` call `signInWithSolana` again.
|
|
701
|
+
|
|
702
|
+
This is the hosted-session flow. For a server-less proof your own backend
|
|
703
|
+
verifies, see `signInWithX1ID` in [SIGNIN.md](./SIGNIN.md).
|
|
704
|
+
|
|
623
705
|
## Errors
|
|
624
706
|
|
|
625
707
|
`ResolveError` carries a `code` a UI can branch on:
|
package/dist/adminConfig.d.ts
CHANGED
|
@@ -44,6 +44,18 @@ export declare const SET_LEASE_GRACE_PERIOD_DISCRIMINATOR: Uint8Array;
|
|
|
44
44
|
export declare const SET_RECOVERY_TIMELOCK_DISCRIMINATOR: Uint8Array;
|
|
45
45
|
export declare const SET_MIN_COMMITMENT_AGE_DISCRIMINATOR: Uint8Array;
|
|
46
46
|
export declare const SET_MAX_COMMITMENT_AGE_DISCRIMINATOR: Uint8Array;
|
|
47
|
+
/** `sha256("global:set_namespace_creation_fee")[0..8]` — sets
|
|
48
|
+
* `Config.namespace_creation_fee_lamports` (u64, the permissionless
|
|
49
|
+
* `create_namespace` TLD creation fee paid to treasury). */
|
|
50
|
+
export declare const SET_NAMESPACE_CREATION_FEE_DISCRIMINATOR: Uint8Array;
|
|
51
|
+
/** Byte offset of `Config.namespace_creation_fee_lamports` (u64 LE) in the
|
|
52
|
+
* Config account data, including the 8-byte Anchor discriminator — mirrors
|
|
53
|
+
* `Config::NAMESPACE_CREATION_FEE_LAMPORTS_OFFSET` (state.rs). */
|
|
54
|
+
export declare const NAMESPACE_CREATION_FEE_LAMPORTS_OFFSET = 436;
|
|
55
|
+
/** Decode `Config.namespace_creation_fee_lamports` from raw Config account
|
|
56
|
+
* data. Returns null for a Config too short to carry the field (pre-v6
|
|
57
|
+
* layout) — never a guessed default. */
|
|
58
|
+
export declare function decodeNamespaceCreationFeeLamports(configData: Uint8Array): bigint | null;
|
|
47
59
|
/** Map an admin config action name → its discriminator (for a relay executor
|
|
48
60
|
* that dispatches on the action string). set_admin/set_attestor intentionally
|
|
49
61
|
* absent — those are owner-gated, never dispatched here. */
|
|
@@ -54,10 +66,12 @@ export interface AdminScalarParams {
|
|
|
54
66
|
readonly config: AddressLike;
|
|
55
67
|
/** The discriminator for the specific setter (from ADMIN_SCALAR_DISCRIMINATORS). */
|
|
56
68
|
readonly discriminator: Uint8Array;
|
|
57
|
-
/** The integer arg (u64/i64 encoding is identical for a non-negative value).
|
|
58
|
-
|
|
69
|
+
/** The integer arg (u64/i64 encoding is identical for a non-negative value).
|
|
70
|
+
* A string must be plain decimal digits (lamports amounts beyond JS's safe
|
|
71
|
+
* integer range arrive from the admin queue as decimal strings). */
|
|
72
|
+
readonly value: number | bigint | string;
|
|
59
73
|
}
|
|
60
74
|
/** Build any one-integer-arg AdminOnly setter (lease rate divisor, lease grace,
|
|
61
|
-
* recovery timelock, min/max commitment age). disc(8) + value as little-endian
|
|
75
|
+
* recovery timelock, min/max commitment age, namespace creation fee). disc(8) + value as little-endian
|
|
62
76
|
* u64(8). The value must be a non-negative integer that fits u64. */
|
|
63
77
|
export declare function buildAdminScalarSetterIx(p: AdminScalarParams): BuiltInstruction;
|
package/dist/adminConfig.js
CHANGED
|
@@ -84,6 +84,22 @@ export const SET_LEASE_GRACE_PERIOD_DISCRIMINATOR = Uint8Array.from([212, 2, 110
|
|
|
84
84
|
export const SET_RECOVERY_TIMELOCK_DISCRIMINATOR = Uint8Array.from([178, 183, 148, 243, 155, 166, 79, 6]);
|
|
85
85
|
export const SET_MIN_COMMITMENT_AGE_DISCRIMINATOR = Uint8Array.from([181, 20, 114, 43, 51, 23, 47, 248]);
|
|
86
86
|
export const SET_MAX_COMMITMENT_AGE_DISCRIMINATOR = Uint8Array.from([163, 106, 196, 206, 146, 156, 79, 249]);
|
|
87
|
+
/** `sha256("global:set_namespace_creation_fee")[0..8]` — sets
|
|
88
|
+
* `Config.namespace_creation_fee_lamports` (u64, the permissionless
|
|
89
|
+
* `create_namespace` TLD creation fee paid to treasury). */
|
|
90
|
+
export const SET_NAMESPACE_CREATION_FEE_DISCRIMINATOR = Uint8Array.from([158, 152, 239, 141, 143, 5, 75, 198]);
|
|
91
|
+
/** Byte offset of `Config.namespace_creation_fee_lamports` (u64 LE) in the
|
|
92
|
+
* Config account data, including the 8-byte Anchor discriminator — mirrors
|
|
93
|
+
* `Config::NAMESPACE_CREATION_FEE_LAMPORTS_OFFSET` (state.rs). */
|
|
94
|
+
export const NAMESPACE_CREATION_FEE_LAMPORTS_OFFSET = 436;
|
|
95
|
+
/** Decode `Config.namespace_creation_fee_lamports` from raw Config account
|
|
96
|
+
* data. Returns null for a Config too short to carry the field (pre-v6
|
|
97
|
+
* layout) — never a guessed default. */
|
|
98
|
+
export function decodeNamespaceCreationFeeLamports(configData) {
|
|
99
|
+
if (configData.length < NAMESPACE_CREATION_FEE_LAMPORTS_OFFSET + 8)
|
|
100
|
+
return null;
|
|
101
|
+
return new DataView(configData.buffer, configData.byteOffset, configData.byteLength).getBigUint64(NAMESPACE_CREATION_FEE_LAMPORTS_OFFSET, true);
|
|
102
|
+
}
|
|
87
103
|
/** Map an admin config action name → its discriminator (for a relay executor
|
|
88
104
|
* that dispatches on the action string). set_admin/set_attestor intentionally
|
|
89
105
|
* absent — those are owner-gated, never dispatched here. */
|
|
@@ -93,11 +109,18 @@ export const ADMIN_SCALAR_DISCRIMINATORS = {
|
|
|
93
109
|
set_recovery_timelock: SET_RECOVERY_TIMELOCK_DISCRIMINATOR,
|
|
94
110
|
set_min_commitment_age: SET_MIN_COMMITMENT_AGE_DISCRIMINATOR,
|
|
95
111
|
set_max_commitment_age: SET_MAX_COMMITMENT_AGE_DISCRIMINATOR,
|
|
112
|
+
set_namespace_creation_fee: SET_NAMESPACE_CREATION_FEE_DISCRIMINATOR,
|
|
96
113
|
};
|
|
97
114
|
/** Build any one-integer-arg AdminOnly setter (lease rate divisor, lease grace,
|
|
98
|
-
* recovery timelock, min/max commitment age). disc(8) + value as little-endian
|
|
115
|
+
* recovery timelock, min/max commitment age, namespace creation fee). disc(8) + value as little-endian
|
|
99
116
|
* u64(8). The value must be a non-negative integer that fits u64. */
|
|
100
117
|
export function buildAdminScalarSetterIx(p) {
|
|
118
|
+
if (typeof p.value === "string" && !/^\d{1,20}$/.test(p.value)) {
|
|
119
|
+
throw new Error(`value ${JSON.stringify(p.value)} is not a non-negative decimal integer`);
|
|
120
|
+
}
|
|
121
|
+
if (typeof p.value === "number" && !Number.isSafeInteger(p.value)) {
|
|
122
|
+
throw new Error(`value ${p.value} is not a safe integer`);
|
|
123
|
+
}
|
|
101
124
|
const raw = typeof p.value === "bigint" ? p.value : BigInt(p.value);
|
|
102
125
|
if (raw < 0n || raw > 0xffffffffffffffffn) {
|
|
103
126
|
throw new Error(`value ${p.value} is out of u64 range`);
|
package/dist/connect.d.ts
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
*
|
|
14
14
|
* So this flow proves wallet control a different, robust way: the wallet signs a
|
|
15
15
|
* throwaway MEMO TRANSACTION — one SPL-Memo instruction carrying
|
|
16
|
-
* `x1id-verify:<nonce>`, fee-paid by the wallet, never broadcast. The hosted
|
|
16
|
+
* `x1id-verify:<audience>:<nonce>`, fee-paid by the wallet, never broadcast. The hosted
|
|
17
17
|
* `POST /v1/auth/verify-tx` endpoint deserializes the signed artifact, asserts
|
|
18
18
|
* it is EXACTLY that memo (no disguised transfer/approval), that the fee payer
|
|
19
19
|
* is the wallet, that the memo binds our single-use nonce, and verifies the
|
|
@@ -33,13 +33,19 @@
|
|
|
33
33
|
* free of a hard web3 dependency and tests can supply a fake.
|
|
34
34
|
*/
|
|
35
35
|
/** Memo prefix the verify-tx proof binds to. The signed memo instruction's
|
|
36
|
-
* UTF-8 data MUST be EXACTLY `x1id-verify:<nonce>` — the server
|
|
37
|
-
* other data, which is what kills replay of an unrelated signed
|
|
36
|
+
* UTF-8 data MUST be EXACTLY `x1id-verify:<audience>:<nonce>` — the server
|
|
37
|
+
* rejects any other data, which is what kills replay of an unrelated signed
|
|
38
|
+
* transaction. The AUDIENCE (your API key id, or developers.x1id.io for the
|
|
39
|
+
* Developer Portal) is returned by the challenge as `audience` and binds the
|
|
40
|
+
* signature to the application it was made for (R3-L2P-1); the hosted API no
|
|
41
|
+
* longer accepts the unbound `x1id-verify:<nonce>`. */
|
|
38
42
|
export declare const VERIFY_TX_MEMO_PREFIX: "x1id-verify:";
|
|
39
43
|
/** Canonical SPL Memo program id — the program the challenge memo rides on. */
|
|
40
44
|
export declare const MEMO_PROGRAM_ID: "MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr";
|
|
41
|
-
/** The exact memo string for a
|
|
42
|
-
|
|
45
|
+
/** The exact memo string for a challenge nonce and its audience. Without an
|
|
46
|
+
* audience this is the legacy unbound form, which the current hosted API
|
|
47
|
+
* REJECTS — always pass the challenge's `audience` (or use its `memo`). */
|
|
48
|
+
export declare function verifyTxMemo(nonce: string, audience?: string): string;
|
|
43
49
|
/** The slice of a web3.js `PublicKey` used here. */
|
|
44
50
|
export interface PublicKeyInstance {
|
|
45
51
|
toBytes(): Uint8Array;
|
|
@@ -87,10 +93,14 @@ export declare const PLACEHOLDER_BLOCKHASH: "11111111111111111111111111111111";
|
|
|
87
93
|
export interface BuildVerifyTxChallengeParams {
|
|
88
94
|
/** The single-use nonce from the tx-mode challenge. */
|
|
89
95
|
readonly nonce: string;
|
|
90
|
-
/** The memo the server returned (`x1id-verify:<nonce>`)
|
|
91
|
-
*
|
|
96
|
+
/** The memo the server returned (`x1id-verify:<audience>:<nonce>`) —
|
|
97
|
+
* RECOMMENDED: pass it verbatim. When omitted it is derived from `nonce` +
|
|
98
|
+
* `audience`. When supplied it MUST be an `x1id-verify:` memo for THIS
|
|
99
|
+
* nonce (and, if `audience` is given, for that audience), or the build
|
|
92
100
|
* throws (catches a client that paired the wrong nonce/memo). */
|
|
93
101
|
readonly memo?: string;
|
|
102
|
+
/** The challenge's `audience` (R3-L2P-1). */
|
|
103
|
+
readonly audience?: string;
|
|
94
104
|
/** The signing wallet, base58 — becomes the fee payer AND the memo signer. */
|
|
95
105
|
readonly feePayer: string;
|
|
96
106
|
/** A recent blockhash. Defaults to {@link PLACEHOLDER_BLOCKHASH} (the tx is
|
|
@@ -101,7 +111,7 @@ export interface BuildVerifyTxChallengeParams {
|
|
|
101
111
|
* Build the memo-only challenge transaction the wallet signs for verify-tx.
|
|
102
112
|
*
|
|
103
113
|
* The result is a legacy `Transaction` with EXACTLY one instruction: an SPL-Memo
|
|
104
|
-
* instruction whose data is the UTF-8 bytes of `x1id-verify:<nonce>`, with the
|
|
114
|
+
* instruction whose data is the UTF-8 bytes of `x1id-verify:<audience>:<nonce>`, with the
|
|
105
115
|
* fee payer listed as a required signer on that instruction. Nothing else — no
|
|
106
116
|
* transfer, no compute-budget, no durable nonce — so the signed artifact can
|
|
107
117
|
* only ever be a sign-in proof, never a disguised value-moving transaction. The
|
|
@@ -136,6 +146,8 @@ export interface ConnectSession {
|
|
|
136
146
|
export interface TxChallenge {
|
|
137
147
|
readonly nonce: string;
|
|
138
148
|
readonly memo: string;
|
|
149
|
+
/** The audience the memo names (R3-L2P-1); absent from older deployments. */
|
|
150
|
+
readonly audience?: string;
|
|
139
151
|
readonly handle: string | null;
|
|
140
152
|
readonly expires_at: number;
|
|
141
153
|
}
|
package/dist/connect.js
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
*
|
|
14
14
|
* So this flow proves wallet control a different, robust way: the wallet signs a
|
|
15
15
|
* throwaway MEMO TRANSACTION — one SPL-Memo instruction carrying
|
|
16
|
-
* `x1id-verify:<nonce>`, fee-paid by the wallet, never broadcast. The hosted
|
|
16
|
+
* `x1id-verify:<audience>:<nonce>`, fee-paid by the wallet, never broadcast. The hosted
|
|
17
17
|
* `POST /v1/auth/verify-tx` endpoint deserializes the signed artifact, asserts
|
|
18
18
|
* it is EXACTLY that memo (no disguised transfer/approval), that the fee payer
|
|
19
19
|
* is the wallet, that the memo binds our single-use nonce, and verifies the
|
|
@@ -33,14 +33,26 @@
|
|
|
33
33
|
* free of a hard web3 dependency and tests can supply a fake.
|
|
34
34
|
*/
|
|
35
35
|
/** Memo prefix the verify-tx proof binds to. The signed memo instruction's
|
|
36
|
-
* UTF-8 data MUST be EXACTLY `x1id-verify:<nonce>` — the server
|
|
37
|
-
* other data, which is what kills replay of an unrelated signed
|
|
36
|
+
* UTF-8 data MUST be EXACTLY `x1id-verify:<audience>:<nonce>` — the server
|
|
37
|
+
* rejects any other data, which is what kills replay of an unrelated signed
|
|
38
|
+
* transaction. The AUDIENCE (your API key id, or developers.x1id.io for the
|
|
39
|
+
* Developer Portal) is returned by the challenge as `audience` and binds the
|
|
40
|
+
* signature to the application it was made for (R3-L2P-1); the hosted API no
|
|
41
|
+
* longer accepts the unbound `x1id-verify:<nonce>`. */
|
|
38
42
|
export const VERIFY_TX_MEMO_PREFIX = "x1id-verify:";
|
|
39
43
|
/** Canonical SPL Memo program id — the program the challenge memo rides on. */
|
|
40
44
|
export const MEMO_PROGRAM_ID = "MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr";
|
|
41
|
-
/**
|
|
42
|
-
|
|
43
|
-
|
|
45
|
+
/** An audience: a key id (16 base64url chars) or a bare DNS name. */
|
|
46
|
+
const AUDIENCE_RE = /^[A-Za-z0-9._-]{1,253}$/;
|
|
47
|
+
/** The exact memo string for a challenge nonce and its audience. Without an
|
|
48
|
+
* audience this is the legacy unbound form, which the current hosted API
|
|
49
|
+
* REJECTS — always pass the challenge's `audience` (or use its `memo`). */
|
|
50
|
+
export function verifyTxMemo(nonce, audience) {
|
|
51
|
+
if (audience === undefined)
|
|
52
|
+
return VERIFY_TX_MEMO_PREFIX + nonce;
|
|
53
|
+
if (!AUDIENCE_RE.test(audience))
|
|
54
|
+
throw new Error(`invalid audience "${audience}"`);
|
|
55
|
+
return `${VERIFY_TX_MEMO_PREFIX}${audience}:${nonce}`;
|
|
44
56
|
}
|
|
45
57
|
/** 32 zero-bytes in base58 — a structurally valid blockhash. The transaction is
|
|
46
58
|
* NEVER broadcast, so a placeholder blockhash is fine; a wallet that insists on
|
|
@@ -50,7 +62,7 @@ export const PLACEHOLDER_BLOCKHASH = "11111111111111111111111111111111";
|
|
|
50
62
|
* Build the memo-only challenge transaction the wallet signs for verify-tx.
|
|
51
63
|
*
|
|
52
64
|
* The result is a legacy `Transaction` with EXACTLY one instruction: an SPL-Memo
|
|
53
|
-
* instruction whose data is the UTF-8 bytes of `x1id-verify:<nonce>`, with the
|
|
65
|
+
* instruction whose data is the UTF-8 bytes of `x1id-verify:<audience>:<nonce>`, with the
|
|
54
66
|
* fee payer listed as a required signer on that instruction. Nothing else — no
|
|
55
67
|
* transfer, no compute-budget, no durable nonce — so the signed artifact can
|
|
56
68
|
* only ever be a sign-in proof, never a disguised value-moving transaction. The
|
|
@@ -62,9 +74,20 @@ export function buildVerifyTxChallenge(web3, params) {
|
|
|
62
74
|
throw new Error("nonce is required");
|
|
63
75
|
if (typeof feePayer !== "string" || feePayer.length === 0)
|
|
64
76
|
throw new Error("feePayer is required");
|
|
65
|
-
const memo = params.memo ?? verifyTxMemo(nonce);
|
|
66
|
-
if (
|
|
67
|
-
|
|
77
|
+
const memo = params.memo ?? verifyTxMemo(nonce, params.audience);
|
|
78
|
+
if (params.audience !== undefined) {
|
|
79
|
+
const want = verifyTxMemo(nonce, params.audience);
|
|
80
|
+
if (memo !== want)
|
|
81
|
+
throw new Error(`memo must be exactly "${want}" for this nonce`);
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
// No audience given: accept the server's audience-bound memo for this
|
|
85
|
+
// nonce, or the legacy unbound form (older deployments).
|
|
86
|
+
const rest = memo.startsWith(VERIFY_TX_MEMO_PREFIX) ? memo.slice(VERIFY_TX_MEMO_PREFIX.length) : null;
|
|
87
|
+
const aud = rest !== null && rest.endsWith(`:${nonce}`) ? rest.slice(0, -(nonce.length + 1)) : null;
|
|
88
|
+
if (rest !== nonce && !(aud !== null && AUDIENCE_RE.test(aud))) {
|
|
89
|
+
throw new Error(`memo must be "${VERIFY_TX_MEMO_PREFIX}<audience>:${nonce}" for this nonce`);
|
|
90
|
+
}
|
|
68
91
|
}
|
|
69
92
|
const payer = new web3.PublicKey(feePayer);
|
|
70
93
|
const memoProgram = new web3.PublicKey(MEMO_PROGRAM_ID);
|
|
@@ -169,6 +192,7 @@ export async function signinWithTx(cfg, params) {
|
|
|
169
192
|
const tx = buildVerifyTxChallenge(params.web3, {
|
|
170
193
|
nonce: challenge.nonce,
|
|
171
194
|
memo: challenge.memo,
|
|
195
|
+
...(typeof challenge.audience === "string" ? { audience: challenge.audience } : {}),
|
|
172
196
|
feePayer: params.wallet,
|
|
173
197
|
...(params.blockhash !== undefined ? { blockhash: params.blockhash } : {}),
|
|
174
198
|
});
|
package/dist/index.d.ts
CHANGED
|
@@ -55,6 +55,7 @@ export * from "./pnftTransfer.js";
|
|
|
55
55
|
export * from "./payment.js";
|
|
56
56
|
export * from "./signin.js";
|
|
57
57
|
export * from "./connect.js";
|
|
58
|
+
export * from "./siws.js";
|
|
58
59
|
export * from "./domainProof.js";
|
|
59
60
|
export { scopedTldCandidate, namespaceIsActive, makeScopedHandleDeriver, type ScopedCandidate, type ScopedHandleDeriver, } from "./scoped.js";
|
|
60
61
|
import { type Chain, type Resolved } from "./types.js";
|
|
@@ -125,4 +126,5 @@ export interface Resolver {
|
|
|
125
126
|
export declare function createResolver(config: ResolverConfig): Resolver;
|
|
126
127
|
export * from "./adminMarket.js";
|
|
127
128
|
export * from "./namespaceOverride.js";
|
|
129
|
+
export * from "./namespacePricing.js";
|
|
128
130
|
export * from "./universal.js";
|
package/dist/index.js
CHANGED
|
@@ -62,6 +62,7 @@ export * from "./pnftTransfer.js";
|
|
|
62
62
|
export * from "./payment.js";
|
|
63
63
|
export * from "./signin.js";
|
|
64
64
|
export * from "./connect.js";
|
|
65
|
+
export * from "./siws.js";
|
|
65
66
|
export * from "./domainProof.js";
|
|
66
67
|
// Named (not `export *`): `NAMESPACE_MAX_LEN` is already exported by
|
|
67
68
|
// adminMarket.js, and two `export *` sharing a name would make it ambiguous
|
|
@@ -815,6 +816,7 @@ export function createResolver(config) {
|
|
|
815
816
|
}
|
|
816
817
|
export * from "./adminMarket.js";
|
|
817
818
|
export * from "./namespaceOverride.js";
|
|
819
|
+
export * from "./namespacePricing.js";
|
|
818
820
|
// The X1ID Universal Resolver (#8475): cross-namespace resolution (.sol via SNS,
|
|
819
821
|
// .eth via ENS) alongside the native path. Additive — the native resolver above
|
|
820
822
|
// is untouched; `createUniversalResolver` composes it with external adapters.
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TLD creator pricing (docs/specs/tld-creator-pricing.md): the SDK side of a
|
|
3
|
+
* TLD creator's own price table for names under their TLD.
|
|
4
|
+
*
|
|
5
|
+
* - `["ns_pricing", label]` holds a `NamespacePricing` account: a flat lamport
|
|
6
|
+
* price per length band (index = `tier_index(len)`: 0 = 1 char … 8 = 9 chars,
|
|
7
|
+
* 9 = 10+), written ONLY by the namespace creator via `set_namespace_pricing`.
|
|
8
|
+
* - `register_under_namespace_v2` (v1's accounts + the pricing PDA as the
|
|
9
|
+
* first remaining account) charges that band FLAT (no launch ramp) when
|
|
10
|
+
* the account exists, else the legacy root price (`Config.tier_lamports` ×
|
|
11
|
+
* the time/volume ramp), and refuses to charge more than the buyer's
|
|
12
|
+
* `max_price_lamports`.
|
|
13
|
+
* - The legacy `register_under_namespace` (v1) fails closed
|
|
14
|
+
* (`NamespacePricingRequiresV2`, 6117) on any TLD whose creator opted in.
|
|
15
|
+
*
|
|
16
|
+
* {@link quoteScopedName} reproduces the program's price selection exactly, so
|
|
17
|
+
* pass its `lamports` as `maxPriceLamports` to {@link buildRegisterUnderNamespaceV2Ix}.
|
|
18
|
+
*
|
|
19
|
+
* Hand-rolled like the rest of this package: no Anchor client, no
|
|
20
|
+
* `@solana/web3.js` import, no `@types/node`. PDAs are derived through an
|
|
21
|
+
* INJECTED `find_program_address` (see {@link PdaFinder}) because this package
|
|
22
|
+
* never hand-rolls the ed25519 on-curve check.
|
|
23
|
+
*/
|
|
24
|
+
import type { AddressLike, BuiltInstruction } from "./delegate.js";
|
|
25
|
+
import type { RpcFn } from "./accounts.js";
|
|
26
|
+
/** Seed prefix of the `["ns_pricing", label]` PDA. */
|
|
27
|
+
export declare const NAMESPACE_PRICING_SEED = "ns_pricing";
|
|
28
|
+
/** `NAMESPACE_TIER_FLOOR_LAMPORTS`: every creator tier must be >= 1 XNT. */
|
|
29
|
+
export declare const NAMESPACE_TIER_FLOOR_LAMPORTS = 1000000000n;
|
|
30
|
+
/** `NAMESPACE_TIER_MAX_LAMPORTS`: every creator tier must be <= 100,000,000 XNT. */
|
|
31
|
+
export declare const NAMESPACE_TIER_MAX_LAMPORTS: bigint;
|
|
32
|
+
/** Number of length bands (same banding as the root `Config.tier_lamports`). */
|
|
33
|
+
export declare const NAMESPACE_TIER_COUNT = 10;
|
|
34
|
+
/** `sha256("global:set_namespace_pricing")[0..8]`. */
|
|
35
|
+
export declare const SET_NAMESPACE_PRICING_DISCRIMINATOR: Uint8Array;
|
|
36
|
+
/** `sha256("global:register_under_namespace_v2")[0..8]`. */
|
|
37
|
+
export declare const REGISTER_UNDER_NAMESPACE_V2_DISCRIMINATOR: Uint8Array;
|
|
38
|
+
/** `sha256("account:NamespacePricing")[0..8]`. */
|
|
39
|
+
export declare const NAMESPACE_PRICING_ACCOUNT_DISCRIMINATOR: Uint8Array;
|
|
40
|
+
/** `Namespace::PRICING_FLAG_EXT_OFFSET` — the one-byte "creator pricing is on"
|
|
41
|
+
* marker past the Anchor-allocated `Namespace` (8 + INIT_SPACE = 107). */
|
|
42
|
+
export declare const NAMESPACE_PRICING_MARKER_OFFSET = 107;
|
|
43
|
+
/** Program error codes added by this feature (6000 + variant index). */
|
|
44
|
+
export declare const NAMESPACE_PRICING_ERRORS: {
|
|
45
|
+
readonly NamespacePricingMismatch: 6116;
|
|
46
|
+
readonly NamespacePricingRequiresV2: 6117;
|
|
47
|
+
readonly PriceExceedsMax: 6118;
|
|
48
|
+
readonly NamespaceTierBelowFloor: 6119;
|
|
49
|
+
readonly NamespaceTierAboveMax: 6120;
|
|
50
|
+
};
|
|
51
|
+
/** lib.rs `tier_index`: 1 char -> 0 … 10+ -> 9 (0 clamps to 0). */
|
|
52
|
+
export declare function scopedTierIndex(len: number): number;
|
|
53
|
+
/** lib.rs `price_now` (registration price), in bigint like the program's u128. */
|
|
54
|
+
export declare function rootPriceNow(tierLamports: bigint, launchTs: bigint, nowSecs: bigint, totalHandlesSold: bigint): bigint;
|
|
55
|
+
/** Problems with a creator tier table, or null when the program would accept
|
|
56
|
+
* it (exactly `validate_namespace_tiers`). */
|
|
57
|
+
export declare function namespaceTiersProblem(tiers: ReadonlyArray<bigint>): string | null;
|
|
58
|
+
/** `find_program_address` — injected so this package never does curve math
|
|
59
|
+
* itself. With web3.js: `(seeds, pid) => PublicKey.findProgramAddressSync(seeds, new PublicKey(pid))[0].toBase58()`. */
|
|
60
|
+
export type PdaFinder = (seeds: Uint8Array[], programId: string) => Promise<string> | string;
|
|
61
|
+
/** The seeds of `["ns_pricing", label]`. */
|
|
62
|
+
export declare function namespacePricingSeeds(label: string): Uint8Array[];
|
|
63
|
+
/** Derive the canonical `["ns_pricing", label]` PDA. */
|
|
64
|
+
export declare function deriveNamespacePricingPda(label: string, programId: AddressLike, find: PdaFinder): Promise<string>;
|
|
65
|
+
/** Decoded `NamespacePricing` account. */
|
|
66
|
+
export interface NamespacePricing {
|
|
67
|
+
readonly version: number;
|
|
68
|
+
readonly bump: number;
|
|
69
|
+
/** Lamports per band, index = {@link scopedTierIndex}(len). */
|
|
70
|
+
readonly tierLamports: readonly bigint[];
|
|
71
|
+
readonly updatedAt: bigint;
|
|
72
|
+
readonly label: string;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Decode a `NamespacePricing` account's raw data (discriminator included):
|
|
76
|
+
* disc(8) | version(u8) | bump(u8) | tier_lamports([u64;10]) | updated_at(i64)
|
|
77
|
+
* | label(String). Returns null when the bytes are not a `NamespacePricing`.
|
|
78
|
+
*/
|
|
79
|
+
export declare function decodeNamespacePricing(data: Uint8Array): NamespacePricing | null;
|
|
80
|
+
/** Whether a raw `Namespace` account carries the "creator pricing is on"
|
|
81
|
+
* marker (any nonzero byte at offset 107; a shorter account = off). */
|
|
82
|
+
export declare function namespaceHasPricingMarker(namespaceData: Uint8Array): boolean;
|
|
83
|
+
export interface SetNamespacePricingParams {
|
|
84
|
+
readonly programId: AddressLike;
|
|
85
|
+
/** The namespace `creator` (signer, pays rent on first set). */
|
|
86
|
+
readonly creator: AddressLike;
|
|
87
|
+
/** `["namespace", label]`. */
|
|
88
|
+
readonly namespace: AddressLike;
|
|
89
|
+
/** `["ns_pricing", label]` ({@link deriveNamespacePricingPda}). */
|
|
90
|
+
readonly namespacePricing: AddressLike;
|
|
91
|
+
readonly label: string;
|
|
92
|
+
/** 10 lamport prices, index = {@link scopedTierIndex}(len). */
|
|
93
|
+
readonly tierLamports: ReadonlyArray<bigint>;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Build `set_namespace_pricing(label, tier_lamports)`. Accounts IN ORDER
|
|
97
|
+
* (`SetNamespacePricing`): creator(s,w), namespace(w), namespace_pricing(w),
|
|
98
|
+
* system_program. Validates the table client-side with the program's exact
|
|
99
|
+
* bounds so a bad table never reaches a wallet prompt.
|
|
100
|
+
*/
|
|
101
|
+
export declare function buildSetNamespacePricingIx(p: SetNamespacePricingParams): BuiltInstruction;
|
|
102
|
+
export interface RegisterUnderNamespaceV2Params {
|
|
103
|
+
readonly programId: AddressLike;
|
|
104
|
+
/** `["ns_pricing", label]` — ALWAYS the canonical PDA, whether or not the
|
|
105
|
+
* creator has set pricing (the program decides from its contents). */
|
|
106
|
+
readonly namespacePricing: AddressLike;
|
|
107
|
+
readonly payer: AddressLike;
|
|
108
|
+
readonly owner: AddressLike;
|
|
109
|
+
readonly config: AddressLike;
|
|
110
|
+
readonly treasury: AddressLike;
|
|
111
|
+
readonly namespace: AddressLike;
|
|
112
|
+
readonly creatorPayout: AddressLike;
|
|
113
|
+
/** `["handle", [label_len], label, name]`. */
|
|
114
|
+
readonly handle: AddressLike;
|
|
115
|
+
readonly mint: AddressLike;
|
|
116
|
+
readonly ownerTokenAccount: AddressLike;
|
|
117
|
+
readonly metadata: AddressLike;
|
|
118
|
+
readonly masterEdition: AddressLike;
|
|
119
|
+
readonly tokenRecord: AddressLike;
|
|
120
|
+
readonly collectionMint: AddressLike;
|
|
121
|
+
readonly collectionMetadata: AddressLike;
|
|
122
|
+
readonly collectionMasterEdition: AddressLike;
|
|
123
|
+
readonly collectionAuthority: AddressLike;
|
|
124
|
+
/** Canonical sub-name (`alice` in `alice.pepe`). */
|
|
125
|
+
readonly name: string;
|
|
126
|
+
/** 0 Human, 1 Merchant, 2 Org, 3 Agent. */
|
|
127
|
+
readonly handleType: number;
|
|
128
|
+
/** Buyer's ceiling: the tx fails (`PriceExceedsMax`) if the on-chain price
|
|
129
|
+
* is higher. Pass {@link quoteScopedName}'s `lamports`. */
|
|
130
|
+
readonly maxPriceLamports: bigint;
|
|
131
|
+
/** OPTIONAL referral pair — both or neither. */
|
|
132
|
+
readonly integrator?: AddressLike;
|
|
133
|
+
readonly integratorAllowlist?: AddressLike;
|
|
134
|
+
/** OPTIONAL `["ns_minter", label, payer]` (invite-only, non-creator). */
|
|
135
|
+
readonly minterAuthorization?: AddressLike;
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Build `register_under_namespace_v2(name, handle_type, max_price_lamports)`.
|
|
139
|
+
* Accounts IN ORDER: the exact v1 `RegisterUnderNamespace` list (payer(s,w),
|
|
140
|
+
* owner, config(w), treasury(w), namespace, creator_payout(w), handle(w),
|
|
141
|
+
* mint(w), owner_token_account(w), metadata(w), master_edition(w),
|
|
142
|
+
* token_record(w), collection_mint, collection_metadata(w),
|
|
143
|
+
* collection_master_edition, collection_authority, token_metadata_program,
|
|
144
|
+
* token_program, associated_token_program, system_program,
|
|
145
|
+
* sysvar_instructions), then the three optional slots ALWAYS present
|
|
146
|
+
* (integrator, integrator_allowlist, minter_authorization — each the program
|
|
147
|
+
* id when absent, Anchor's "explicitly None"), then `namespace_pricing` as
|
|
148
|
+
* `remaining_accounts[0]`. The program fails closed if it is missing or
|
|
149
|
+
* shifted.
|
|
150
|
+
*/
|
|
151
|
+
export declare function buildRegisterUnderNamespaceV2Ix(p: RegisterUnderNamespaceV2Params): BuiltInstruction;
|
|
152
|
+
/** The program's root pricing inputs, read from `Config`. */
|
|
153
|
+
export interface RootPricingInputs {
|
|
154
|
+
readonly launchTs: bigint;
|
|
155
|
+
readonly tierLamports: readonly bigint[];
|
|
156
|
+
readonly totalHandlesSold: bigint;
|
|
157
|
+
}
|
|
158
|
+
/** Read `launch_ts`, `tier_lamports` and `total_handles_sold` from raw Config bytes. */
|
|
159
|
+
export declare function decodeRootPricingInputs(config: Uint8Array): RootPricingInputs | null;
|
|
160
|
+
/**
|
|
161
|
+
* The price `register_under_namespace_v2` charges, from already-read state —
|
|
162
|
+
* `scoped_registration_price` in lib.rs: the creator's band flat when a table
|
|
163
|
+
* exists, else the root band × the ramp.
|
|
164
|
+
*/
|
|
165
|
+
export declare function scopedRegistrationPrice(pricing: NamespacePricing | null, root: RootPricingInputs, nameLen: number, nowSecs: bigint): bigint;
|
|
166
|
+
export interface ScopedQuote {
|
|
167
|
+
/** Lamports `register_under_namespace_v2` will charge (pass as `maxPriceLamports`). */
|
|
168
|
+
readonly lamports: bigint;
|
|
169
|
+
/** `custom` = the creator's table; `fallback` = root tiers × ramp. */
|
|
170
|
+
readonly source: "custom" | "fallback";
|
|
171
|
+
readonly tierIndex: number;
|
|
172
|
+
/** The canonical `["ns_pricing", label]` PDA (pass to the v2 builder). */
|
|
173
|
+
readonly pricingAccount: string;
|
|
174
|
+
/** The creator's full table when `source === "custom"`. */
|
|
175
|
+
readonly pricing: NamespacePricing | null;
|
|
176
|
+
}
|
|
177
|
+
export interface QuoteScopedNameOptions {
|
|
178
|
+
readonly rpc: RpcFn;
|
|
179
|
+
readonly programId: AddressLike;
|
|
180
|
+
readonly find: PdaFinder;
|
|
181
|
+
/** Unix seconds; defaults to the local clock (the ramp moves per quarter, so
|
|
182
|
+
* seconds of skew never matter for a fresh quote). */
|
|
183
|
+
readonly nowSecs?: bigint;
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Quote `name.label` exactly like the program prices it (pricing-or-fallback).
|
|
187
|
+
* Throws when Config is missing, or when the pricing account exists but is not
|
|
188
|
+
* a valid `NamespacePricing` owned by the program (the program would refuse
|
|
189
|
+
* too), so a caller never shows a price the chain will not honour.
|
|
190
|
+
*/
|
|
191
|
+
export declare function quoteScopedName(label: string, name: string, opts: QuoteScopedNameOptions): Promise<ScopedQuote>;
|
|
192
|
+
/**
|
|
193
|
+
* The instruction to SIMULATE (never send) to learn whether the deployed
|
|
194
|
+
* program has this feature: `set_namespace_pricing` with no accounts. An
|
|
195
|
+
* upgraded program recognizes the discriminator and fails account validation
|
|
196
|
+
* (e.g. 3005 `AccountNotEnoughKeys`); a pre-upgrade program fails with
|
|
197
|
+
* Anchor's `InstructionFallbackNotFound` (101). Interpret the simulation's
|
|
198
|
+
* `err` with {@link interpretNamespacePricingProbe}.
|
|
199
|
+
*/
|
|
200
|
+
export declare function buildNamespacePricingProbeIx(programId: AddressLike): BuiltInstruction;
|
|
201
|
+
/**
|
|
202
|
+
* Read a `simulateTransaction` result's `value.err` for the probe tx (whose
|
|
203
|
+
* probe instruction is at `probeIndex`). `true` = the program knows the
|
|
204
|
+
* instruction; `false` = it does not, or the result is not the shape we
|
|
205
|
+
* expect (fail closed: hide the feature rather than show a broken button).
|
|
206
|
+
*/
|
|
207
|
+
export declare function interpretNamespacePricingProbe(err: unknown, probeIndex: number): boolean;
|