@relai-fi/x402 0.5.21 → 0.5.22

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.
@@ -1,7 +1,9 @@
1
1
  "use strict";
2
+ var __create = Object.create;
2
3
  var __defProp = Object.defineProperty;
3
4
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
5
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
5
7
  var __hasOwnProp = Object.prototype.hasOwnProperty;
6
8
  var __export = (target, all) => {
7
9
  for (var name in all)
@@ -15,17 +17,32 @@ var __copyProps = (to, from, except, desc) => {
15
17
  }
16
18
  return to;
17
19
  };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
18
28
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
29
 
20
30
  // src/crossmint.ts
21
31
  var crossmint_exports = {};
22
32
  __export(crossmint_exports, {
33
+ createCrossmintDelegatedX402Fetch: () => createCrossmintDelegatedX402Fetch,
23
34
  createCrossmintX402Fetch: () => createCrossmintX402Fetch
24
35
  });
25
36
  module.exports = __toCommonJS(crossmint_exports);
26
37
  var import_web3 = require("@solana/web3.js");
27
38
  var import_spl_token = require("@solana/spl-token");
39
+ var import_tweetnacl = __toESM(require("tweetnacl"), 1);
28
40
  var DEFAULT_API_BASE = "https://www.crossmint.com/api/2025-06-09";
41
+ var STAGING_API_BASE = "https://staging.crossmint.com/api/2025-06-09";
42
+ function detectApiBase(apiKey) {
43
+ if (apiKey.startsWith("sk_staging")) return STAGING_API_BASE;
44
+ return DEFAULT_API_BASE;
45
+ }
29
46
  function toBase58(bytes) {
30
47
  const ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
31
48
  const digits = [0];
@@ -51,42 +68,135 @@ function toBase58(bytes) {
51
68
  }
52
69
  return str;
53
70
  }
71
+ function fromBase58(str) {
72
+ const ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
73
+ const BASE_MAP = new Uint8Array(256).fill(255);
74
+ for (let i = 0; i < ALPHABET.length; i++) BASE_MAP[ALPHABET.charCodeAt(i)] = i;
75
+ const bytes = [0];
76
+ for (const ch of str) {
77
+ const carry_init = BASE_MAP[ch.charCodeAt(0)];
78
+ if (carry_init === 255) throw new Error(`Invalid base58 character: ${ch}`);
79
+ let carry = carry_init;
80
+ for (let j = 0; j < bytes.length; j++) {
81
+ carry += bytes[j] * 58;
82
+ bytes[j] = carry & 255;
83
+ carry >>= 8;
84
+ }
85
+ while (carry > 0) {
86
+ bytes.push(carry & 255);
87
+ carry >>= 8;
88
+ }
89
+ }
90
+ for (const ch of str) {
91
+ if (ch === ALPHABET[0]) bytes.push(0);
92
+ else break;
93
+ }
94
+ return new Uint8Array(bytes.reverse());
95
+ }
54
96
  function extractSignature(onChainTx) {
55
97
  if (!onChainTx) return null;
56
98
  if (onChainTx.length <= 100) return onChainTx;
57
99
  try {
58
- const ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
59
- const BASE_MAP = new Uint8Array(256).fill(255);
60
- for (let i = 0; i < ALPHABET.length; i++) BASE_MAP[ALPHABET.charCodeAt(i)] = i;
61
- const bytes = [0];
62
- for (const ch of onChainTx) {
63
- const carry_init = BASE_MAP[ch.charCodeAt(0)];
64
- if (carry_init === 255) return null;
65
- let carry = carry_init;
66
- for (let j = 0; j < bytes.length; j++) {
67
- carry += bytes[j] * 58;
68
- bytes[j] = carry & 255;
69
- carry >>= 8;
70
- }
71
- while (carry > 0) {
72
- bytes.push(carry & 255);
73
- carry >>= 8;
74
- }
75
- }
76
- for (const ch of onChainTx) {
77
- if (ch === ALPHABET[0]) bytes.push(0);
78
- else break;
79
- }
80
- const decoded = new Uint8Array(bytes.reverse());
100
+ const decoded = fromBase58(onChainTx);
81
101
  const vtx = import_web3.VersionedTransaction.deserialize(decoded);
82
102
  if (vtx.signatures?.[0]) return toBase58(vtx.signatures[0]);
83
103
  } catch {
84
104
  }
85
105
  return null;
86
106
  }
