@x1id/resolve 0.22.0 → 0.43.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +71 -0
- package/dist/accounts.d.ts +6 -0
- package/dist/accounts.js +6 -1
- package/dist/agent.d.ts +184 -8
- package/dist/agent.js +351 -3
- package/dist/agentPolicy.d.ts +238 -0
- package/dist/agentPolicy.js +364 -0
- package/dist/attestation.d.ts +25 -0
- package/dist/attestation.js +25 -0
- package/dist/checkout.d.ts +216 -0
- package/dist/checkout.js +327 -0
- package/dist/credentials.d.ts +466 -0
- package/dist/credentials.js +669 -0
- package/dist/gate.d.ts +94 -4
- package/dist/gate.js +206 -7
- package/dist/gateClient.d.ts +12 -0
- package/dist/giftLink.d.ts +272 -0
- package/dist/giftLink.js +452 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +33 -0
- package/dist/loginButton.d.ts +69 -0
- package/dist/loginButton.js +207 -0
- package/dist/logo.d.ts +1 -0
- package/dist/logo.js +8 -0
- package/dist/messaging.d.ts +439 -0
- package/dist/messaging.js +979 -0
- package/dist/mintNft.d.ts +247 -0
- package/dist/mintNft.js +328 -0
- package/dist/nsGate.d.ts +98 -0
- package/dist/nsGate.js +172 -0
- package/dist/oidc.d.ts +272 -0
- package/dist/oidc.js +641 -0
- package/dist/pay.d.ts +513 -0
- package/dist/pay.js +756 -0
- package/dist/payment.d.ts +9 -2
- package/dist/payment.js +9 -2
- package/dist/recovery.d.ts +186 -0
- package/dist/recovery.js +276 -0
- package/dist/recoveryNft.d.ts +183 -0
- package/dist/recoveryNft.js +291 -0
- package/dist/shield.d.ts +373 -0
- package/dist/shield.js +695 -0
- package/dist/subscription.d.ts +439 -0
- package/dist/subscription.js +642 -0
- package/dist/textRecords.d.ts +52 -6
- package/dist/textRecords.js +80 -8
- package/dist/types.d.ts +3 -0
- package/dist/x402Receipt.d.ts +246 -0
- package/dist/x402Receipt.js +374 -0
- package/package.json +18 -1
- package/schema/agent-manifest.json +91 -8
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The "Sign in with X1ID" button — framework-free (no React), for the OIDC
|
|
3
|
+
* flow in ./oidc.ts. Two forms:
|
|
4
|
+
*
|
|
5
|
+
* ```ts
|
|
6
|
+
* // 1. A function that returns a <button> (append it anywhere):
|
|
7
|
+
* const client = createX1idOidcClient({ clientId: "x1c_…", redirectUri: "https://app.example/callback" });
|
|
8
|
+
* document.querySelector("#login")!.append(createX1idLoginButton({ client }));
|
|
9
|
+
*
|
|
10
|
+
* // 2. A custom element (register once, then use markup):
|
|
11
|
+
* defineX1idLoginButton();
|
|
12
|
+
* // <x1id-login-button client-id="x1c_…" redirect-uri="https://app.example/callback"></x1id-login-button>
|
|
13
|
+
* ```
|
|
14
|
+
*
|
|
15
|
+
* Nothing touches the DOM at import time (safe to import on a server, and
|
|
16
|
+
* tree-shaken away if unused). Accessible by construction: a native
|
|
17
|
+
* `<button type="button">` (keyboard + focus for free), a visible
|
|
18
|
+
* `:focus-visible` ring, `aria-busy` while the redirect starts, the mark is
|
|
19
|
+
* decorative (`alt=""`), light/dark follow `prefers-color-scheme` unless forced,
|
|
20
|
+
* Windows high-contrast (`forced-colors`) and `prefers-reduced-motion` are
|
|
21
|
+
* honored. Styles go in a constructable stylesheet where supported (not
|
|
22
|
+
* blocked by a strict `style-src` CSP), else one `<style>` element.
|
|
23
|
+
*
|
|
24
|
+
* Brand: the label is "Sign in with X1ID" — X1ID is one word, uppercase. Don't
|
|
25
|
+
* recolor or redraw the mark.
|
|
26
|
+
*/
|
|
27
|
+
import { X1ID_LOGO_DATA_URI } from "./logo.js";
|
|
28
|
+
import { beginX1idLogin, createX1idOidcClient, } from "./oidc.js";
|
|
29
|
+
export const X1ID_LOGIN_LABEL = "Sign in with X1ID";
|
|
30
|
+
export const X1ID_LOGIN_BUTTON_TAG = "x1id-login-button";
|
|
31
|
+
const STYLE_ID = "x1id-login-style";
|
|
32
|
+
export const X1ID_LOGIN_BUTTON_CSS = `
|
|
33
|
+
:host{display:inline-block}
|
|
34
|
+
:host([hidden]){display:none}
|
|
35
|
+
.x1id-login{display:inline-flex;align-items:center;justify-content:center;gap:.6em;
|
|
36
|
+
box-sizing:border-box;min-height:44px;margin:0;padding:.6em 1.1em;
|
|
37
|
+
font:inherit;font-size:1rem;font-weight:600;line-height:1.2;letter-spacing:0;
|
|
38
|
+
cursor:pointer;border-radius:8px;border:1px solid rgba(15,20,32,.16);
|
|
39
|
+
background:#fff;color:#0f1420;-webkit-font-smoothing:antialiased;
|
|
40
|
+
transition:background-color .12s ease,border-color .12s ease}
|
|
41
|
+
.x1id-login:hover{background:#f6f8fb;border-color:rgba(15,20,32,.28)}
|
|
42
|
+
.x1id-login:active{background:#eef1f6}
|
|
43
|
+
.x1id-login:focus-visible{outline:2px solid #0a84ff;outline-offset:2px}
|
|
44
|
+
.x1id-login:disabled{opacity:.55;cursor:default}
|
|
45
|
+
.x1id-login[aria-busy="true"]{cursor:progress}
|
|
46
|
+
.x1id-login-logo{width:20px;height:20px;border-radius:5px;flex:0 0 auto;display:block}
|
|
47
|
+
.x1id-login-label{white-space:nowrap}
|
|
48
|
+
.x1id-login--dark{background:#161b26;color:#f3f6fb;border-color:rgba(255,255,255,.16)}
|
|
49
|
+
.x1id-login--dark:hover{background:#1d2431;border-color:rgba(255,255,255,.28)}
|
|
50
|
+
.x1id-login--dark:active{background:#131822}
|
|
51
|
+
@media (prefers-color-scheme:dark){
|
|
52
|
+
.x1id-login:not(.x1id-login--light){background:#161b26;color:#f3f6fb;border-color:rgba(255,255,255,.16)}
|
|
53
|
+
.x1id-login:not(.x1id-login--light):hover{background:#1d2431;border-color:rgba(255,255,255,.28)}
|
|
54
|
+
.x1id-login:not(.x1id-login--light):active{background:#131822}
|
|
55
|
+
}
|
|
56
|
+
@media (prefers-reduced-motion:reduce){.x1id-login{transition:none}}
|
|
57
|
+
@media (forced-colors:active){.x1id-login{border-color:ButtonText}.x1id-login:focus-visible{outline-color:Highlight}}
|
|
58
|
+
`;
|
|
59
|
+
/** Add the button CSS once to a document or shadow root. */
|
|
60
|
+
function adoptStyles(root, doc) {
|
|
61
|
+
const Sheet = globalThis.CSSStyleSheet;
|
|
62
|
+
if (Sheet && Array.isArray(root.adoptedStyleSheets) && "replaceSync" in Sheet.prototype) {
|
|
63
|
+
const marked = root.adoptedStyleSheets.some((s) => s.__x1idLogin);
|
|
64
|
+
if (marked)
|
|
65
|
+
return;
|
|
66
|
+
const sheet = new Sheet();
|
|
67
|
+
sheet.replaceSync(X1ID_LOGIN_BUTTON_CSS);
|
|
68
|
+
sheet.__x1idLogin = true;
|
|
69
|
+
root.adoptedStyleSheets = [...root.adoptedStyleSheets, sheet];
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
const host = "head" in root ? (root.head ?? root.documentElement) : root;
|
|
73
|
+
if (!host)
|
|
74
|
+
return;
|
|
75
|
+
if ("getElementById" in root && root.getElementById(STYLE_ID))
|
|
76
|
+
return;
|
|
77
|
+
const el = doc.createElement("style");
|
|
78
|
+
el.id = STYLE_ID;
|
|
79
|
+
el.textContent = X1ID_LOGIN_BUTTON_CSS;
|
|
80
|
+
host.appendChild(el);
|
|
81
|
+
}
|
|
82
|
+
function buildButton(doc, opts) {
|
|
83
|
+
const text = opts.label ?? X1ID_LOGIN_LABEL;
|
|
84
|
+
const btn = doc.createElement("button");
|
|
85
|
+
btn.type = "button";
|
|
86
|
+
btn.className = "x1id-login" + (opts.theme === "light" ? " x1id-login--light" : opts.theme === "dark" ? " x1id-login--dark" : "");
|
|
87
|
+
if (opts.disabled)
|
|
88
|
+
btn.disabled = true;
|
|
89
|
+
const logo = doc.createElement("img");
|
|
90
|
+
logo.className = "x1id-login-logo";
|
|
91
|
+
logo.src = X1ID_LOGO_DATA_URI;
|
|
92
|
+
logo.width = 20;
|
|
93
|
+
logo.height = 20;
|
|
94
|
+
logo.alt = ""; // decorative: the label names the button
|
|
95
|
+
logo.setAttribute("aria-hidden", "true");
|
|
96
|
+
const label = doc.createElement("span");
|
|
97
|
+
label.className = "x1id-login-label";
|
|
98
|
+
label.textContent = text;
|
|
99
|
+
btn.append(logo, label);
|
|
100
|
+
btn.addEventListener("click", (ev) => {
|
|
101
|
+
if (btn.disabled || btn.getAttribute("aria-busy") === "true")
|
|
102
|
+
return;
|
|
103
|
+
const run = opts.onClick
|
|
104
|
+
? () => opts.onClick(ev)
|
|
105
|
+
: opts.client
|
|
106
|
+
? () => beginX1idLogin(opts.client, opts.authorization ?? {}).then(() => undefined)
|
|
107
|
+
: null;
|
|
108
|
+
if (!run)
|
|
109
|
+
return;
|
|
110
|
+
btn.setAttribute("aria-busy", "true");
|
|
111
|
+
btn.disabled = true;
|
|
112
|
+
Promise.resolve()
|
|
113
|
+
.then(run)
|
|
114
|
+
.catch((err) => {
|
|
115
|
+
btn.disabled = !!opts.disabled;
|
|
116
|
+
btn.removeAttribute("aria-busy");
|
|
117
|
+
(opts.onError ?? ((e) => console.error("[x1id] Sign in with X1ID failed", e)))(err);
|
|
118
|
+
});
|
|
119
|
+
// On success the page navigates away; the button stays busy until then.
|
|
120
|
+
});
|
|
121
|
+
return btn;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Build a "Sign in with X1ID" `<button>`. Pass `client` (click → redirect to
|
|
125
|
+
* auth.x1id.io) or your own `onClick`. Injects its stylesheet once.
|
|
126
|
+
*/
|
|
127
|
+
export function createX1idLoginButton(opts = {}) {
|
|
128
|
+
const doc = opts.document ?? globalThis.document;
|
|
129
|
+
if (!doc)
|
|
130
|
+
throw new Error("createX1idLoginButton needs a DOM (call it in the browser)");
|
|
131
|
+
adoptStyles(doc, doc);
|
|
132
|
+
return buildButton(doc, opts);
|
|
133
|
+
}
|
|
134
|
+
/** The custom element's attributes → client config (pure; exported for tests). */
|
|
135
|
+
export function loginButtonAttributes(get) {
|
|
136
|
+
const clientId = get("client-id");
|
|
137
|
+
const redirectUri = get("redirect-uri");
|
|
138
|
+
const issuer = get("issuer");
|
|
139
|
+
const scope = get("scope");
|
|
140
|
+
const prompt = get("prompt");
|
|
141
|
+
const t = get("theme");
|
|
142
|
+
const config = clientId && redirectUri ? { clientId, redirectUri, ...(issuer ? { issuer } : {}), ...(scope ? { scope } : {}) } : null;
|
|
143
|
+
return {
|
|
144
|
+
config,
|
|
145
|
+
authorization: prompt ? { prompt } : {},
|
|
146
|
+
theme: t === "light" || t === "dark" ? t : "auto",
|
|
147
|
+
label: get("label") ?? undefined,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Register `<x1id-login-button>` (idempotent). Attributes: `client-id`,
|
|
152
|
+
* `redirect-uri` (required unless you set `el.client`), `issuer`, `scope`,
|
|
153
|
+
* `prompt`, `theme` (auto|light|dark), `label`, `disabled`. A failure to start
|
|
154
|
+
* dispatches a bubbling `x1id-error` CustomEvent (`detail` = the error).
|
|
155
|
+
*/
|
|
156
|
+
export function defineX1idLoginButton(tagName = X1ID_LOGIN_BUTTON_TAG) {
|
|
157
|
+
const g = globalThis;
|
|
158
|
+
if (!g.customElements || !g.HTMLElement)
|
|
159
|
+
throw new Error("defineX1idLoginButton needs a browser with custom elements");
|
|
160
|
+
if (g.customElements.get(tagName))
|
|
161
|
+
return;
|
|
162
|
+
class X1idLoginButtonElement extends g.HTMLElement {
|
|
163
|
+
static get observedAttributes() {
|
|
164
|
+
return ["client-id", "redirect-uri", "issuer", "scope", "prompt", "theme", "label", "disabled"];
|
|
165
|
+
}
|
|
166
|
+
/** Set a client programmatically instead of the attributes. */
|
|
167
|
+
client = null;
|
|
168
|
+
cached = null;
|
|
169
|
+
connectedCallback() {
|
|
170
|
+
this.render();
|
|
171
|
+
}
|
|
172
|
+
attributeChangedCallback() {
|
|
173
|
+
if (this.isConnected)
|
|
174
|
+
this.render();
|
|
175
|
+
}
|
|
176
|
+
resolveClient(config) {
|
|
177
|
+
if (this.client)
|
|
178
|
+
return this.client;
|
|
179
|
+
if (!config)
|
|
180
|
+
throw new Error(`<${tagName}> needs client-id and redirect-uri attributes (or el.client)`);
|
|
181
|
+
const key = JSON.stringify(config);
|
|
182
|
+
if (this.cached?.key !== key)
|
|
183
|
+
this.cached = { key, client: createX1idOidcClient(config) };
|
|
184
|
+
return this.cached.client;
|
|
185
|
+
}
|
|
186
|
+
render() {
|
|
187
|
+
const root = this.shadowRoot ?? this.attachShadow({ mode: "open" });
|
|
188
|
+
const a = loginButtonAttributes((n) => this.getAttribute(n));
|
|
189
|
+
adoptStyles(root, this.ownerDocument);
|
|
190
|
+
const btn = buildButton(this.ownerDocument, {
|
|
191
|
+
theme: a.theme,
|
|
192
|
+
...(a.label !== undefined ? { label: a.label } : {}),
|
|
193
|
+
disabled: this.hasAttribute("disabled"),
|
|
194
|
+
onClick: () => beginX1idLogin(this.resolveClient(a.config), a.authorization).then(() => undefined),
|
|
195
|
+
onError: (err) => {
|
|
196
|
+
this.dispatchEvent(new CustomEvent("x1id-error", { detail: err, bubbles: true, composed: true }));
|
|
197
|
+
},
|
|
198
|
+
});
|
|
199
|
+
btn.setAttribute("part", "button");
|
|
200
|
+
for (const n of Array.from(root.childNodes))
|
|
201
|
+
if (!(n instanceof HTMLStyleElement))
|
|
202
|
+
n.remove();
|
|
203
|
+
root.appendChild(btn);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
g.customElements.define(tagName, X1idLoginButtonElement);
|
|
207
|
+
}
|
package/dist/logo.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const X1ID_LOGO_DATA_URI = "data:image/png;base64,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";
|
package/dist/logo.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// Brand asset — do not edit by hand. Shared by the widget (widget/src/logo.ts
|
|
2
|
+
// re-exports it) and the "Sign in with X1ID" button (src/loginButton.ts).
|
|
3
|
+
// The official X1ID square logo (app/public/branding/x1id-512-square.png),
|
|
4
|
+
// downscaled to 48px and inlined as a data: URI so the production
|
|
5
|
+
// "Sign in with X1ID" button carries our mark with no external request and
|
|
6
|
+
// nothing for a host page's CSP to block (same philosophy as the inlined WASM).
|
|
7
|
+
// Regenerate from that PNG (48px, base64) if the mark ever changes.
|
|
8
|
+
export const X1ID_LOGO_DATA_URI = "data:image/png;base64,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";
|
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* X1ID Messaging — end-to-end encrypted messages between @names
|
|
3
|
+
* (docs/specs/x1id-messaging.md, OpenProject #8873: Msg 1 + Msg 2).
|
|
4
|
+
*
|
|
5
|
+
* What this module gives an integrator:
|
|
6
|
+
*
|
|
7
|
+
* - **Keys from a wallet signature.** {@link messagingKeyDerivationMessage} is
|
|
8
|
+
* the fixed, domain-separated text a wallet signs; {@link deriveMessagingKeyPair}
|
|
9
|
+
* turns that signature into an X25519 keypair (HKDF-SHA256). The user never
|
|
10
|
+
* manages a separate secret: the same wallet signing the same message gives
|
|
11
|
+
* the same key on every device. Same pattern as Shield's key derivation
|
|
12
|
+
* (app/src/lib/shield/keys.ts), including the "sign twice, both must match"
|
|
13
|
+
* determinism check ({@link signaturesMatch}).
|
|
14
|
+
* - **The on-chain key record.** The public key is published by the name's
|
|
15
|
+
* owner as the text record {@link MSG_KEY_TEXT_KEY} (`x1.msg.key`), value
|
|
16
|
+
* `x1msg1;k=<x25519 pubkey, base64url>;w=<inbox wallet>;i=<key index>` —
|
|
17
|
+
* {@link encodeMessagingKeyRecord} / {@link parseMessagingKeyRecord}.
|
|
18
|
+
* Rotation = derive with the next index and overwrite the record.
|
|
19
|
+
* - **The trust rule** ({@link judgeMessagingKey}, pure) and the chain read
|
|
20
|
+
* that applies it ({@link resolveMessagingKey}): a key is honoured only if
|
|
21
|
+
* the record was written AFTER the current holder's ownership epoch and its
|
|
22
|
+
* inbox wallet is the current authority, or a record delegate granted after
|
|
23
|
+
* that epoch. A key left behind by a previous owner is never used.
|
|
24
|
+
* - **Envelopes.** {@link sealMessage} encrypts (X25519 + HKDF-SHA256 +
|
|
25
|
+
* XChaCha20-Poly1305, fresh ephemeral key per recipient, padded to a length
|
|
26
|
+
* bucket) for ONE X1 network ({@link MessagingNetwork}: the network byte is
|
|
27
|
+
* in the authenticated header and the signed digest, so a message can't be
|
|
28
|
+
* replayed across mainnet/testnet); the sender's WALLET signs the envelope digest
|
|
29
|
+
* ({@link envelopeSigningMessage}); {@link attachSignature} completes it;
|
|
30
|
+
* {@link parseEnvelope} / {@link verifyEnvelopeSignature} check it without
|
|
31
|
+
* decrypting (what the relay does); {@link openMessage} verifies and decrypts.
|
|
32
|
+
* - **Optional on-chain proof of sending**, paid by the SENDER:
|
|
33
|
+
* {@link buildMessageProofMemoIx} — an SPL Memo `x1msg1:<digest hex>` signed
|
|
34
|
+
* by the sender's wallet.
|
|
35
|
+
*
|
|
36
|
+
* # Dependencies — why this module is a separate entry point
|
|
37
|
+
*
|
|
38
|
+
* The rest of `@x1id/resolve` is dependency-free. Messaging needs X25519,
|
|
39
|
+
* XChaCha20-Poly1305, HKDF and strict Ed25519 verification, and hand-rolling
|
|
40
|
+
* any of those is not acceptable. It uses the audited, pure-TypeScript @noble
|
|
41
|
+
* libraries (`@noble/curves`, `@noble/ciphers`, `@noble/hashes`; @solana/web3.js
|
|
42
|
+
* itself depends on curves + hashes, and the X1ID app already ships curves).
|
|
43
|
+
* It is published as the `@x1id/resolve/messaging` subpath and is NOT
|
|
44
|
+
* re-exported from the package root, so importing the resolver never loads
|
|
45
|
+
* them.
|
|
46
|
+
*
|
|
47
|
+
* Nothing here talks to the relay or signs anything: wallet signatures come in
|
|
48
|
+
* as bytes, and the relay is plain HTTP (spec §6).
|
|
49
|
+
*/
|
|
50
|
+
import { type ParsedHandle } from "./accounts.js";
|
|
51
|
+
import type { AddressLike, BuiltInstruction } from "./delegate.js";
|
|
52
|
+
import type { PdaFinder } from "./namespacePricing.js";
|
|
53
|
+
import type { WasmResolver } from "./wasm.js";
|
|
54
|
+
/** The text-record key a name publishes its messaging key under. */
|
|
55
|
+
export declare const MSG_KEY_TEXT_KEY = "x1.msg.key";
|
|
56
|
+
/** Version tag that starts every v1 key-record value. */
|
|
57
|
+
export declare const MSG_KEY_RECORD_VERSION = "x1msg1";
|
|
58
|
+
/** Highest key index accepted (rotation counter). */
|
|
59
|
+
export declare const MSG_MAX_KEY_INDEX = 65535;
|
|
60
|
+
/** The X1 network an envelope is bound to. */
|
|
61
|
+
export type MessagingNetwork = "mainnet" | "testnet";
|
|
62
|
+
/** The header's network byte (offset 4) per network. 0 (the pre-network
|
|
63
|
+
* "reserved flags" value) and anything else are refused. */
|
|
64
|
+
export declare const MSG_NETWORK_BYTE: Readonly<Record<MessagingNetwork, number>>;
|
|
65
|
+
/** Is `v` a {@link MessagingNetwork}? */
|
|
66
|
+
export declare function isMessagingNetwork(v: unknown): v is MessagingNetwork;
|
|
67
|
+
/** Envelope magic + version: ASCII "X1M" then 0x01. */
|
|
68
|
+
export declare const MSG_ENVELOPE_MAGIC: Uint8Array;
|
|
69
|
+
/** Plaintext is padded up to the smallest of these sizes that fits, so the
|
|
70
|
+
* ciphertext reveals only which bucket the message falls in. */
|
|
71
|
+
export declare const MSG_PAD_BUCKETS: readonly number[];
|
|
72
|
+
/** Longest message text, UTF-8 bytes. */
|
|
73
|
+
export declare const MSG_MAX_TEXT_BYTES: number;
|
|
74
|
+
/** Content type byte: UTF-8 plain text (the only one in v1). */
|
|
75
|
+
export declare const MSG_CONTENT_TEXT = 1;
|
|
76
|
+
/** Largest valid envelope, bytes (two stanzas, largest bucket, tx trailer). */
|
|
77
|
+
export declare const MSG_MAX_ENVELOPE_BYTES: number;
|
|
78
|
+
/** SPL Memo v2 — the program the optional proof of sending rides on. */
|
|
79
|
+
export declare const MSG_MEMO_PROGRAM_ID = "MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr";
|
|
80
|
+
/** Prefix of the proof-of-sending memo: `x1msg1:<64 hex digest>`. */
|
|
81
|
+
export declare const MSG_PROOF_MEMO_PREFIX = "x1msg1:";
|
|
82
|
+
/** A typed failure from this module. `code` is stable; `message` is for people. */
|
|
83
|
+
export declare class MessagingError extends Error {
|
|
84
|
+
readonly code: "bad-input" | "malformed" | "bad-signature" | "not-for-this-key" | "decrypt-failed" | "wrong-network" | "too-long";
|
|
85
|
+
constructor(code: MessagingError["code"], message: string);
|
|
86
|
+
}
|
|
87
|
+
/** Lowercase hex. */
|
|
88
|
+
export declare function toHex(b: Uint8Array): string;
|
|
89
|
+
/** RFC 4648 §5 base64url, no padding. */
|
|
90
|
+
export declare function base64urlEncode(b: Uint8Array): string;
|
|
91
|
+
/** Strict base64url decode (no padding, canonical trailing bits), or null. */
|
|
92
|
+
export declare function base64urlDecode(s: string): Uint8Array | null;
|
|
93
|
+
/** Random bytes source — WebCrypto by default; injectable for test vectors. */
|
|
94
|
+
export type RandomBytes = (n: number) => Uint8Array;
|
|
95
|
+
/**
|
|
96
|
+
* The exact text a wallet signs to derive its messaging key. Fixed and
|
|
97
|
+
* domain-separated (it names X1ID Messaging, the wallet and the key index, so
|
|
98
|
+
* a signature made for anything else — a sign-in, another app, another index —
|
|
99
|
+
* never yields this key). Plain printable ASCII + newlines so every wallet
|
|
100
|
+
* shows it as text.
|
|
101
|
+
*
|
|
102
|
+
* The signature IS the secret: anyone who obtains it can derive the key and
|
|
103
|
+
* read the wallet's messages. The text says so, and apps must never send or
|
|
104
|
+
* store it (the X1ID app keeps it in memory only, like Shield).
|
|
105
|
+
*/
|
|
106
|
+
export declare function messagingKeyDerivationMessage(wallet: AddressLike, index?: number): Uint8Array;
|
|
107
|
+
/** A messaging keypair. `secretKey` is the X25519 private scalar — keep it in
|
|
108
|
+
* memory only. */
|
|
109
|
+
export interface MessagingKeyPair {
|
|
110
|
+
readonly wallet: string;
|
|
111
|
+
readonly index: number;
|
|
112
|
+
readonly publicKey: Uint8Array;
|
|
113
|
+
readonly secretKey: Uint8Array;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Derive the X25519 keypair from the wallet's 64-byte signature over
|
|
117
|
+
* {@link messagingKeyDerivationMessage}`(wallet, index)`:
|
|
118
|
+
*
|
|
119
|
+
* secret = HKDF-SHA256(ikm = signature, salt = "x1id-msg/v1/key-derivation",
|
|
120
|
+
* info = "x25519|<wallet>|<index>", L = 32)
|
|
121
|
+
*
|
|
122
|
+
* (X25519 clamps the scalar itself.) Only deterministic wallets give a stable
|
|
123
|
+
* key — have the wallet sign twice on set-up and require
|
|
124
|
+
* {@link signaturesMatch}, exactly as Shield does.
|
|
125
|
+
*/
|
|
126
|
+
export declare function deriveMessagingKeyPair(signature: Uint8Array, wallet: AddressLike, index?: number): MessagingKeyPair;
|
|
127
|
+
/** Constant-time equality of two signatures — the determinism check. */
|
|
128
|
+
export declare function signaturesMatch(a: Uint8Array, b: Uint8Array): boolean;
|
|
129
|
+
/** A parsed `x1.msg.key` record value. */
|
|
130
|
+
export interface MessagingKeyRecord {
|
|
131
|
+
readonly version: 1;
|
|
132
|
+
/** X25519 public key. */
|
|
133
|
+
readonly publicKey: Uint8Array;
|
|
134
|
+
/** The inbox wallet: the wallet whose signature derived this key, and the
|
|
135
|
+
* address the relay delivers to. */
|
|
136
|
+
readonly wallet: string;
|
|
137
|
+
/** Derivation index (rotation counter). */
|
|
138
|
+
readonly index: number;
|
|
139
|
+
}
|
|
140
|
+
/** The record value, ASCII: `x1msg1;k=<base64url pubkey>;w=<wallet>;i=<index>`. */
|
|
141
|
+
export declare function encodeMessagingKeyRecord(r: {
|
|
142
|
+
publicKey: Uint8Array;
|
|
143
|
+
wallet: AddressLike;
|
|
144
|
+
index: number;
|
|
145
|
+
}): string;
|
|
146
|
+
/** {@link encodeMessagingKeyRecord} as the raw bytes a text-record ix takes. */
|
|
147
|
+
export declare function encodeMessagingKeyRecordBytes(r: {
|
|
148
|
+
publicKey: Uint8Array;
|
|
149
|
+
wallet: AddressLike;
|
|
150
|
+
index: number;
|
|
151
|
+
}): Uint8Array;
|
|
152
|
+
/**
|
|
153
|
+
* Parse a record value strictly: exactly the v1 shape, canonical base64url, a
|
|
154
|
+
* real 32-byte wallet, a non-zero key. Anything else (another version, extra
|
|
155
|
+
* fields, padding, whitespace) is null — a reader must then treat the name as
|
|
156
|
+
* having no usable key.
|
|
157
|
+
*/
|
|
158
|
+
export declare function parseMessagingKeyRecord(value: string | Uint8Array): MessagingKeyRecord | null;
|
|
159
|
+
/** Everything the trust rule needs, read from chain by the caller. */
|
|
160
|
+
export interface MessagingKeyTrustInput {
|
|
161
|
+
/** The `x1.msg.key` TextRecord, or null when the account does not exist. */
|
|
162
|
+
readonly record: {
|
|
163
|
+
readonly value: Uint8Array;
|
|
164
|
+
readonly updatedAt: bigint;
|
|
165
|
+
} | null;
|
|
166
|
+
/** The handle's epochs (from `parseHandleAccount`). */
|
|
167
|
+
readonly handle: Pick<ParsedHandle, "registeredAt" | "recordsClearedAt" | "holderEpoch">;
|
|
168
|
+
/** The name's CURRENT authority: `Handle.owner`, or the NFT holder when
|
|
169
|
+
* tokenized. Base58. */
|
|
170
|
+
readonly authority: string;
|
|
171
|
+
/** The `["delegate", handle]` RecordDelegate, or null. */
|
|
172
|
+
readonly delegate: {
|
|
173
|
+
readonly delegate: string;
|
|
174
|
+
readonly delegatedAt: bigint;
|
|
175
|
+
} | null;
|
|
176
|
+
}
|
|
177
|
+
export type MessagingKeyVerdict = {
|
|
178
|
+
readonly ok: true;
|
|
179
|
+
readonly key: MessagingKeyRecord;
|
|
180
|
+
/** Why the inbox wallet may speak for the name. */
|
|
181
|
+
readonly basis: "authority" | "delegate";
|
|
182
|
+
} | {
|
|
183
|
+
readonly ok: false;
|
|
184
|
+
readonly reason: "no-record" | "malformed" | "stale" | "wallet-not-authorized" | "delegate-stale";
|
|
185
|
+
/** The parsed key when it parsed (for an owner surface: "replace this"). */
|
|
186
|
+
readonly key?: MessagingKeyRecord;
|
|
187
|
+
};
|
|
188
|
+
/**
|
|
189
|
+
* The ownership epoch a messaging key must post-date:
|
|
190
|
+
* `max(registered_at, holder_epoch)` — the last native register/sale/transfer
|
|
191
|
+
* or native pNFT sale (docs/record-trust.md, attestation.ts).
|
|
192
|
+
*/
|
|
193
|
+
export declare function messagingOwnershipEpoch(h: Pick<ParsedHandle, "registeredAt" | "holderEpoch">): bigint;
|
|
194
|
+
/**
|
|
195
|
+
* THE trust rule (spec §3). A name's messaging key is honoured only when ALL of:
|
|
196
|
+
*
|
|
197
|
+
* 1. the record exists and parses as a v1 key;
|
|
198
|
+
* 2. `updated_at >= max(registered_at, holder_epoch)` — it was written in the
|
|
199
|
+
* CURRENT holder's ownership epoch — and `updated_at >= records_cleared_at`;
|
|
200
|
+
* 3. its inbox wallet `w` is the name's current authority (owner / NFT holder),
|
|
201
|
+
* OR the current record delegate whose `delegated_at >= max(registered_at,
|
|
202
|
+
* holder_epoch)` (the holder authorised that delegate after their epoch).
|
|
203
|
+
*
|
|
204
|
+
* Rule 3 also catches what the epochs cannot: a bearer-NFT trade on an
|
|
205
|
+
* outside marketplace bumps no epoch, but the old holder's key names the old
|
|
206
|
+
* holder's wallet, which is no longer the authority, so it stops counting.
|
|
207
|
+
* Pure; no I/O.
|
|
208
|
+
*/
|
|
209
|
+
export declare function judgeMessagingKey(input: MessagingKeyTrustInput): MessagingKeyVerdict;
|
|
210
|
+
/** Config for {@link resolveMessagingKey} — the shape Shield's recipient
|
|
211
|
+
* resolver takes. */
|
|
212
|
+
export interface MessagingResolveConfig {
|
|
213
|
+
readonly rpcUrl: string;
|
|
214
|
+
readonly handleProgramId: AddressLike;
|
|
215
|
+
readonly wasm: WasmResolver;
|
|
216
|
+
/** Canonical `find_program_address` (never hand-rolled here). */
|
|
217
|
+
readonly find: PdaFinder;
|
|
218
|
+
readonly fetchImpl?: typeof fetch;
|
|
219
|
+
}
|
|
220
|
+
/** What {@link resolveMessagingKey} found. */
|
|
221
|
+
export interface ResolvedMessagingKey {
|
|
222
|
+
/** Canonical name (no `@`). */
|
|
223
|
+
readonly name: string;
|
|
224
|
+
readonly handleAccount: string;
|
|
225
|
+
/** The name's current authority (owner / NFT holder). */
|
|
226
|
+
readonly authority: string;
|
|
227
|
+
/** The `["text", handle, sha256("x1.msg.key")]` account. */
|
|
228
|
+
readonly recordAccount: string;
|
|
229
|
+
/** Whether that account exists at all (stale or not) — a set-up flow
|
|
230
|
+
* UPDATES an existing account (re-stamping it into the new epoch) and
|
|
231
|
+
* CREATES a missing one. */
|
|
232
|
+
readonly recordExists: boolean;
|
|
233
|
+
readonly verdict: MessagingKeyVerdict;
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Read `@name`'s messaging key from chain and judge it with
|
|
237
|
+
* {@link judgeMessagingKey}. Bare `@handles` only (like Shield). Throws a
|
|
238
|
+
* `ResolveError` for an invalid or unregistered name; a name without a usable
|
|
239
|
+
* key is a normal result (`verdict.ok === false`), not an error.
|
|
240
|
+
*/
|
|
241
|
+
export declare function resolveMessagingKey(name: string, config: MessagingResolveConfig): Promise<ResolvedMessagingKey>;
|
|
242
|
+
/** The padded size for `textBytes` bytes of text. */
|
|
243
|
+
export declare function paddedLength(textBytes: number): number;
|
|
244
|
+
/** One recipient stanza: which key it is for, the ephemeral key, the wrapped
|
|
245
|
+
* content key. */
|
|
246
|
+
export interface EnvelopeStanza {
|
|
247
|
+
readonly key: Uint8Array;
|
|
248
|
+
readonly ephemeral: Uint8Array;
|
|
249
|
+
readonly wrapped: Uint8Array;
|
|
250
|
+
}
|
|
251
|
+
/** A structurally valid envelope (nothing decrypted). */
|
|
252
|
+
export interface ParsedEnvelope {
|
|
253
|
+
/** The X1 network the sender sealed it for (authenticated + signed). */
|
|
254
|
+
readonly network: MessagingNetwork;
|
|
255
|
+
readonly sender: string;
|
|
256
|
+
readonly recipient: string;
|
|
257
|
+
readonly createdAtMs: number;
|
|
258
|
+
/** 16 random bytes, hex. */
|
|
259
|
+
readonly messageId: string;
|
|
260
|
+
readonly stanzas: readonly EnvelopeStanza[];
|
|
261
|
+
/** The signed bytes (everything but the trailing signature). */
|
|
262
|
+
readonly body: Uint8Array;
|
|
263
|
+
/** sha256(body) — the value the sender signs and the proof memo carries. */
|
|
264
|
+
readonly digest: Uint8Array;
|
|
265
|
+
/** How the sender's wallet signed (trailer kind byte). */
|
|
266
|
+
readonly signatureKind: "message" | "transaction";
|
|
267
|
+
readonly signature: Uint8Array;
|
|
268
|
+
/** `transaction` kind only: the signed transaction MESSAGE bytes. */
|
|
269
|
+
readonly signedTxMessage: Uint8Array | null;
|
|
270
|
+
/** Total length, bytes. */
|
|
271
|
+
readonly size: number;
|
|
272
|
+
}
|
|
273
|
+
/** An envelope awaiting the sender's wallet signature. */
|
|
274
|
+
export interface UnsignedEnvelope {
|
|
275
|
+
readonly body: Uint8Array;
|
|
276
|
+
readonly digest: Uint8Array;
|
|
277
|
+
/** Exactly what the sender's wallet must `signMessage`. */
|
|
278
|
+
readonly signingMessage: Uint8Array;
|
|
279
|
+
readonly messageId: string;
|
|
280
|
+
}
|
|
281
|
+
export interface SealParams {
|
|
282
|
+
/** The X1 network this message is for. Bound into the header (AEAD
|
|
283
|
+
* associated data and the signed digest) and the wallet's signing text, so
|
|
284
|
+
* a relay or reader on the other network refuses it. */
|
|
285
|
+
readonly network: MessagingNetwork;
|
|
286
|
+
/** The sender's wallet (signs the envelope). */
|
|
287
|
+
readonly sender: AddressLike;
|
|
288
|
+
/** The recipient's inbox wallet — the key record's `w`. */
|
|
289
|
+
readonly recipient: AddressLike;
|
|
290
|
+
/** The recipient's X25519 key — the key record's `k`. */
|
|
291
|
+
readonly recipientKey: Uint8Array;
|
|
292
|
+
/** The sender's own X25519 public key: adds a second stanza so the sender
|
|
293
|
+
* can re-read what they sent. Omit for recipient-only. */
|
|
294
|
+
readonly senderKey?: Uint8Array;
|
|
295
|
+
readonly text: string;
|
|
296
|
+
/** Default: now. */
|
|
297
|
+
readonly createdAtMs?: number;
|
|
298
|
+
/** Test seam for vectors. Default: WebCrypto. */
|
|
299
|
+
readonly random?: RandomBytes;
|
|
300
|
+
}
|
|
301
|
+
/**
|
|
302
|
+
* The text the sender's wallet signs for an envelope: the X1 network and the
|
|
303
|
+
* envelope's sha256 fingerprint under a fixed, domain-separated heading
|
|
304
|
+
* (printable ASCII). The digest already covers the network byte; naming it in
|
|
305
|
+
* the text also shows the user which network they are sending on.
|
|
306
|
+
*/
|
|
307
|
+
export declare function envelopeSigningMessage(digest: Uint8Array, network: MessagingNetwork): Uint8Array;
|
|
308
|
+
/**
|
|
309
|
+
* Encrypt `text` to the recipient (and optionally the sender's own key).
|
|
310
|
+
*
|
|
311
|
+
* Per message: a random 32-byte content key encrypts the padded text with
|
|
312
|
+
* XChaCha20-Poly1305 (random 24-byte nonce; AAD = every header byte before
|
|
313
|
+
* the ciphertext, so network, sender, recipient, time, id and stanzas are all
|
|
314
|
+
* authenticated). Per stanza: a FRESH ephemeral X25519 key, ECDH with the
|
|
315
|
+
* stanza's key, HKDF-SHA256 (salt = ephemeral ‖ key) → wrap key + nonce, and
|
|
316
|
+
* the content key sealed under them (AAD = the fixed header ‖ the stanza key).
|
|
317
|
+
*
|
|
318
|
+
* Returns the unsigned envelope: have the sender's wallet sign
|
|
319
|
+
* `signingMessage`, then {@link attachSignature}.
|
|
320
|
+
*/
|
|
321
|
+
export declare function sealMessage(p: SealParams): UnsignedEnvelope;
|
|
322
|
+
/**
|
|
323
|
+
* Complete an envelope with the sender wallet's `signMessage` signature over
|
|
324
|
+
* `signingMessage` (raw, or SIMD-0048-wrapped — both verify). Checks it
|
|
325
|
+
* before returning, so a wallet that signed the wrong thing (or whose message
|
|
326
|
+
* signatures don't verify on X1) fails HERE, not at the recipient — fall back
|
|
327
|
+
* to {@link envelopeSigningTransaction} + {@link attachTransactionSignature}.
|
|
328
|
+
*/
|
|
329
|
+
export declare function attachSignature(unsigned: UnsignedEnvelope, signature: Uint8Array): Uint8Array;
|
|
330
|
+
/**
|
|
331
|
+
* Complete an envelope with a TRANSACTION signature: for wallets that can sign
|
|
332
|
+
* transactions but not (verifiable) messages — the same reason X1ID sign-in
|
|
333
|
+
* has `/v1/auth/verify-tx`. `signedTx` is the wire transaction the wallet
|
|
334
|
+
* returned from signing {@link envelopeSigningTransaction} (never broadcast).
|
|
335
|
+
* Only the message bytes and the fee payer's signature are kept.
|
|
336
|
+
*/
|
|
337
|
+
export declare function attachTransactionSignature(unsigned: UnsignedEnvelope, signedTx: Uint8Array): Uint8Array;
|
|
338
|
+
/** Structural parse — the relay's check. Null for anything malformed. */
|
|
339
|
+
export declare function parseEnvelope(env: Uint8Array): ParsedEnvelope | null;
|
|
340
|
+
/** The memo a transaction-mode sender signs: `x1msg1-sig:<digest hex>`. */
|
|
341
|
+
export declare function envelopeSigningMemo(digest: Uint8Array): string;
|
|
342
|
+
/**
|
|
343
|
+
* The unsigned wire transaction a transaction-mode sender's wallet signs (and
|
|
344
|
+
* must NOT broadcast): a legacy message, fee payer = sender, ONE SPL Memo
|
|
345
|
+
* instruction carrying {@link envelopeSigningMemo}. Its "recent blockhash" is
|
|
346
|
+
* the envelope digest itself — never a real blockhash, so the signed artifact
|
|
347
|
+
* can never land on chain and cost the sender a fee, whoever holds it.
|
|
348
|
+
*/
|
|
349
|
+
export declare function envelopeSigningTransaction(sender: AddressLike, digest: Uint8Array): Uint8Array;
|
|
350
|
+
/** Split a signed wire transaction into its message and the FIRST (fee
|
|
351
|
+
* payer's) signature, or null. */
|
|
352
|
+
export declare function splitSignedTransaction(tx: Uint8Array): {
|
|
353
|
+
message: Uint8Array;
|
|
354
|
+
signature: Uint8Array;
|
|
355
|
+
} | null;
|
|
356
|
+
/**
|
|
357
|
+
* Strictly check a transaction MESSAGE as a signature over `digest` by
|
|
358
|
+
* `sender`, and verify the signature. Accepted: legacy or v0 without address
|
|
359
|
+
* lookups; exactly one required signer, the fee payer = `sender`; exactly one
|
|
360
|
+
* SPL Memo instruction whose data is {@link envelopeSigningMemo}`(digest)`;
|
|
361
|
+
* nothing else except ComputeBudget instructions (some wallets add a priority
|
|
362
|
+
* fee before signing). Any blockhash (it is never broadcast).
|
|
363
|
+
*/
|
|
364
|
+
export declare function verifyTransactionSignature(sender: AddressLike, digest: Uint8Array, message: Uint8Array, signature: Uint8Array): boolean;
|
|
365
|
+
/**
|
|
366
|
+
* The Solana off-chain message envelope (SIMD-0048, v0) some wallets wrap a
|
|
367
|
+
* `signMessage` payload in before signing: 0xff "solana offchain", version 0,
|
|
368
|
+
* a zero application domain, `format`, one signer, u16 length, the message.
|
|
369
|
+
* Same bytes as the X1ID app's unlock check.
|
|
370
|
+
*/
|
|
371
|
+
export declare function solanaOffchainEnvelope(message: Uint8Array, signer: Uint8Array, format: 0 | 1 | 2): Uint8Array;
|
|
372
|
+
export type WalletSignatureMode = "raw" | "offchain-v0-fmt0" | "offchain-v0-fmt1" | "offchain-v0-fmt2";
|
|
373
|
+
/** How an envelope's sender signature verified: a message mode, or
|
|
374
|
+
* `transaction` (a signed, never-broadcast memo transaction). */
|
|
375
|
+
export type EnvelopeSignatureMode = WalletSignatureMode | "transaction";
|
|
376
|
+
/**
|
|
377
|
+
* Verify a wallet's ed25519 signature over `message` — over the raw bytes, or
|
|
378
|
+
* over the SIMD-0048 envelope some wallets sign instead. Strict RFC 8032
|
|
379
|
+
* (non-canonical encodings and small-order keys refused). Returns which form
|
|
380
|
+
* verified, or null.
|
|
381
|
+
*/
|
|
382
|
+
export declare function verifyWalletSignature(wallet: AddressLike, message: Uint8Array, signature: Uint8Array): WalletSignatureMode | null;
|
|
383
|
+
/** Does the envelope's signature verify for its sender? Returns the mode, or
|
|
384
|
+
* null. This is what the relay checks before storing anything. */
|
|
385
|
+
export declare function verifyEnvelopeSignature(p: ParsedEnvelope): EnvelopeSignatureMode | null;
|
|
386
|
+
/**
|
|
387
|
+
* Is the envelope addressed to exactly this (trusted) key record: its
|
|
388
|
+
* recipient wallet is the record's inbox wallet `w` AND its recipient stanza
|
|
389
|
+
* (always the first) is for the record's key `k`? What the relay checks on
|
|
390
|
+
* send against the recipient's on-chain key (spec §6.3). Pure.
|
|
391
|
+
*/
|
|
392
|
+
export declare function envelopeMatchesKey(p: Pick<ParsedEnvelope, "recipient" | "stanzas">, key: Pick<MessagingKeyRecord, "wallet" | "publicKey">): boolean;
|
|
393
|
+
/** A verified, decrypted message. `text` is UNTRUSTED user input: render it
|
|
394
|
+
* as plain text, never as HTML/Markdown, never follow links automatically. */
|
|
395
|
+
export interface OpenedMessage {
|
|
396
|
+
readonly network: MessagingNetwork;
|
|
397
|
+
readonly sender: string;
|
|
398
|
+
readonly recipient: string;
|
|
399
|
+
readonly createdAtMs: number;
|
|
400
|
+
readonly messageId: string;
|
|
401
|
+
readonly text: string;
|
|
402
|
+
/** sha256 of the signed envelope body, hex — the proof-of-sending hash. */
|
|
403
|
+
readonly digest: string;
|
|
404
|
+
readonly signatureMode: EnvelopeSignatureMode;
|
|
405
|
+
/** True when opened with the sender's own key (a "sent" copy). */
|
|
406
|
+
readonly asSender: boolean;
|
|
407
|
+
}
|
|
408
|
+
/** Options for {@link openMessage}. */
|
|
409
|
+
export interface OpenOptions {
|
|
410
|
+
/** The network the reader is on. When set, an envelope sealed for the other
|
|
411
|
+
* network is refused (`wrong-network`) before anything is decrypted. Apps
|
|
412
|
+
* should always pass it. */
|
|
413
|
+
readonly network?: MessagingNetwork;
|
|
414
|
+
}
|
|
415
|
+
/**
|
|
416
|
+
* Verify the sender's signature, then decrypt with `keys` (the reader's
|
|
417
|
+
* keypair; either side of the conversation). Throws {@link MessagingError}:
|
|
418
|
+
* `malformed`, `wrong-network`, `bad-signature`, `not-for-this-key`,
|
|
419
|
+
* `decrypt-failed`.
|
|
420
|
+
*/
|
|
421
|
+
export declare function openMessage(env: Uint8Array, keys: Pick<MessagingKeyPair, "publicKey" | "secretKey">, opts?: OpenOptions): OpenedMessage;
|
|
422
|
+
/** Envelope bytes → the relay's wire string (base64url). */
|
|
423
|
+
export declare function encodeEnvelope(env: Uint8Array): string;
|
|
424
|
+
/** The relay's wire string → envelope bytes, or null. */
|
|
425
|
+
export declare function decodeEnvelope(s: string): Uint8Array | null;
|
|
426
|
+
/** The proof memo for an envelope digest: `x1msg1:<64 hex>` (71 bytes). */
|
|
427
|
+
export declare function messageProofMemo(digest: Uint8Array): string;
|
|
428
|
+
/** Parse a proof memo back to its digest, or null. */
|
|
429
|
+
export declare function parseMessageProofMemo(memo: string): Uint8Array | null;
|
|
430
|
+
/**
|
|
431
|
+
* The SPL Memo instruction that records "this wallet sent the message with
|
|
432
|
+
* this digest" on chain. The SENDER signs and pays the fee (X1ID pays nothing
|
|
433
|
+
* and never sees the transaction). The sender is listed as a signer of the
|
|
434
|
+
* memo itself, so the proof is attributable even if someone else paid.
|
|
435
|
+
*/
|
|
436
|
+
export declare function buildMessageProofMemoIx(p: {
|
|
437
|
+
sender: AddressLike;
|
|
438
|
+
digest: Uint8Array;
|
|
439
|
+
}): BuiltInstruction;
|