107
+ async function crossmintApi(baseUrl, apiKey, endpoint, body, method = "POST") {
108
+ const url = `${baseUrl}/wallets/${endpoint}`;
109
+ const opts = {
110
+ method,
111
+ headers: { "Content-Type": "application/json", "X-API-KEY": apiKey }
112
+ };
113
+ if (method === "POST" && body) opts.body = JSON.stringify(body);
114
+ const resp = await fetch(url, opts);
115
+ const json = await resp.json();
116
+ if (!resp.ok) throw new Error(`[x402/crossmint] API ${resp.status}: ${JSON.stringify(json)}`);
117
+ return json;
118
+ }
119
+ function signMessage(message, secretKey) {
120
+ let messageBytes;
121
+ try {
122
+ messageBytes = fromBase58(message);
123
+ } catch {
124
+ messageBytes = new TextEncoder().encode(message);
125
+ }
126
+ const signature = import_tweetnacl.default.sign.detached(messageBytes, secretKey);
127
+ return toBase58(signature);
128
+ }
129
+ function deriveSignerAddress(secretKey) {
130
+ const publicKey = secretKey.length === 64 ? secretKey.slice(32) : import_tweetnacl.default.sign.keyPair.fromSeed(secretKey).publicKey;
131
+ return toBase58(publicKey);
132
+ }
133
+ async function pollForSignature(apiBase, apiKey, wallet, txId, maxPolls, pollMs, initialData) {
134
+ let sig = initialData?.onChain?.txId || extractSignature(initialData?.onChain?.transaction) || null;
135
+ for (let i = 0; i < maxPolls && !sig; i++) {
136
+ await new Promise((r) => setTimeout(r, pollMs));
137
+ const p = await crossmintApi(
138
+ apiBase,
139
+ apiKey,
140
+ `${encodeURIComponent(wallet)}/transactions/${txId}`,
141
+ void 0,
142
+ "GET"
143
+ );
144
+ if (p.status === "failed") {
145
+ throw new Error(`[x402/crossmint] Crossmint tx failed: ${p.error?.message || "unknown"}`);
146
+ }
147
+ sig = p.onChain?.txId || extractSignature(p.onChain?.transaction) || null;
148
+ }
149
+ return sig;
150
+ }
151
+ function buildXPaymentHeader(unsignedTx, sig, accept) {
152
+ const payload = {
153
+ x402Version: 2,
154
+ scheme: "exact",
155
+ network: accept.network,
156
+ payload: {
157
+ transaction: Buffer.from(unsignedTx.serialize()).toString("base64"),
158
+ txId: sig
159
+ },
160
+ accepted: {
161
+ scheme: accept.scheme,
162
+ network: accept.network,
163
+ amount: accept.amount,
164
+ asset: accept.asset,
165
+ payTo: accept.payTo
166
+ }
167
+ };
168
+ return Buffer.from(JSON.stringify(payload)).toString("base64");
169
+ }
170
+ async function buildTransferTx(connection, userPubkey, accept) {
171
+ const mintPubkey = new import_web3.PublicKey(accept.asset);
172
+ const merchantPubkey = new import_web3.PublicKey(accept.payTo);
173
+ const mintAccountInfo = await connection.getAccountInfo(mintPubkey);
174
+ const programId = mintAccountInfo?.owner.equals(import_spl_token.TOKEN_2022_PROGRAM_ID) ? import_spl_token.TOKEN_2022_PROGRAM_ID : import_spl_token.TOKEN_PROGRAM_ID;
175
+ const mintInfo = await (0, import_spl_token.getMint)(connection, mintPubkey, "confirmed", programId);
176
+ const amount = BigInt(accept.amount);
177
+ const sourceAta = await (0, import_spl_token.getAssociatedTokenAddress)(mintPubkey, userPubkey, true, programId);
178
+ const destAta = await (0, import_spl_token.getAssociatedTokenAddress)(mintPubkey, merchantPubkey, true, programId);
179
+ const ix = (0, import_spl_token.createTransferCheckedInstruction)(
180
+ sourceAta,
181
+ mintPubkey,
182
+ destAta,
183
+ userPubkey,
184
+ amount,
185
+ mintInfo.decimals,
186
+ [],
187
+ programId
188
+ );
189
+ const { blockhash } = await connection.getLatestBlockhash("confirmed");
190
+ const msg = new import_web3.TransactionMessage({
191
+ payerKey: userPubkey,
192
+ recentBlockhash: blockhash,
193
+ instructions: [ix]
194
+ }).compileToV0Message();
195
+ return new import_web3.VersionedTransaction(msg);
196
+ }
87
197
  function createCrossmintX402Fetch(config) {
88
198
  const { apiKey, wallet, connection } = config;
89
- const apiBase = config.crossmintApiBase || DEFAULT_API_BASE;
199
+ const apiBase = config.crossmintApiBase || detectApiBase(apiKey);
90
200
  const maxPolls = config.maxPollAttempts ?? 30;
91
201
  const pollMs = config.pollIntervalMs ?? 2e3;
92
202
  const userPubkey = new import_web3.PublicKey(wallet);
@@ -99,77 +209,84 @@ function createCrossmintX402Fetch(config) {
99
209
  const requirements = await firstResp.json();
100
210
  const accept = requirements.accepts?.[0];
101
211
  if (!accept) throw new Error("[x402/crossmint] No payment requirements in 402 response");
102
- const mintPubkey = new import_web3.PublicKey(accept.asset);
103
- const merchantPubkey = new import_web3.PublicKey(accept.payTo);
104
- const mintAccountInfo = await connection.getAccountInfo(mintPubkey);
105
- const programId = mintAccountInfo?.owner.equals(import_spl_token.TOKEN_2022_PROGRAM_ID) ? import_spl_token.TOKEN_2022_PROGRAM_ID : import_spl_token.TOKEN_PROGRAM_ID;
106
- const mintInfo = await (0, import_spl_token.getMint)(connection, mintPubkey, "confirmed", programId);
107
- const amount = BigInt(accept.amount);
108
- const sourceAta = await (0, import_spl_token.getAssociatedTokenAddress)(mintPubkey, userPubkey, true, programId);
109
- const destAta = await (0, import_spl_token.getAssociatedTokenAddress)(mintPubkey, merchantPubkey, true, programId);
110
- const ix = (0, import_spl_token.createTransferCheckedInstruction)(
111
- sourceAta,
112
- mintPubkey,
113
- destAta,
114
- userPubkey,
115
- amount,
116
- mintInfo.decimals,
117
- [],
118
- programId
212
+ const unsignedTx = await buildTransferTx(connection, userPubkey, accept);
213
+ const txData = await crossmintApi(
214
+ apiBase,
215
+ apiKey,
216
+ `${encodeURIComponent(wallet)}/transactions`,
217
+ { params: { transaction: toBase58(unsignedTx.serialize()) } }
119
218
  );
120
- const { blockhash } = await connection.getLatestBlockhash("confirmed");
121
- const msg = new import_web3.TransactionMessage({
122
- payerKey: userPubkey,
123
- recentBlockhash: blockhash,
124
- instructions: [ix]
125
- }).compileToV0Message();
126
- const unsignedTx = new import_web3.VersionedTransaction(msg);
127
- const serializedBase58 = toBase58(unsignedTx.serialize());
128
- const txResp = await fetch(
129
- `${apiBase}/wallets/${encodeURIComponent(wallet)}/transactions`,
219
+ const sig = await pollForSignature(apiBase, apiKey, wallet, txData.id, maxPolls, pollMs, txData);
220
+ if (!sig) throw new Error("[x402/crossmint] Timed out waiting for tx confirmation");
221
+ const xPayment = buildXPaymentHeader(unsignedTx, sig, accept);
222
+ return fetch(url, {
223
+ ...init,
224
+ headers: { Accept: "application/json", "X-PAYMENT": xPayment, ...init?.headers }
225
+ });
226
+ };
227
+ }
228
+ function createCrossmintDelegatedX402Fetch(config) {
229
+ const { apiKey, wallet, connection, signerSecretKey } = config;
230
+ const apiBase = config.crossmintApiBase || detectApiBase(apiKey);
231
+ const maxPolls = config.maxPollAttempts ?? 30;
232
+ const pollMs = config.pollIntervalMs ?? 2e3;
233
+ const userPubkey = new import_web3.PublicKey(wallet);
234
+ const signerAddress = deriveSignerAddress(signerSecretKey);
235
+ return async function fetch402(url, init) {
236
+ const firstResp = await fetch(url, {
237
+ ...init,
238
+ headers: { Accept: "application/json", ...init?.headers }
239
+ });
240
+ if (firstResp.status !== 402) return firstResp;
241
+ const requirements = await firstResp.json();
242
+ const accept = requirements.accepts?.[0];
243
+ if (!accept) throw new Error("[x402/crossmint] No payment requirements in 402 response");
244
+ const unsignedTx = await buildTransferTx(connection, userPubkey, accept);
245
+ const createResp = await crossmintApi(
246
+ apiBase,
247
+ apiKey,
248
+ `${encodeURIComponent(wallet)}/transactions`,
130
249
  {
131
- method: "POST",
132
- headers: { "Content-Type": "application/json", "X-API-KEY": apiKey },
133
- body: JSON.stringify({ params: { transaction: serializedBase58 } })
250
+ params: {
251
+ transaction: toBase58(unsignedTx.serialize()),
252
+ signer: {
253
+ type: "external-wallet",
254
+ locator: `external-wallet:${signerAddress}`
255
+ }
256
+ }
134
257
  }
135
258
  );
136
- const txData = await txResp.json();
137
- if (!txResp.ok || !txData.id) {
138
- throw new Error(`[x402/crossmint] Crossmint API error: ${JSON.stringify(txData)}`);
139
- }
140
- let sig = txData.onChain?.txId || extractSignature(txData.onChain?.transaction) || null;
141
- for (let i = 0; i < maxPolls && !sig; i++) {
142
- await new Promise((r) => setTimeout(r, pollMs));
143
- const poll = await fetch(
144
- `${apiBase}/wallets/${encodeURIComponent(wallet)}/transactions/${txData.id}`,
145
- { headers: { "X-API-KEY": apiKey } }
259
+ const transactionId = createResp.id;
260
+ const pendingApproval = createResp.approvals?.pending?.[0];
261
+ if (!transactionId || !pendingApproval) {
262
+ throw new Error(
263
+ `[x402/crossmint] No pending approval returned. Ensure external signer ${signerAddress} is registered on wallet ${wallet}. Response: ${JSON.stringify(createResp)}`
146
264
  );
147
- const p = await poll.json();
148
- if (p.status === "failed") {
149
- throw new Error(`[x402/crossmint] Crossmint tx failed: ${p.error?.message || "unknown"}`);
150
- }
151
- sig = p.onChain?.txId || extractSignature(p.onChain?.transaction) || null;
152
- }
153
- if (!sig) {
154
- throw new Error("[x402/crossmint] Timed out waiting for Crossmint tx confirmation");
155
265
  }
156
- const payload = {
157
- x402Version: 2,
158
- scheme: "exact",
159
- network: accept.network,
160
- payload: {
161
- transaction: Buffer.from(unsignedTx.serialize()).toString("base64"),
162
- txId: sig
163
- },
164
- accepted: {
165
- scheme: accept.scheme,
166
- network: accept.network,
167
- amount: accept.amount,
168
- asset: accept.asset,
169
- payTo: accept.payTo
266
+ const pendingSigner = pendingApproval.signer?.address ?? pendingApproval.signer?.locator?.split(":")[1] ?? signerAddress;
267
+ const approvalSig = signMessage(pendingApproval.message, signerSecretKey);
268
+ const approvalResp = await crossmintApi(
269
+ apiBase,
270
+ apiKey,
271
+ `${encodeURIComponent(wallet)}/transactions/${encodeURIComponent(transactionId)}/approvals`,
272
+ {
273
+ approvals: [{
274
+ signer: `external-wallet:${pendingSigner}`,
275
+ signature: approvalSig
276
+ }]
170
277
  }
171
- };
172
- const xPayment = Buffer.from(JSON.stringify(payload)).toString("base64");
278
+ );
279
+ const sig = await pollForSignature(
280
+ apiBase,
281
+ apiKey,
282
+ wallet,
283
+ transactionId,
284
+ maxPolls,
285
+ pollMs,
286
+ approvalResp
287
+ );
288
+ if (!sig) throw new Error("[x402/crossmint] Timed out waiting for tx confirmation");
289
+ const xPayment = buildXPaymentHeader(unsignedTx, sig, accept);
173
290
  return fetch(url, {
174
291
  ...init,
175
292
  headers: { Accept: "application/json", "X-PAYMENT": xPayment, ...init?.headers }
@@ -178,6 +295,7 @@ function createCrossmintX402Fetch(config) {
178
295
  }
179
296
  // Annotate the CommonJS export names for ESM import in node:
180
297
  0 && (module.exports = {
298
+ createCrossmintDelegatedX402Fetch,
181
299
  createCrossmintX402Fetch
182
300
  });
183
301
  //# sourceMappingURL=crossmint.cjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/crossmint.ts"],"sourcesContent":["/**\n * Crossmint smart wallet integration for RelAI x402.\n *\n * Auto-handles 402 Payment Required responses using a Crossmint\n * API-key smart wallet on Solana. Zero private keys needed —\n * Crossmint signs and broadcasts, RelAI facilitator verifies on-chain.\n *\n * @example\n * ```ts\n * import { createCrossmintX402Fetch } from \"@relai-fi/x402/crossmint\";\n * import { Connection } from \"@solana/web3.js\";\n *\n * const fetch402 = createCrossmintX402Fetch({\n * apiKey: process.env.CROSSMINT_API_KEY!,\n * wallet: process.env.CROSSMINT_WALLET!,\n * connection: new Connection(process.env.RPC_URL!),\n * });\n *\n * const resp = await fetch402(\"https://api.example.com/protected\");\n * console.log(await resp.json()); // paid content\n * ```\n *\n * @module\n */\nimport {\n Connection,\n PublicKey,\n VersionedTransaction,\n TransactionMessage,\n} from \"@solana/web3.js\";\nimport {\n getAssociatedTokenAddress,\n createTransferCheckedInstruction,\n getMint,\n TOKEN_PROGRAM_ID,\n TOKEN_2022_PROGRAM_ID,\n} from \"@solana/spl-token\";\n\n// ── Types ───────────────────────────────────────────────────────────\n\nexport interface CrossmintX402Config {\n /** Crossmint server-side API key (`sk_production_...` or `sk_staging_...`) */\n apiKey: string;\n\n /** Crossmint smart wallet address (Solana public key) */\n wallet: string;\n\n /** Solana RPC connection */\n connection: Connection;\n\n /** Override Crossmint API base URL (default: `https://www.crossmint.com/api/2025-06-09`) */\n crossmintApiBase?: string;\n\n /** Max polling attempts for tx confirmation (default: 30) */\n maxPollAttempts?: number;\n\n /** Polling interval in ms (default: 2000) */\n pollIntervalMs?: number;\n}\n\n// ── Internals ───────────────────────────────────────────────────────\n\nconst DEFAULT_API_BASE = \"https://www.crossmint.com/api/2025-06-09\";\n\n/** Encode bytes to base58 without external dependency. */\nfunction toBase58(bytes: Uint8Array): string {\n const ALPHABET = \"123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz\";\n const digits = [0];\n for (const byte of bytes) {\n let carry = byte;\n for (let j = 0; j < digits.length; j++) {\n carry += digits[j] << 8;\n digits[j] = carry % 58;\n carry = (carry / 58) | 0;\n }\n while (carry > 0) {\n digits.push(carry % 58);\n carry = (carry / 58) | 0;\n }\n }\n let str = \"\";\n for (const byte of bytes) {\n if (byte === 0) str += ALPHABET[0];\n else break;\n }\n for (let i = digits.length - 1; i >= 0; i--) {\n str += ALPHABET[digits[i]];\n }\n return str;\n}\n\n/** Extract Solana signature from Crossmint's full serialized tx (base58). */\nfunction extractSignature(onChainTx: string): string | null {\n if (!onChainTx) return null;\n if (onChainTx.length <= 100) return onChainTx; // Already a bare signature\n try {\n // Decode base58 → deserialize VersionedTransaction → first signature\n const ALPHABET = \"123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz\";\n const BASE_MAP = new Uint8Array(256).fill(255);\n for (let i = 0; i < ALPHABET.length; i++) BASE_MAP[ALPHABET.charCodeAt(i)] = i;\n const bytes: number[] = [0];\n for (const ch of onChainTx) {\n const carry_init = BASE_MAP[ch.charCodeAt(0)];\n if (carry_init === 255) return null;\n let carry = carry_init;\n for (let j = 0; j < bytes.length; j++) {\n carry += bytes[j] * 58;\n bytes[j] = carry & 0xff;\n carry >>= 8;\n }\n while (carry > 0) {\n bytes.push(carry & 0xff);\n carry >>= 8;\n }\n }\n for (const ch of onChainTx) {\n if (ch === ALPHABET[0]) bytes.push(0);\n else break;\n }\n const decoded = new Uint8Array(bytes.reverse());\n const vtx = VersionedTransaction.deserialize(decoded);\n if (vtx.signatures?.[0]) return toBase58(vtx.signatures[0]);\n } catch {\n // Not a valid serialized tx\n }\n return null;\n}\n\n// ── Public API ──────────────────────────────────────────────────────\n\n/**\n * Create a fetch wrapper that auto-handles x402 `402 Payment Required`\n * responses using a Crossmint smart wallet on Solana.\n *\n * The returned function has the same signature as `fetch()`.\n * On a 402 response it will:\n * 1. Parse payment requirements\n * 2. Build a Solana SPL transfer instruction\n * 3. Submit to Crossmint API (which signs + broadcasts)\n * 4. Poll for on-chain confirmation\n * 5. Retry the original request with an `X-PAYMENT` header\n */\nexport function createCrossmintX402Fetch(config: CrossmintX402Config) {\n const { apiKey, wallet, connection } = config;\n const apiBase = config.crossmintApiBase || DEFAULT_API_BASE;\n const maxPolls = config.maxPollAttempts ?? 30;\n const pollMs = config.pollIntervalMs ?? 2000;\n const userPubkey = new PublicKey(wallet);\n\n return async function fetch402(\n url: string,\n init?: RequestInit,\n ): Promise<Response> {\n // 1. Initial request\n const firstResp = await fetch(url, {\n ...init,\n headers: { Accept: \"application/json\", ...init?.headers },\n });\n if (firstResp.status !== 402) return firstResp;\n\n // 2. Parse payment requirements\n const requirements = (await firstResp.json()) as any;\n const accept = requirements.accepts?.[0];\n if (!accept) throw new Error(\"[x402/crossmint] No payment requirements in 402 response\");\n\n // 3. Build SPL transfer instruction\n const mintPubkey = new PublicKey(accept.asset);\n const merchantPubkey = new PublicKey(accept.payTo);\n const mintAccountInfo = await connection.getAccountInfo(mintPubkey);\n const programId = mintAccountInfo?.owner.equals(TOKEN_2022_PROGRAM_ID)\n ? TOKEN_2022_PROGRAM_ID\n : TOKEN_PROGRAM_ID;\n\n const mintInfo = await getMint(connection, mintPubkey, \"confirmed\", programId);\n const amount = BigInt(accept.amount);\n\n const sourceAta = await getAssociatedTokenAddress(mintPubkey, userPubkey, true, programId);\n const destAta = await getAssociatedTokenAddress(mintPubkey, merchantPubkey, true, programId);\n\n const ix = createTransferCheckedInstruction(\n sourceAta, mintPubkey, destAta, userPubkey,\n amount, mintInfo.decimals, [], programId,\n );\n\n const { blockhash } = await connection.getLatestBlockhash(\"confirmed\");\n const msg = new TransactionMessage({\n payerKey: userPubkey,\n recentBlockhash: blockhash,\n instructions: [ix],\n }).compileToV0Message();\n\n const unsignedTx = new VersionedTransaction(msg);\n const serializedBase58 = toBase58(unsignedTx.serialize());\n\n // 4. Send to Crossmint (signs + broadcasts)\n const txResp = await fetch(\n `${apiBase}/wallets/${encodeURIComponent(wallet)}/transactions`,\n {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\", \"X-API-KEY\": apiKey },\n body: JSON.stringify({ params: { transaction: serializedBase58 } }),\n },\n );\n const txData = (await txResp.json()) as any;\n if (!txResp.ok || !txData.id) {\n throw new Error(`[x402/crossmint] Crossmint API error: ${JSON.stringify(txData)}`);\n }\n\n // 5. Poll for on-chain signature\n let sig: string | null =\n txData.onChain?.txId || extractSignature(txData.onChain?.transaction) || null;\n\n for (let i = 0; i < maxPolls && !sig; i++) {\n await new Promise((r) => setTimeout(r, pollMs));\n const poll = await fetch(\n `${apiBase}/wallets/${encodeURIComponent(wallet)}/transactions/${txData.id}`,\n { headers: { \"X-API-KEY\": apiKey } },\n );\n const p = (await poll.json()) as any;\n if (p.status === \"failed\") {\n throw new Error(`[x402/crossmint] Crossmint tx failed: ${p.error?.message || \"unknown\"}`);\n }\n sig = p.onChain?.txId || extractSignature(p.onChain?.transaction) || null;\n }\n if (!sig) {\n throw new Error(\"[x402/crossmint] Timed out waiting for Crossmint tx confirmation\");\n }\n\n // 6. Build X-PAYMENT header with pre-broadcast txId\n const payload = {\n x402Version: 2,\n scheme: \"exact\",\n network: accept.network,\n payload: {\n transaction: Buffer.from(unsignedTx.serialize()).toString(\"base64\"),\n txId: sig,\n },\n accepted: {\n scheme: accept.scheme,\n network: accept.network,\n amount: accept.amount,\n asset: accept.asset,\n payTo: accept.payTo,\n },\n };\n const xPayment = Buffer.from(JSON.stringify(payload)).toString(\"base64\");\n\n // 7. Retry with payment\n return fetch(url, {\n ...init,\n headers: { Accept: \"application/json\", \"X-PAYMENT\": xPayment, ...init?.headers },\n });\n };\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/crossmint.ts"],"sourcesContent":["/**\n * Crossmint smart wallet integration for RelAI x402.\n *\n * Two modes:\n *\n * **API-key mode** — zero private keys, Crossmint signs + broadcasts:\n * ```ts\n * import { createCrossmintX402Fetch } from \"@relai-fi/x402/crossmint\";\n *\n * const fetch402 = createCrossmintX402Fetch({\n * apiKey: process.env.CROSSMINT_API_KEY!,\n * wallet: process.env.CROSSMINT_WALLET!,\n * connection: new Connection(process.env.RPC_URL!),\n * });\n * const resp = await fetch402(\"https://api.example.com/protected\");\n * ```\n *\n * **Delegated mode** — external signer approves each transaction:\n * ```ts\n * import { createCrossmintDelegatedX402Fetch } from \"@relai-fi/x402/crossmint\";\n *\n * const fetch402 = createCrossmintDelegatedX402Fetch({\n * apiKey: process.env.CROSSMINT_API_KEY!,\n * wallet: process.env.CROSSMINT_WALLET!,\n * signerSecretKey: myKeypairBytes, // 64-byte Ed25519\n * connection: new Connection(process.env.RPC_URL!),\n * });\n * const resp = await fetch402(\"https://api.example.com/protected\");\n * ```\n *\n * @module\n */\nimport {\n Connection,\n PublicKey,\n VersionedTransaction,\n TransactionMessage,\n} from \"@solana/web3.js\";\nimport {\n getAssociatedTokenAddress,\n createTransferCheckedInstruction,\n getMint,\n TOKEN_PROGRAM_ID,\n TOKEN_2022_PROGRAM_ID,\n} from \"@solana/spl-token\";\n\nimport nacl from \"tweetnacl\";\n\n// ── Types ───────────────────────────────────────────────────────────\n\nexport interface CrossmintX402Config {\n /** Crossmint server-side API key (`sk_production_...` or `sk_staging_...`) */\n apiKey: string;\n\n /** Crossmint smart wallet address (Solana public key) */\n wallet: string;\n\n /** Solana RPC connection */\n connection: Connection;\n\n /** Override Crossmint API base URL (default: `https://www.crossmint.com/api/2025-06-09`) */\n crossmintApiBase?: string;\n\n /** Max polling attempts for tx confirmation (default: 30) */\n maxPollAttempts?: number;\n\n /** Polling interval in ms (default: 2000) */\n pollIntervalMs?: number;\n}\n\nexport interface CrossmintDelegatedX402Config extends CrossmintX402Config {\n /**\n * Ed25519 secret key for the external signer (64 bytes).\n * The corresponding public key must be registered as an external signer\n * on the Crossmint smart wallet.\n */\n signerSecretKey: Uint8Array;\n\n /** Crossmint environment: \"staging\" or \"production\" (default: auto-detect from apiKey) */\n environment?: \"staging\" | \"production\";\n}\n\n// ── Internals ───────────────────────────────────────────────────────\n\nconst DEFAULT_API_BASE = \"https://www.crossmint.com/api/2025-06-09\";\nconst STAGING_API_BASE = \"https://staging.crossmint.com/api/2025-06-09\";\n\n/** Auto-detect Crossmint API base from API key prefix. */\nfunction detectApiBase(apiKey: string): string {\n if (apiKey.startsWith(\"sk_staging\")) return STAGING_API_BASE;\n return DEFAULT_API_BASE;\n}\n\n/** Encode bytes to base58 without external dependency. */\nfunction toBase58(bytes: Uint8Array): string {\n const ALPHABET = \"123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz\";\n const digits = [0];\n for (const byte of bytes) {\n let carry = byte;\n for (let j = 0; j < digits.length; j++) {\n carry += digits[j] << 8;\n digits[j] = carry % 58;\n carry = (carry / 58) | 0;\n }\n while (carry > 0) {\n digits.push(carry % 58);\n carry = (carry / 58) | 0;\n }\n }\n let str = \"\";\n for (const byte of bytes) {\n if (byte === 0) str += ALPHABET[0];\n else break;\n }\n for (let i = digits.length - 1; i >= 0; i--) {\n str += ALPHABET[digits[i]];\n }\n return str;\n}\n\n/** Decode base58 string to bytes. */\nfunction fromBase58(str: string): Uint8Array {\n const ALPHABET = \"123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz\";\n const BASE_MAP = new Uint8Array(256).fill(255);\n for (let i = 0; i < ALPHABET.length; i++) BASE_MAP[ALPHABET.charCodeAt(i)] = i;\n const bytes: number[] = [0];\n for (const ch of str) {\n const carry_init = BASE_MAP[ch.charCodeAt(0)];\n if (carry_init === 255) throw new Error(`Invalid base58 character: ${ch}`);\n let carry = carry_init;\n for (let j = 0; j < bytes.length; j++) {\n carry += bytes[j] * 58;\n bytes[j] = carry & 0xff;\n carry >>= 8;\n }\n while (carry > 0) {\n bytes.push(carry & 0xff);\n carry >>= 8;\n }\n }\n for (const ch of str) {\n if (ch === ALPHABET[0]) bytes.push(0);\n else break;\n }\n return new Uint8Array(bytes.reverse());\n}\n\n/** Extract Solana signature from Crossmint's full serialized tx (base58). */\nfunction extractSignature(onChainTx: string): string | null {\n if (!onChainTx) return null;\n if (onChainTx.length <= 100) return onChainTx; // Already a bare signature\n try {\n const decoded = fromBase58(onChainTx);\n const vtx = VersionedTransaction.deserialize(decoded);\n if (vtx.signatures?.[0]) return toBase58(vtx.signatures[0]);\n } catch {\n // Not a valid serialized tx\n }\n return null;\n}\n\n/** Make a Crossmint API call. */\nasync function crossmintApi(\n baseUrl: string, apiKey: string, endpoint: string,\n body?: unknown, method: \"GET\" | \"POST\" = \"POST\",\n): Promise<any> {\n const url = `${baseUrl}/wallets/${endpoint}`;\n const opts: RequestInit = {\n method,\n headers: { \"Content-Type\": \"application/json\", \"X-API-KEY\": apiKey },\n };\n if (method === \"POST\" && body) opts.body = JSON.stringify(body);\n const resp = await fetch(url, opts);\n const json = await resp.json();\n if (!resp.ok) throw new Error(`[x402/crossmint] API ${resp.status}: ${JSON.stringify(json)}`);\n return json;\n}\n\n/** Sign a message with Ed25519. Message can be base58-encoded or UTF-8 string. */\nfunction signMessage(message: string, secretKey: Uint8Array): string {\n let messageBytes: Uint8Array;\n try {\n messageBytes = fromBase58(message);\n } catch {\n messageBytes = new TextEncoder().encode(message);\n }\n const signature = nacl.sign.detached(messageBytes, secretKey);\n return toBase58(signature);\n}\n\n/** Derive public key address from a 64-byte secret key. */\nfunction deriveSignerAddress(secretKey: Uint8Array): string {\n const publicKey = secretKey.length === 64\n ? secretKey.slice(32)\n : nacl.sign.keyPair.fromSeed(secretKey).publicKey;\n return toBase58(publicKey);\n}\n\n/** Poll Crossmint for on-chain txId. Returns signature or null. */\nasync function pollForSignature(\n apiBase: string, apiKey: string, wallet: string,\n txId: string, maxPolls: number, pollMs: number,\n initialData?: any,\n): Promise<string | null> {\n let sig = initialData?.onChain?.txId\n || extractSignature(initialData?.onChain?.transaction) || null;\n\n for (let i = 0; i < maxPolls && !sig; i++) {\n await new Promise((r) => setTimeout(r, pollMs));\n const p = await crossmintApi(apiBase, apiKey,\n `${encodeURIComponent(wallet)}/transactions/${txId}`, undefined, \"GET\");\n if (p.status === \"failed\") {\n throw new Error(`[x402/crossmint] Crossmint tx failed: ${p.error?.message || \"unknown\"}`);\n }\n sig = p.onChain?.txId || extractSignature(p.onChain?.transaction) || null;\n }\n return sig;\n}\n\n/** Build X-PAYMENT header from unsigned tx + on-chain signature + payment requirements. */\nfunction buildXPaymentHeader(unsignedTx: VersionedTransaction, sig: string, accept: any): string {\n const payload = {\n x402Version: 2,\n scheme: \"exact\",\n network: accept.network,\n payload: {\n transaction: Buffer.from(unsignedTx.serialize()).toString(\"base64\"),\n txId: sig,\n },\n accepted: {\n scheme: accept.scheme,\n network: accept.network,\n amount: accept.amount,\n asset: accept.asset,\n payTo: accept.payTo,\n },\n };\n return Buffer.from(JSON.stringify(payload)).toString(\"base64\");\n}\n\n/** Build unsigned SPL transfer tx from 402 payment requirements. */\nasync function buildTransferTx(\n connection: Connection, userPubkey: PublicKey, accept: any,\n): Promise<VersionedTransaction> {\n const mintPubkey = new PublicKey(accept.asset);\n const merchantPubkey = new PublicKey(accept.payTo);\n const mintAccountInfo = await connection.getAccountInfo(mintPubkey);\n const programId = mintAccountInfo?.owner.equals(TOKEN_2022_PROGRAM_ID)\n ? TOKEN_2022_PROGRAM_ID\n : TOKEN_PROGRAM_ID;\n\n const mintInfo = await getMint(connection, mintPubkey, \"confirmed\", programId);\n const amount = BigInt(accept.amount);\n\n const sourceAta = await getAssociatedTokenAddress(mintPubkey, userPubkey, true, programId);\n const destAta = await getAssociatedTokenAddress(mintPubkey, merchantPubkey, true, programId);\n\n const ix = createTransferCheckedInstruction(\n sourceAta, mintPubkey, destAta, userPubkey,\n amount, mintInfo.decimals, [], programId,\n );\n\n const { blockhash } = await connection.getLatestBlockhash(\"confirmed\");\n const msg = new TransactionMessage({\n payerKey: userPubkey,\n recentBlockhash: blockhash,\n instructions: [ix],\n }).compileToV0Message();\n\n return new VersionedTransaction(msg);\n}\n\n// ── Public API ──────────────────────────────────────────────────────\n\n/**\n * **API-key mode.** Create a fetch wrapper that auto-handles x402 `402`\n * responses using a Crossmint smart wallet on Solana.\n *\n * Crossmint signs and broadcasts — no private key needed.\n * The returned function has the same signature as `fetch()`.\n */\nexport function createCrossmintX402Fetch(config: CrossmintX402Config) {\n const { apiKey, wallet, connection } = config;\n const apiBase = config.crossmintApiBase || detectApiBase(apiKey);\n const maxPolls = config.maxPollAttempts ?? 30;\n const pollMs = config.pollIntervalMs ?? 2000;\n const userPubkey = new PublicKey(wallet);\n\n return async function fetch402(url: string, init?: RequestInit): Promise<Response> {\n // 1. Initial request\n const firstResp = await fetch(url, {\n ...init, headers: { Accept: \"application/json\", ...init?.headers },\n });\n if (firstResp.status !== 402) return firstResp;\n\n // 2. Parse 402\n const requirements = (await firstResp.json()) as any;\n const accept = requirements.accepts?.[0];\n if (!accept) throw new Error(\"[x402/crossmint] No payment requirements in 402 response\");\n\n // 3. Build transfer tx\n const unsignedTx = await buildTransferTx(connection, userPubkey, accept);\n\n // 4. Submit to Crossmint (signs + broadcasts)\n const txData = await crossmintApi(apiBase, apiKey,\n `${encodeURIComponent(wallet)}/transactions`,\n { params: { transaction: toBase58(unsignedTx.serialize()) } });\n\n // 5. Poll for on-chain signature\n const sig = await pollForSignature(apiBase, apiKey, wallet, txData.id, maxPolls, pollMs, txData);\n if (!sig) throw new Error(\"[x402/crossmint] Timed out waiting for tx confirmation\");\n\n // 6. Retry with X-PAYMENT\n const xPayment = buildXPaymentHeader(unsignedTx, sig, accept);\n return fetch(url, {\n ...init, headers: { Accept: \"application/json\", \"X-PAYMENT\": xPayment, ...init?.headers },\n });\n };\n}\n\n/**\n * **Delegated mode.** Create a fetch wrapper that auto-handles x402 `402`\n * responses using a Crossmint smart wallet with external signer approval.\n *\n * Each transaction requires cryptographic approval from `signerSecretKey`\n * before Crossmint broadcasts. This provides an extra security layer —\n * even with the API key, transactions cannot execute without the signer.\n *\n * The external signer must be registered on the Crossmint smart wallet\n * via the Crossmint console or API.\n */\nexport function createCrossmintDelegatedX402Fetch(config: CrossmintDelegatedX402Config) {\n const { apiKey, wallet, connection, signerSecretKey } = config;\n const apiBase = config.crossmintApiBase || detectApiBase(apiKey);\n const maxPolls = config.maxPollAttempts ?? 30;\n const pollMs = config.pollIntervalMs ?? 2000;\n const userPubkey = new PublicKey(wallet);\n const signerAddress = deriveSignerAddress(signerSecretKey);\n\n return async function fetch402(url: string, init?: RequestInit): Promise<Response> {\n // 1. Initial request\n const firstResp = await fetch(url, {\n ...init, headers: { Accept: \"application/json\", ...init?.headers },\n });\n if (firstResp.status !== 402) return firstResp;\n\n // 2. Parse 402\n const requirements = (await firstResp.json()) as any;\n const accept = requirements.accepts?.[0];\n if (!accept) throw new Error(\"[x402/crossmint] No payment requirements in 402 response\");\n\n // 3. Build transfer tx\n const unsignedTx = await buildTransferTx(connection, userPubkey, accept);\n\n // 4. Submit to Crossmint with external signer\n const createResp = await crossmintApi(apiBase, apiKey,\n `${encodeURIComponent(wallet)}/transactions`,\n {\n params: {\n transaction: toBase58(unsignedTx.serialize()),\n signer: {\n type: \"external-wallet\",\n locator: `external-wallet:${signerAddress}`,\n },\n },\n });\n\n const transactionId = createResp.id;\n const pendingApproval = createResp.approvals?.pending?.[0];\n\n if (!transactionId || !pendingApproval) {\n throw new Error(\n `[x402/crossmint] No pending approval returned. ` +\n `Ensure external signer ${signerAddress} is registered on wallet ${wallet}. ` +\n `Response: ${JSON.stringify(createResp)}`\n );\n }\n\n // 5. Sign the approval message\n const pendingSigner = pendingApproval.signer?.address\n ?? pendingApproval.signer?.locator?.split(\":\")[1]\n ?? signerAddress;\n\n const approvalSig = signMessage(pendingApproval.message, signerSecretKey);\n\n // 6. Submit approval\n const approvalResp = await crossmintApi(apiBase, apiKey,\n `${encodeURIComponent(wallet)}/transactions/${encodeURIComponent(transactionId)}/approvals`,\n {\n approvals: [{\n signer: `external-wallet:${pendingSigner}`,\n signature: approvalSig,\n }],\n });\n\n // 7. Poll for on-chain signature\n const sig = await pollForSignature(\n apiBase, apiKey, wallet, transactionId, maxPolls, pollMs, approvalResp);\n if (!sig) throw new Error(\"[x402/crossmint] Timed out waiting for tx confirmation\");\n\n // 8. Retry with X-PAYMENT\n const xPayment = buildXPaymentHeader(unsignedTx, sig, accept);\n return fetch(url, {\n ...init, headers: { Accept: \"application/json\", \"X-PAYMENT\": xPayment, ...init?.headers },\n });\n };\n}\n"],"mappings":"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","names":["nacl"]}
@@ -3,23 +3,31 @@ import { Connection } from '@solana/web3.js';
3
3
  /**
4
4
  * Crossmint smart wallet integration for RelAI x402.
5
5
  *
6
- * Auto-handles 402 Payment Required responses using a Crossmint
7
- * API-key smart wallet on Solana. Zero private keys needed —
8
- * Crossmint signs and broadcasts, RelAI facilitator verifies on-chain.
6
+ * Two modes:
9
7
  *
10
- * @example
8
+ * **API-key mode** — zero private keys, Crossmint signs + broadcasts:
11
9
  * ```ts
12
10
  * import { createCrossmintX402Fetch } from "@relai-fi/x402/crossmint";
13
- * import { Connection } from "@solana/web3.js";
14
11
  *
15
12
  * const fetch402 = createCrossmintX402Fetch({
16
13
  * apiKey: process.env.CROSSMINT_API_KEY!,
17
14
  * wallet: process.env.CROSSMINT_WALLET!,
18
15
  * connection: new Connection(process.env.RPC_URL!),
19
16
  * });
17
+ * const resp = await fetch402("https://api.example.com/protected");
18
+ * ```
19
+ *
20
+ * **Delegated mode** — external signer approves each transaction:
21
+ * ```ts
22
+ * import { createCrossmintDelegatedX402Fetch } from "@relai-fi/x402/crossmint";
20
23
  *
24
+ * const fetch402 = createCrossmintDelegatedX402Fetch({
25
+ * apiKey: process.env.CROSSMINT_API_KEY!,
26
+ * wallet: process.env.CROSSMINT_WALLET!,
27
+ * signerSecretKey: myKeypairBytes, // 64-byte Ed25519
28
+ * connection: new Connection(process.env.RPC_URL!),
29
+ * });
21
30
  * const resp = await fetch402("https://api.example.com/protected");
22
- * console.log(await resp.json()); // paid content
23
31
  * ```
24
32
  *
25
33
  * @module
@@ -39,18 +47,35 @@ interface CrossmintX402Config {
39
47
  /** Polling interval in ms (default: 2000) */
40
48
  pollIntervalMs?: number;
41
49
  }
50
+ interface CrossmintDelegatedX402Config extends CrossmintX402Config {
51
+ /**
52
+ * Ed25519 secret key for the external signer (64 bytes).
53
+ * The corresponding public key must be registered as an external signer
54
+ * on the Crossmint smart wallet.
55
+ */
56
+ signerSecretKey: Uint8Array;
57
+ /** Crossmint environment: "staging" or "production" (default: auto-detect from apiKey) */
58
+ environment?: "staging" | "production";
59
+ }
42
60
  /**
43
- * Create a fetch wrapper that auto-handles x402 `402 Payment Required`
61
+ * **API-key mode.** Create a fetch wrapper that auto-handles x402 `402`
44
62
  * responses using a Crossmint smart wallet on Solana.
45
63
  *
64
+ * Crossmint signs and broadcasts — no private key needed.
46
65
  * The returned function has the same signature as `fetch()`.
47
- * On a 402 response it will:
48
- * 1. Parse payment requirements
49
- * 2. Build a Solana SPL transfer instruction
50
- * 3. Submit to Crossmint API (which signs + broadcasts)
51
- * 4. Poll for on-chain confirmation
52
- * 5. Retry the original request with an `X-PAYMENT` header
53
66
  */
54
67
  declare function createCrossmintX402Fetch(config: CrossmintX402Config): (url: string, init?: RequestInit) => Promise<Response>;
68
+ /**
69
+ * **Delegated mode.** Create a fetch wrapper that auto-handles x402 `402`
70
+ * responses using a Crossmint smart wallet with external signer approval.
71
+ *
72
+ * Each transaction requires cryptographic approval from `signerSecretKey`
73
+ * before Crossmint broadcasts. This provides an extra security layer —
74
+ * even with the API key, transactions cannot execute without the signer.
75
+ *
76
+ * The external signer must be registered on the Crossmint smart wallet
77
+ * via the Crossmint console or API.
78
+ */
79
+ declare function createCrossmintDelegatedX402Fetch(config: CrossmintDelegatedX402Config): (url: string, init?: RequestInit) => Promise<Response>;
55
80
 
56
- export { type CrossmintX402Config, createCrossmintX402Fetch };
81
+ export { type CrossmintDelegatedX402Config, type CrossmintX402Config, createCrossmintDelegatedX402Fetch, createCrossmintX402Fetch };
@@ -3,23 +3,31 @@ import { Connection } from '@solana/web3.js';
3
3
  /**
4
4
  * Crossmint smart wallet integration for RelAI x402.
5
5
  *
6
- * Auto-handles 402 Payment Required responses using a Crossmint
7
- * API-key smart wallet on Solana. Zero private keys needed —
8
- * Crossmint signs and broadcasts, RelAI facilitator verifies on-chain.
6
+ * Two modes:
9
7
  *
10
- * @example
8
+ * **API-key mode** — zero private keys, Crossmint signs + broadcasts:
11
9
  * ```ts
12
10
  * import { createCrossmintX402Fetch } from "@relai-fi/x402/crossmint";
13
- * import { Connection } from "@solana/web3.js";
14
11
  *
15
12
  * const fetch402 = createCrossmintX402Fetch({
16
13
  * apiKey: process.env.CROSSMINT_API_KEY!,
17
14
  * wallet: process.env.CROSSMINT_WALLET!,
18
15
  * connection: new Connection(process.env.RPC_URL!),
19
16
  * });
17
+ * const resp = await fetch402("https://api.example.com/protected");
18
+ * ```
19
+ *
20
+ * **Delegated mode** — external signer approves each transaction:
21
+ * ```ts
22
+ * import { createCrossmintDelegatedX402Fetch } from "@relai-fi/x402/crossmint";
20
23
  *
24
+ * const fetch402 = createCrossmintDelegatedX402Fetch({
25
+ * apiKey: process.env.CROSSMINT_API_KEY!,
26
+ * wallet: process.env.CROSSMINT_WALLET!,
27
+ * signerSecretKey: myKeypairBytes, // 64-byte Ed25519
28
+ * connection: new Connection(process.env.RPC_URL!),
29
+ * });
21
30
  * const resp = await fetch402("https://api.example.com/protected");
22
- * console.log(await resp.json()); // paid content
23
31
  * ```
24
32
  *
25
33
  * @module
@@ -39,18 +47,35 @@ interface CrossmintX402Config {
39
47
  /** Polling interval in ms (default: 2000) */
40
48
  pollIntervalMs?: number;
41
49
  }
50
+ interface CrossmintDelegatedX402Config extends CrossmintX402Config {
51
+ /**
52
+ * Ed25519 secret key for the external signer (64 bytes).
53
+ * The corresponding public key must be registered as an external signer
54
+ * on the Crossmint smart wallet.
55
+ */
56
+ signerSecretKey: Uint8Array;
57
+ /** Crossmint environment: "staging" or "production" (default: auto-detect from apiKey) */
58
+ environment?: "staging" | "production";
59
+ }
42
60
  /**
43
- * Create a fetch wrapper that auto-handles x402 `402 Payment Required`
61
+ * **API-key mode.** Create a fetch wrapper that auto-handles x402 `402`
44
62
  * responses using a Crossmint smart wallet on Solana.
45
63
  *
64
+ * Crossmint signs and broadcasts — no private key needed.
46
65
  * The returned function has the same signature as `fetch()`.
47
- * On a 402 response it will:
48
- * 1. Parse payment requirements
49
- * 2. Build a Solana SPL transfer instruction
50
- * 3. Submit to Crossmint API (which signs + broadcasts)
51
- * 4. Poll for on-chain confirmation
52
- * 5. Retry the original request with an `X-PAYMENT` header
53
66
  */
54
67
  declare function createCrossmintX402Fetch(config: CrossmintX402Config): (url: string, init?: RequestInit) => Promise<Response>;
68
+ /**
69
+ * **Delegated mode.** Create a fetch wrapper that auto-handles x402 `402`
70
+ * responses using a Crossmint smart wallet with external signer approval.
71
+ *
72
+ * Each transaction requires cryptographic approval from `signerSecretKey`
73
+ * before Crossmint broadcasts. This provides an extra security layer —
74
+ * even with the API key, transactions cannot execute without the signer.
75
+ *
76
+ * The external signer must be registered on the Crossmint smart wallet
77
+ * via the Crossmint console or API.
78
+ */
79
+ declare function createCrossmintDelegatedX402Fetch(config: CrossmintDelegatedX402Config): (url: string, init?: RequestInit) => Promise<Response>;
55
80
 
56
- export { type CrossmintX402Config, createCrossmintX402Fetch };
81
+ export { type CrossmintDelegatedX402Config, type CrossmintX402Config, createCrossmintDelegatedX402Fetch, createCrossmintX402Fetch };
package/dist/crossmint.js CHANGED
@@ -11,7 +11,13 @@ import {
11
11
  TOKEN_PROGRAM_ID,
12
12
  TOKEN_2022_PROGRAM_ID
13
13
  } from "@solana/spl-token";
14
+ import nacl from "tweetnacl";
14
15
  var DEFAULT_API_BASE = "https://www.crossmint.com/api/2025-06-09";
16
+ var STAGING_API_BASE = "https://staging.crossmint.com/api/2025-06-09";
17
+ function detectApiBase(apiKey) {
18
+ if (apiKey.startsWith("sk_staging")) return STAGING_API_BASE;
19
+ return DEFAULT_API_BASE;
20
+ }
15
21
  function toBase58(bytes) {
16
22
  const ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
17
23
  const digits = [0];
@@ -37,42 +43,135 @@ function toBase58(bytes) {
37
43
  }
38
44
  return str;
39
45
  }
46
+ function fromBase58(str) {
47
+ const ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
48
+ const BASE_MAP = new Uint8Array(256).fill(255);
49
+ for (let i = 0; i < ALPHABET.length; i++) BASE_MAP[ALPHABET.charCodeAt(i)] = i;
50
+ const bytes = [0];
51
+ for (const ch of str) {
52
+ const carry_init = BASE_MAP[ch.charCodeAt(0)];
53
+ if (carry_init === 255) throw new Error(`Invalid base58 character: ${ch}`);
54
+ let carry = carry_init;
55
+ for (let j = 0; j < bytes.length; j++) {
56
+ carry += bytes[j] * 58;
57
+ bytes[j] = carry & 255;
58
+ carry >>= 8;
59
+ }
60
+ while (carry > 0) {
61
+ bytes.push(carry & 255);
62
+ carry >>= 8;
63
+ }
64
+ }
65
+ for (const ch of str) {
66
+ if (ch === ALPHABET[0]) bytes.push(0);
67
+ else break;
68
+ }
69
+ return new Uint8Array(bytes.reverse());
70
+ }
40
71
  function extractSignature(onChainTx) {
41
72
  if (!onChainTx) return null;
42
73
  if (onChainTx.length <= 100) return onChainTx;
43
74
  try {
44
- const ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
45
- const BASE_MAP = new Uint8Array(256).fill(255);
46
- for (let i = 0; i < ALPHABET.length; i++) BASE_MAP[ALPHABET.charCodeAt(i)] = i;
47
- const bytes = [0];
48
- for (const ch of onChainTx) {
49
- const carry_init = BASE_MAP[ch.charCodeAt(0)];
50
- if (carry_init === 255) return null;
51
- let carry = carry_init;
52
- for (let j = 0; j < bytes.length; j++) {
53
- carry += bytes[j] * 58;
54
- bytes[j] = carry & 255;
55
- carry >>= 8;
56
- }
57
- while (carry > 0) {
58
- bytes.push(carry & 255);
59
- carry >>= 8;
60
- }
61
- }
62
- for (const ch of onChainTx) {
63
- if (ch === ALPHABET[0]) bytes.push(0);
64
- else break;
65
- }
66
- const decoded = new Uint8Array(bytes.reverse());
75
+ const decoded = fromBase58(onChainTx);
67
76
  const vtx = VersionedTransaction.deserialize(decoded);
68
77
  if (vtx.signatures?.[0]) return toBase58(vtx.signatures[0]);
69
78
  } catch {
70
79
  }
71
80
  return null;
72
81
  }
82
+ async function crossmintApi(baseUrl, apiKey, endpoint, body, method = "POST") {
83
+ const url = `${baseUrl}/wallets/${endpoint}`;
84
+ const opts = {
85
+ method,
86
+ headers: { "Content-Type": "application/json", "X-API-KEY": apiKey }
87
+ };
88
+ if (method === "POST" && body) opts.body = JSON.stringify(body);
89
+ const resp = await fetch(url, opts);
90
+ const json = await resp.json();
91
+ if (!resp.ok) throw new Error(`[x402/crossmint] API ${resp.status}: ${JSON.stringify(json)}`);
92
+ return json;
93
+ }
94
+ function signMessage(message, secretKey) {
95
+ let messageBytes;
96
+ try {
97
+ messageBytes = fromBase58(message);
98
+ } catch {
99
+ messageBytes = new TextEncoder().encode(message);
100
+ }
101
+ const signature = nacl.sign.detached(messageBytes, secretKey);
102
+ return toBase58(signature);
103
+ }
104
+ function deriveSignerAddress(secretKey) {
105
+ const publicKey = secretKey.length === 64 ? secretKey.slice(32) : nacl.sign.keyPair.fromSeed(secretKey).publicKey;
106
+ return toBase58(publicKey);
107
+ }
108
+ async function pollForSignature(apiBase, apiKey, wallet, txId, maxPolls, pollMs, initialData) {
109
+ let sig = initialData?.onChain?.txId || extractSignature(initialData?.onChain?.transaction) || null;
110
+ for (let i = 0; i < maxPolls && !sig; i++) {
111
+ await new Promise((r) => setTimeout(r, pollMs));
112
+ const p = await crossmintApi(
113
+ apiBase,
114
+ apiKey,
115
+ `${encodeURIComponent(wallet)}/transactions/${txId}`,
116
+ void 0,
117
+ "GET"
118
+ );
119
+ if (p.status === "failed") {
120
+ throw new Error(`[x402/crossmint] Crossmint tx failed: ${p.error?.message || "unknown"}`);
121
+ }
122
+ sig = p.onChain?.txId || extractSignature(p.onChain?.transaction) || null;
123
+ }
124
+ return sig;
125
+ }
126
+ function buildXPaymentHeader(unsignedTx, sig, accept) {
127
+ const payload = {
128
+ x402Version: 2,
129
+ scheme: "exact",
130
+ network: accept.network,
131
+ payload: {
132
+ transaction: Buffer.from(unsignedTx.serialize()).toString("base64"),
133
+ txId: sig
134
+ },
135
+ accepted: {
136
+ scheme: accept.scheme,
137
+ network: accept.network,
138
+ amount: accept.amount,
139
+ asset: accept.asset,
140
+ payTo: accept.payTo
141
+ }
142
+ };
143
+ return Buffer.from(JSON.stringify(payload)).toString("base64");
144
+ }
145
+ async function buildTransferTx(connection, userPubkey, accept) {
146
+ const mintPubkey = new PublicKey(accept.asset);
147
+ const merchantPubkey = new PublicKey(accept.payTo);
148
+ const mintAccountInfo = await connection.getAccountInfo(mintPubkey);
149
+ const programId = mintAccountInfo?.owner.equals(TOKEN_2022_PROGRAM_ID) ? TOKEN_2022_PROGRAM_ID : TOKEN_PROGRAM_ID;
150
+ const mintInfo = await getMint(connection, mintPubkey, "confirmed", programId);
151
+ const amount = BigInt(accept.amount);
152
+ const sourceAta = await getAssociatedTokenAddress(mintPubkey, userPubkey, true, programId);
153
+ const destAta = await getAssociatedTokenAddress(mintPubkey, merchantPubkey, true, programId);
154
+ const ix = createTransferCheckedInstruction(
155
+ sourceAta,
156
+ mintPubkey,
157
+ destAta,
158
+ userPubkey,
159
+ amount,
160
+ mintInfo.decimals,
161
+ [],
162
+ programId
163
+ );
164
+ const { blockhash } = await connection.getLatestBlockhash("confirmed");
165
+ const msg = new TransactionMessage({
166
+ payerKey: userPubkey,
167
+ recentBlockhash: blockhash,
168
+ instructions: [ix]
169
+ }).compileToV0Message();
170
+ return new VersionedTransaction(msg);
171
+ }
73
172
  function createCrossmintX402Fetch(config) {
74
173
  const { apiKey, wallet, connection } = config;
75
- const apiBase = config.crossmintApiBase || DEFAULT_API_BASE;
174
+ const apiBase = config.crossmintApiBase || detectApiBase(apiKey);
76
175
  const maxPolls = config.maxPollAttempts ?? 30;
77
176
  const pollMs = config.pollIntervalMs ?? 2e3;
78
177
  const userPubkey = new PublicKey(wallet);
@@ -85,77 +184,84 @@ function createCrossmintX402Fetch(config) {
85
184
  const requirements = await firstResp.json();
86
185
  const accept = requirements.accepts?.[0];
87
186
  if (!accept) throw new Error("[x402/crossmint] No payment requirements in 402 response");
88
- const mintPubkey = new PublicKey(accept.asset);
89
- const merchantPubkey = new PublicKey(accept.payTo);
90
- const mintAccountInfo = await connection.getAccountInfo(mintPubkey);
91
- const programId = mintAccountInfo?.owner.equals(TOKEN_2022_PROGRAM_ID) ? TOKEN_2022_PROGRAM_ID : TOKEN_PROGRAM_ID;
92
- const mintInfo = await getMint(connection, mintPubkey, "confirmed", programId);
93
- const amount = BigInt(accept.amount);
94
- const sourceAta = await getAssociatedTokenAddress(mintPubkey, userPubkey, true, programId);
95
- const destAta = await getAssociatedTokenAddress(mintPubkey, merchantPubkey, true, programId);
96
- const ix = createTransferCheckedInstruction(
97
- sourceAta,
98
- mintPubkey,
99
- destAta,
100
- userPubkey,
101
- amount,
102
- mintInfo.decimals,
103
- [],
104
- programId
187
+ const unsignedTx = await buildTransferTx(connection, userPubkey, accept);
188
+ const txData = await crossmintApi(
189
+ apiBase,
190
+ apiKey,
191
+ `${encodeURIComponent(wallet)}/transactions`,
192
+ { params: { transaction: toBase58(unsignedTx.serialize()) } }
105
193
  );
106
- const { blockhash } = await connection.getLatestBlockhash("confirmed");
107
- const msg = new TransactionMessage({
108
- payerKey: userPubkey,
109
- recentBlockhash: blockhash,
110
- instructions: [ix]
111
- }).compileToV0Message();
112
- const unsignedTx = new VersionedTransaction(msg);
113
- const serializedBase58 = toBase58(unsignedTx.serialize());
114
- const txResp = await fetch(
115
- `${apiBase}/wallets/${encodeURIComponent(wallet)}/transactions`,
194
+ const sig = await pollForSignature(apiBase, apiKey, wallet, txData.id, maxPolls, pollMs, txData);
195
+ if (!sig) throw new Error("[x402/crossmint] Timed out waiting for tx confirmation");
196
+ const xPayment = buildXPaymentHeader(unsignedTx, sig, accept);
197
+ return fetch(url, {
198
+ ...init,
199
+ headers: { Accept: "application/json", "X-PAYMENT": xPayment, ...init?.headers }
200
+ });
201
+ };
202
+ }
203
+ function createCrossmintDelegatedX402Fetch(config) {
204
+ const { apiKey, wallet, connection, signerSecretKey } = config;
205
+ const apiBase = config.crossmintApiBase || detectApiBase(apiKey);
206
+ const maxPolls = config.maxPollAttempts ?? 30;
207
+ const pollMs = config.pollIntervalMs ?? 2e3;
208
+ const userPubkey = new PublicKey(wallet);
209
+ const signerAddress = deriveSignerAddress(signerSecretKey);
210
+ return async function fetch402(url, init) {
211
+ const firstResp = await fetch(url, {
212
+ ...init,
213
+ headers: { Accept: "application/json", ...init?.headers }
214
+ });
215
+ if (firstResp.status !== 402) return firstResp;
216
+ const requirements = await firstResp.json();
217
+ const accept = requirements.accepts?.[0];
218
+ if (!accept) throw new Error("[x402/crossmint] No payment requirements in 402 response");
219
+ const unsignedTx = await buildTransferTx(connection, userPubkey, accept);
220
+ const createResp = await crossmintApi(
221
+ apiBase,
222
+ apiKey,
223
+ `${encodeURIComponent(wallet)}/transactions`,
116
224
  {
117
- method: "POST",
118
- headers: { "Content-Type": "application/json", "X-API-KEY": apiKey },
119
- body: JSON.stringify({ params: { transaction: serializedBase58 } })
225
+ params: {
226
+ transaction: toBase58(unsignedTx.serialize()),
227
+ signer: {
228
+ type: "external-wallet",
229
+ locator: `external-wallet:${signerAddress}`
230
+ }
231
+ }
120
232
  }
121
233
  );
122
- const txData = await txResp.json();
123
- if (!txResp.ok || !txData.id) {
124
- throw new Error(`[x402/crossmint] Crossmint API error: ${JSON.stringify(txData)}`);
125
- }
126
- let sig = txData.onChain?.txId || extractSignature(txData.onChain?.transaction) || null;
127
- for (let i = 0; i < maxPolls && !sig; i++) {
128
- await new Promise((r) => setTimeout(r, pollMs));
129
- const poll = await fetch(
130
- `${apiBase}/wallets/${encodeURIComponent(wallet)}/transactions/${txData.id}`,
131
- { headers: { "X-API-KEY": apiKey } }
234
+ const transactionId = createResp.id;
235
+ const pendingApproval = createResp.approvals?.pending?.[0];
236
+ if (!transactionId || !pendingApproval) {
237
+ throw new Error(
238
+ `[x402/crossmint] No pending approval returned. Ensure external signer ${signerAddress} is registered on wallet ${wallet}. Response: ${JSON.stringify(createResp)}`
132
239
  );
133
- const p = await poll.json();
134
- if (p.status === "failed") {
135
- throw new Error(`[x402/crossmint] Crossmint tx failed: ${p.error?.message || "unknown"}`);
136
- }
137
- sig = p.onChain?.txId || extractSignature(p.onChain?.transaction) || null;
138
- }
139
- if (!sig) {
140
- throw new Error("[x402/crossmint] Timed out waiting for Crossmint tx confirmation");
141
240
  }
142
- const payload = {
143
- x402Version: 2,
144
- scheme: "exact",
145
- network: accept.network,
146
- payload: {
147
- transaction: Buffer.from(unsignedTx.serialize()).toString("base64"),
148
- txId: sig
149
- },
150
- accepted: {
151
- scheme: accept.scheme,
152
- network: accept.network,
153
- amount: accept.amount,
154
- asset: accept.asset,
155
- payTo: accept.payTo
241
+ const pendingSigner = pendingApproval.signer?.address ?? pendingApproval.signer?.locator?.split(":")[1] ?? signerAddress;
242
+ const approvalSig = signMessage(pendingApproval.message, signerSecretKey);
243
+ const approvalResp = await crossmintApi(
244
+ apiBase,
245
+ apiKey,
246
+ `${encodeURIComponent(wallet)}/transactions/${encodeURIComponent(transactionId)}/approvals`,
247
+ {
248
+ approvals: [{
249
+ signer: `external-wallet:${pendingSigner}`,
250
+ signature: approvalSig
251
+ }]
156
252
  }
157
- };
158
- const xPayment = Buffer.from(JSON.stringify(payload)).toString("base64");
253
+ );
254
+ const sig = await pollForSignature(
255
+ apiBase,
256
+ apiKey,
257
+ wallet,
258
+ transactionId,
259
+ maxPolls,
260
+ pollMs,
261
+ approvalResp
262
+ );
263
+ if (!sig) throw new Error("[x402/crossmint] Timed out waiting for tx confirmation");
264
+ const xPayment = buildXPaymentHeader(unsignedTx, sig, accept);
159
265
  return fetch(url, {
160
266
  ...init,
161
267
  headers: { Accept: "application/json", "X-PAYMENT": xPayment, ...init?.headers }
@@ -163,6 +269,7 @@ function createCrossmintX402Fetch(config) {
163
269
  };
164
270
  }
165
271
  export {
272
+ createCrossmintDelegatedX402Fetch,
166
273
  createCrossmintX402Fetch
167
274
  };
168
275
  //# sourceMappingURL=crossmint.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/crossmint.ts"],"sourcesContent":["/**\n * Crossmint smart wallet integration for RelAI x402.\n *\n * Auto-handles 402 Payment Required responses using a Crossmint\n * API-key smart wallet on Solana. Zero private keys needed —\n * Crossmint signs and broadcasts, RelAI facilitator verifies on-chain.\n *\n * @example\n * ```ts\n * import { createCrossmintX402Fetch } from \"@relai-fi/x402/crossmint\";\n * import { Connection } from \"@solana/web3.js\";\n *\n * const fetch402 = createCrossmintX402Fetch({\n * apiKey: process.env.CROSSMINT_API_KEY!,\n * wallet: process.env.CROSSMINT_WALLET!,\n * connection: new Connection(process.env.RPC_URL!),\n * });\n *\n * const resp = await fetch402(\"https://api.example.com/protected\");\n * console.log(await resp.json()); // paid content\n * ```\n *\n * @module\n */\nimport {\n Connection,\n PublicKey,\n VersionedTransaction,\n TransactionMessage,\n} from \"@solana/web3.js\";\nimport {\n getAssociatedTokenAddress,\n createTransferCheckedInstruction,\n getMint,\n TOKEN_PROGRAM_ID,\n TOKEN_2022_PROGRAM_ID,\n} from \"@solana/spl-token\";\n\n// ── Types ───────────────────────────────────────────────────────────\n\nexport interface CrossmintX402Config {\n /** Crossmint server-side API key (`sk_production_...` or `sk_staging_...`) */\n apiKey: string;\n\n /** Crossmint smart wallet address (Solana public key) */\n wallet: string;\n\n /** Solana RPC connection */\n connection: Connection;\n\n /** Override Crossmint API base URL (default: `https://www.crossmint.com/api/2025-06-09`) */\n crossmintApiBase?: string;\n\n /** Max polling attempts for tx confirmation (default: 30) */\n maxPollAttempts?: number;\n\n /** Polling interval in ms (default: 2000) */\n pollIntervalMs?: number;\n}\n\n// ── Internals ───────────────────────────────────────────────────────\n\nconst DEFAULT_API_BASE = \"https://www.crossmint.com/api/2025-06-09\";\n\n/** Encode bytes to base58 without external dependency. */\nfunction toBase58(bytes: Uint8Array): string {\n const ALPHABET = \"123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz\";\n const digits = [0];\n for (const byte of bytes) {\n let carry = byte;\n for (let j = 0; j < digits.length; j++) {\n carry += digits[j] << 8;\n digits[j] = carry % 58;\n carry = (carry / 58) | 0;\n }\n while (carry > 0) {\n digits.push(carry % 58);\n carry = (carry / 58) | 0;\n }\n }\n let str = \"\";\n for (const byte of bytes) {\n if (byte === 0) str += ALPHABET[0];\n else break;\n }\n for (let i = digits.length - 1; i >= 0; i--) {\n str += ALPHABET[digits[i]];\n }\n return str;\n}\n\n/** Extract Solana signature from Crossmint's full serialized tx (base58). */\nfunction extractSignature(onChainTx: string): string | null {\n if (!onChainTx) return null;\n if (onChainTx.length <= 100) return onChainTx; // Already a bare signature\n try {\n // Decode base58 → deserialize VersionedTransaction → first signature\n const ALPHABET = \"123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz\";\n const BASE_MAP = new Uint8Array(256).fill(255);\n for (let i = 0; i < ALPHABET.length; i++) BASE_MAP[ALPHABET.charCodeAt(i)] = i;\n const bytes: number[] = [0];\n for (const ch of onChainTx) {\n const carry_init = BASE_MAP[ch.charCodeAt(0)];\n if (carry_init === 255) return null;\n let carry = carry_init;\n for (let j = 0; j < bytes.length; j++) {\n carry += bytes[j] * 58;\n bytes[j] = carry & 0xff;\n carry >>= 8;\n }\n while (carry > 0) {\n bytes.push(carry & 0xff);\n carry >>= 8;\n }\n }\n for (const ch of onChainTx) {\n if (ch === ALPHABET[0]) bytes.push(0);\n else break;\n }\n const decoded = new Uint8Array(bytes.reverse());\n const vtx = VersionedTransaction.deserialize(decoded);\n if (vtx.signatures?.[0]) return toBase58(vtx.signatures[0]);\n } catch {\n // Not a valid serialized tx\n }\n return null;\n}\n\n// ── Public API ──────────────────────────────────────────────────────\n\n/**\n * Create a fetch wrapper that auto-handles x402 `402 Payment Required`\n * responses using a Crossmint smart wallet on Solana.\n *\n * The returned function has the same signature as `fetch()`.\n * On a 402 response it will:\n * 1. Parse payment requirements\n * 2. Build a Solana SPL transfer instruction\n * 3. Submit to Crossmint API (which signs + broadcasts)\n * 4. Poll for on-chain confirmation\n * 5. Retry the original request with an `X-PAYMENT` header\n */\nexport function createCrossmintX402Fetch(config: CrossmintX402Config) {\n const { apiKey, wallet, connection } = config;\n const apiBase = config.crossmintApiBase || DEFAULT_API_BASE;\n const maxPolls = config.maxPollAttempts ?? 30;\n const pollMs = config.pollIntervalMs ?? 2000;\n const userPubkey = new PublicKey(wallet);\n\n return async function fetch402(\n url: string,\n init?: RequestInit,\n ): Promise<Response> {\n // 1. Initial request\n const firstResp = await fetch(url, {\n ...init,\n headers: { Accept: \"application/json\", ...init?.headers },\n });\n if (firstResp.status !== 402) return firstResp;\n\n // 2. Parse payment requirements\n const requirements = (await firstResp.json()) as any;\n const accept = requirements.accepts?.[0];\n if (!accept) throw new Error(\"[x402/crossmint] No payment requirements in 402 response\");\n\n // 3. Build SPL transfer instruction\n const mintPubkey = new PublicKey(accept.asset);\n const merchantPubkey = new PublicKey(accept.payTo);\n const mintAccountInfo = await connection.getAccountInfo(mintPubkey);\n const programId = mintAccountInfo?.owner.equals(TOKEN_2022_PROGRAM_ID)\n ? TOKEN_2022_PROGRAM_ID\n : TOKEN_PROGRAM_ID;\n\n const mintInfo = await getMint(connection, mintPubkey, \"confirmed\", programId);\n const amount = BigInt(accept.amount);\n\n const sourceAta = await getAssociatedTokenAddress(mintPubkey, userPubkey, true, programId);\n const destAta = await getAssociatedTokenAddress(mintPubkey, merchantPubkey, true, programId);\n\n const ix = createTransferCheckedInstruction(\n sourceAta, mintPubkey, destAta, userPubkey,\n amount, mintInfo.decimals, [], programId,\n );\n\n const { blockhash } = await connection.getLatestBlockhash(\"confirmed\");\n const msg = new TransactionMessage({\n payerKey: userPubkey,\n recentBlockhash: blockhash,\n instructions: [ix],\n }).compileToV0Message();\n\n const unsignedTx = new VersionedTransaction(msg);\n const serializedBase58 = toBase58(unsignedTx.serialize());\n\n // 4. Send to Crossmint (signs + broadcasts)\n const txResp = await fetch(\n `${apiBase}/wallets/${encodeURIComponent(wallet)}/transactions`,\n {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\", \"X-API-KEY\": apiKey },\n body: JSON.stringify({ params: { transaction: serializedBase58 } }),\n },\n );\n const txData = (await txResp.json()) as any;\n if (!txResp.ok || !txData.id) {\n throw new Error(`[x402/crossmint] Crossmint API error: ${JSON.stringify(txData)}`);\n }\n\n // 5. Poll for on-chain signature\n let sig: string | null =\n txData.onChain?.txId || extractSignature(txData.onChain?.transaction) || null;\n\n for (let i = 0; i < maxPolls && !sig; i++) {\n await new Promise((r) => setTimeout(r, pollMs));\n const poll = await fetch(\n `${apiBase}/wallets/${encodeURIComponent(wallet)}/transactions/${txData.id}`,\n { headers: { \"X-API-KEY\": apiKey } },\n );\n const p = (await poll.json()) as any;\n if (p.status === \"failed\") {\n throw new Error(`[x402/crossmint] Crossmint tx failed: ${p.error?.message || \"unknown\"}`);\n }\n sig = p.onChain?.txId || extractSignature(p.onChain?.transaction) || null;\n }\n if (!sig) {\n throw new Error(\"[x402/crossmint] Timed out waiting for Crossmint tx confirmation\");\n }\n\n // 6. Build X-PAYMENT header with pre-broadcast txId\n const payload = {\n x402Version: 2,\n scheme: \"exact\",\n network: accept.network,\n payload: {\n transaction: Buffer.from(unsignedTx.serialize()).toString(\"base64\"),\n txId: sig,\n },\n accepted: {\n scheme: accept.scheme,\n network: accept.network,\n amount: accept.amount,\n asset: accept.asset,\n payTo: accept.payTo,\n },\n };\n const xPayment = Buffer.from(JSON.stringify(payload)).toString(\"base64\");\n\n // 7. Retry with payment\n return fetch(url, {\n ...init,\n headers: { Accept: \"application/json\", \"X-PAYMENT\": xPayment, ...init?.headers },\n });\n };\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/crossmint.ts"],"sourcesContent":["/**\n * Crossmint smart wallet integration for RelAI x402.\n *\n * Two modes:\n *\n * **API-key mode** — zero private keys, Crossmint signs + broadcasts:\n * ```ts\n * import { createCrossmintX402Fetch } from \"@relai-fi/x402/crossmint\";\n *\n * const fetch402 = createCrossmintX402Fetch({\n * apiKey: process.env.CROSSMINT_API_KEY!,\n * wallet: process.env.CROSSMINT_WALLET!,\n * connection: new Connection(process.env.RPC_URL!),\n * });\n * const resp = await fetch402(\"https://api.example.com/protected\");\n * ```\n *\n * **Delegated mode** — external signer approves each transaction:\n * ```ts\n * import { createCrossmintDelegatedX402Fetch } from \"@relai-fi/x402/crossmint\";\n *\n * const fetch402 = createCrossmintDelegatedX402Fetch({\n * apiKey: process.env.CROSSMINT_API_KEY!,\n * wallet: process.env.CROSSMINT_WALLET!,\n * signerSecretKey: myKeypairBytes, // 64-byte Ed25519\n * connection: new Connection(process.env.RPC_URL!),\n * });\n * const resp = await fetch402(\"https://api.example.com/protected\");\n * ```\n *\n * @module\n */\nimport {\n Connection,\n PublicKey,\n VersionedTransaction,\n TransactionMessage,\n} from \"@solana/web3.js\";\nimport {\n getAssociatedTokenAddress,\n createTransferCheckedInstruction,\n getMint,\n TOKEN_PROGRAM_ID,\n TOKEN_2022_PROGRAM_ID,\n} from \"@solana/spl-token\";\n\nimport nacl from \"tweetnacl\";\n\n// ── Types ───────────────────────────────────────────────────────────\n\nexport interface CrossmintX402Config {\n /** Crossmint server-side API key (`sk_production_...` or `sk_staging_...`) */\n apiKey: string;\n\n /** Crossmint smart wallet address (Solana public key) */\n wallet: string;\n\n /** Solana RPC connection */\n connection: Connection;\n\n /** Override Crossmint API base URL (default: `https://www.crossmint.com/api/2025-06-09`) */\n crossmintApiBase?: string;\n\n /** Max polling attempts for tx confirmation (default: 30) */\n maxPollAttempts?: number;\n\n /** Polling interval in ms (default: 2000) */\n pollIntervalMs?: number;\n}\n\nexport interface CrossmintDelegatedX402Config extends CrossmintX402Config {\n /**\n * Ed25519 secret key for the external signer (64 bytes).\n * The corresponding public key must be registered as an external signer\n * on the Crossmint smart wallet.\n */\n signerSecretKey: Uint8Array;\n\n /** Crossmint environment: \"staging\" or \"production\" (default: auto-detect from apiKey) */\n environment?: \"staging\" | \"production\";\n}\n\n// ── Internals ───────────────────────────────────────────────────────\n\nconst DEFAULT_API_BASE = \"https://www.crossmint.com/api/2025-06-09\";\nconst STAGING_API_BASE = \"https://staging.crossmint.com/api/2025-06-09\";\n\n/** Auto-detect Crossmint API base from API key prefix. */\nfunction detectApiBase(apiKey: string): string {\n if (apiKey.startsWith(\"sk_staging\")) return STAGING_API_BASE;\n return DEFAULT_API_BASE;\n}\n\n/** Encode bytes to base58 without external dependency. */\nfunction toBase58(bytes: Uint8Array): string {\n const ALPHABET = \"123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz\";\n const digits = [0];\n for (const byte of bytes) {\n let carry = byte;\n for (let j = 0; j < digits.length; j++) {\n carry += digits[j] << 8;\n digits[j] = carry % 58;\n carry = (carry / 58) | 0;\n }\n while (carry > 0) {\n digits.push(carry % 58);\n carry = (carry / 58) | 0;\n }\n }\n let str = \"\";\n for (const byte of bytes) {\n if (byte === 0) str += ALPHABET[0];\n else break;\n }\n for (let i = digits.length - 1; i >= 0; i--) {\n str += ALPHABET[digits[i]];\n }\n return str;\n}\n\n/** Decode base58 string to bytes. */\nfunction fromBase58(str: string): Uint8Array {\n const ALPHABET = \"123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz\";\n const BASE_MAP = new Uint8Array(256).fill(255);\n for (let i = 0; i < ALPHABET.length; i++) BASE_MAP[ALPHABET.charCodeAt(i)] = i;\n const bytes: number[] = [0];\n for (const ch of str) {\n const carry_init = BASE_MAP[ch.charCodeAt(0)];\n if (carry_init === 255) throw new Error(`Invalid base58 character: ${ch}`);\n let carry = carry_init;\n for (let j = 0; j < bytes.length; j++) {\n carry += bytes[j] * 58;\n bytes[j] = carry & 0xff;\n carry >>= 8;\n }\n while (carry > 0) {\n bytes.push(carry & 0xff);\n carry >>= 8;\n }\n }\n for (const ch of str) {\n if (ch === ALPHABET[0]) bytes.push(0);\n else break;\n }\n return new Uint8Array(bytes.reverse());\n}\n\n/** Extract Solana signature from Crossmint's full serialized tx (base58). */\nfunction extractSignature(onChainTx: string): string | null {\n if (!onChainTx) return null;\n if (onChainTx.length <= 100) return onChainTx; // Already a bare signature\n try {\n const decoded = fromBase58(onChainTx);\n const vtx = VersionedTransaction.deserialize(decoded);\n if (vtx.signatures?.[0]) return toBase58(vtx.signatures[0]);\n } catch {\n // Not a valid serialized tx\n }\n return null;\n}\n\n/** Make a Crossmint API call. */\nasync function crossmintApi(\n baseUrl: string, apiKey: string, endpoint: string,\n body?: unknown, method: \"GET\" | \"POST\" = \"POST\",\n): Promise<any> {\n const url = `${baseUrl}/wallets/${endpoint}`;\n const opts: RequestInit = {\n method,\n headers: { \"Content-Type\": \"application/json\", \"X-API-KEY\": apiKey },\n };\n if (method === \"POST\" && body) opts.body = JSON.stringify(body);\n const resp = await fetch(url, opts);\n const json = await resp.json();\n if (!resp.ok) throw new Error(`[x402/crossmint] API ${resp.status}: ${JSON.stringify(json)}`);\n return json;\n}\n\n/** Sign a message with Ed25519. Message can be base58-encoded or UTF-8 string. */\nfunction signMessage(message: string, secretKey: Uint8Array): string {\n let messageBytes: Uint8Array;\n try {\n messageBytes = fromBase58(message);\n } catch {\n messageBytes = new TextEncoder().encode(message);\n }\n const signature = nacl.sign.detached(messageBytes, secretKey);\n return toBase58(signature);\n}\n\n/** Derive public key address from a 64-byte secret key. */\nfunction deriveSignerAddress(secretKey: Uint8Array): string {\n const publicKey = secretKey.length === 64\n ? secretKey.slice(32)\n : nacl.sign.keyPair.fromSeed(secretKey).publicKey;\n return toBase58(publicKey);\n}\n\n/** Poll Crossmint for on-chain txId. Returns signature or null. */\nasync function pollForSignature(\n apiBase: string, apiKey: string, wallet: string,\n txId: string, maxPolls: number, pollMs: number,\n initialData?: any,\n): Promise<string | null> {\n let sig = initialData?.onChain?.txId\n || extractSignature(initialData?.onChain?.transaction) || null;\n\n for (let i = 0; i < maxPolls && !sig; i++) {\n await new Promise((r) => setTimeout(r, pollMs));\n const p = await crossmintApi(apiBase, apiKey,\n `${encodeURIComponent(wallet)}/transactions/${txId}`, undefined, \"GET\");\n if (p.status === \"failed\") {\n throw new Error(`[x402/crossmint] Crossmint tx failed: ${p.error?.message || \"unknown\"}`);\n }\n sig = p.onChain?.txId || extractSignature(p.onChain?.transaction) || null;\n }\n return sig;\n}\n\n/** Build X-PAYMENT header from unsigned tx + on-chain signature + payment requirements. */\nfunction buildXPaymentHeader(unsignedTx: VersionedTransaction, sig: string, accept: any): string {\n const payload = {\n x402Version: 2,\n scheme: \"exact\",\n network: accept.network,\n payload: {\n transaction: Buffer.from(unsignedTx.serialize()).toString(\"base64\"),\n txId: sig,\n },\n accepted: {\n scheme: accept.scheme,\n network: accept.network,\n amount: accept.amount,\n asset: accept.asset,\n payTo: accept.payTo,\n },\n };\n return Buffer.from(JSON.stringify(payload)).toString(\"base64\");\n}\n\n/** Build unsigned SPL transfer tx from 402 payment requirements. */\nasync function buildTransferTx(\n connection: Connection, userPubkey: PublicKey, accept: any,\n): Promise<VersionedTransaction> {\n const mintPubkey = new PublicKey(accept.asset);\n const merchantPubkey = new PublicKey(accept.payTo);\n const mintAccountInfo = await connection.getAccountInfo(mintPubkey);\n const programId = mintAccountInfo?.owner.equals(TOKEN_2022_PROGRAM_ID)\n ? TOKEN_2022_PROGRAM_ID\n : TOKEN_PROGRAM_ID;\n\n const mintInfo = await getMint(connection, mintPubkey, \"confirmed\", programId);\n const amount = BigInt(accept.amount);\n\n const sourceAta = await getAssociatedTokenAddress(mintPubkey, userPubkey, true, programId);\n const destAta = await getAssociatedTokenAddress(mintPubkey, merchantPubkey, true, programId);\n\n const ix = createTransferCheckedInstruction(\n sourceAta, mintPubkey, destAta, userPubkey,\n amount, mintInfo.decimals, [], programId,\n );\n\n const { blockhash } = await connection.getLatestBlockhash(\"confirmed\");\n const msg = new TransactionMessage({\n payerKey: userPubkey,\n recentBlockhash: blockhash,\n instructions: [ix],\n }).compileToV0Message();\n\n return new VersionedTransaction(msg);\n}\n\n// ── Public API ──────────────────────────────────────────────────────\n\n/**\n * **API-key mode.** Create a fetch wrapper that auto-handles x402 `402`\n * responses using a Crossmint smart wallet on Solana.\n *\n * Crossmint signs and broadcasts — no private key needed.\n * The returned function has the same signature as `fetch()`.\n */\nexport function createCrossmintX402Fetch(config: CrossmintX402Config) {\n const { apiKey, wallet, connection } = config;\n const apiBase = config.crossmintApiBase || detectApiBase(apiKey);\n const maxPolls = config.maxPollAttempts ?? 30;\n const pollMs = config.pollIntervalMs ?? 2000;\n const userPubkey = new PublicKey(wallet);\n\n return async function fetch402(url: string, init?: RequestInit): Promise<Response> {\n // 1. Initial request\n const firstResp = await fetch(url, {\n ...init, headers: { Accept: \"application/json\", ...init?.headers },\n });\n if (firstResp.status !== 402) return firstResp;\n\n // 2. Parse 402\n const requirements = (await firstResp.json()) as any;\n const accept = requirements.accepts?.[0];\n if (!accept) throw new Error(\"[x402/crossmint] No payment requirements in 402 response\");\n\n // 3. Build transfer tx\n const unsignedTx = await buildTransferTx(connection, userPubkey, accept);\n\n // 4. Submit to Crossmint (signs + broadcasts)\n const txData = await crossmintApi(apiBase, apiKey,\n `${encodeURIComponent(wallet)}/transactions`,\n { params: { transaction: toBase58(unsignedTx.serialize()) } });\n\n // 5. Poll for on-chain signature\n const sig = await pollForSignature(apiBase, apiKey, wallet, txData.id, maxPolls, pollMs, txData);\n if (!sig) throw new Error(\"[x402/crossmint] Timed out waiting for tx confirmation\");\n\n // 6. Retry with X-PAYMENT\n const xPayment = buildXPaymentHeader(unsignedTx, sig, accept);\n return fetch(url, {\n ...init, headers: { Accept: \"application/json\", \"X-PAYMENT\": xPayment, ...init?.headers },\n });\n };\n}\n\n/**\n * **Delegated mode.** Create a fetch wrapper that auto-handles x402 `402`\n * responses using a Crossmint smart wallet with external signer approval.\n *\n * Each transaction requires cryptographic approval from `signerSecretKey`\n * before Crossmint broadcasts. This provides an extra security layer —\n * even with the API key, transactions cannot execute without the signer.\n *\n * The external signer must be registered on the Crossmint smart wallet\n * via the Crossmint console or API.\n */\nexport function createCrossmintDelegatedX402Fetch(config: CrossmintDelegatedX402Config) {\n const { apiKey, wallet, connection, signerSecretKey } = config;\n const apiBase = config.crossmintApiBase || detectApiBase(apiKey);\n const maxPolls = config.maxPollAttempts ?? 30;\n const pollMs = config.pollIntervalMs ?? 2000;\n const userPubkey = new PublicKey(wallet);\n const signerAddress = deriveSignerAddress(signerSecretKey);\n\n return async function fetch402(url: string, init?: RequestInit): Promise<Response> {\n // 1. Initial request\n const firstResp = await fetch(url, {\n ...init, headers: { Accept: \"application/json\", ...init?.headers },\n });\n if (firstResp.status !== 402) return firstResp;\n\n // 2. Parse 402\n const requirements = (await firstResp.json()) as any;\n const accept = requirements.accepts?.[0];\n if (!accept) throw new Error(\"[x402/crossmint] No payment requirements in 402 response\");\n\n // 3. Build transfer tx\n const unsignedTx = await buildTransferTx(connection, userPubkey, accept);\n\n // 4. Submit to Crossmint with external signer\n const createResp = await crossmintApi(apiBase, apiKey,\n `${encodeURIComponent(wallet)}/transactions`,\n {\n params: {\n transaction: toBase58(unsignedTx.serialize()),\n signer: {\n type: \"external-wallet\",\n locator: `external-wallet:${signerAddress}`,\n },\n },\n });\n\n const transactionId = createResp.id;\n const pendingApproval = createResp.approvals?.pending?.[0];\n\n if (!transactionId || !pendingApproval) {\n throw new Error(\n `[x402/crossmint] No pending approval returned. ` +\n `Ensure external signer ${signerAddress} is registered on wallet ${wallet}. ` +\n `Response: ${JSON.stringify(createResp)}`\n );\n }\n\n // 5. Sign the approval message\n const pendingSigner = pendingApproval.signer?.address\n ?? pendingApproval.signer?.locator?.split(\":\")[1]\n ?? signerAddress;\n\n const approvalSig = signMessage(pendingApproval.message, signerSecretKey);\n\n // 6. Submit approval\n const approvalResp = await crossmintApi(apiBase, apiKey,\n `${encodeURIComponent(wallet)}/transactions/${encodeURIComponent(transactionId)}/approvals`,\n {\n approvals: [{\n signer: `external-wallet:${pendingSigner}`,\n signature: approvalSig,\n }],\n });\n\n // 7. Poll for on-chain signature\n const sig = await pollForSignature(\n apiBase, apiKey, wallet, transactionId, maxPolls, pollMs, approvalResp);\n if (!sig) throw new Error(\"[x402/crossmint] Timed out waiting for tx confirmation\");\n\n // 8. Retry with X-PAYMENT\n const xPayment = buildXPaymentHeader(unsignedTx, sig, accept);\n return fetch(url, {\n ...init, headers: { Accept: \"application/json\", \"X-PAYMENT\": xPayment, ...init?.headers },\n });\n };\n}\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@relai-fi/x402",
3
- "version": "0.5.21",
3
+ "version": "0.5.22",
4
4
  "description": "Unified x402 payment SDK for Solana, Base, Avalanche, SKALE Base, SKALE BITE, Polygon, and Ethereum. Automatic 402 handling with zero gas fees.",
5
5
  "main": "dist/index.cjs",
6
6
  "module": "dist/index.js",
@@ -43,7 +43,8 @@
43
43
  "dependencies": {
44
44
  "@solana/spl-token": "^0.4.14",
45
45
  "@solana/web3.js": "^1.98.4",
46
- "axios": "^1.6.7"
46
+ "axios": "^1.6.7",
47
+ "tweetnacl": "^1.0.3"
47
48
  },
48
49
  "devDependencies": {
49
50
  "@types/node": "^20.19.24",