@solana/pay
Version:
`@solana/pay` is a JavaScript library for facilitating commerce on Solana by using a token transfer URL scheme. The URL scheme ensures that no matter the wallet or service used, the payment request must be created and interpreted in one standard way.
732 lines (716 loc) • 29.1 kB
JavaScript
// src/constants.ts
import { MEMO_PROGRAM_ADDRESS } from "@solana-program/memo";
import { TOKEN_2022_PROGRAM_ADDRESS } from "@solana-program/token-2022";
var SOLANA_PROTOCOL = "solana:";
var HTTPS_PROTOCOL = "https:";
var SOL_DECIMALS = 9;
// src/client.ts
import { createClient } from "@solana/kit";
import { planAndSendTransactions } from "@solana/kit-plugin-instruction-plan";
import { rpcTransactionPlanExecutor, rpcTransactionPlanner, solanaRpcConnection } from "@solana/kit-plugin-rpc";
import { payer } from "@solana/kit-plugin-signer";
// src/plugins/merchant.ts
import {
appendTransactionMessageInstructions,
compileTransaction,
createTransactionMessage,
getBase64EncodedWireTransaction,
pipe,
setTransactionMessageFeePayer
} from "@solana/kit";
// src/createQR.ts
import QRCodeStylingModule from "@solana/qr-code-styling";
var QRCodeStyling = typeof QRCodeStylingModule.default === "function" ? QRCodeStylingModule.default : QRCodeStylingModule;
function createQR(url, size = 512, background = "white", color = "black") {
return new QRCodeStyling(createQROptions(url, size, background, color));
}
function createQROptions(url, size = 512, background = "white", color = "black") {
return {
type: "svg",
width: size,
height: size,
data: String(url),
margin: 16,
qrOptions: {
typeNumber: 0,
mode: "Byte",
errorCorrectionLevel: "Q"
},
backgroundOptions: { color: background },
dotsOptions: { type: "extra-rounded", color },
cornersSquareOptions: {
type: "extra-rounded",
color
},
cornersDotOptions: { type: "square", color },
imageOptions: { hideBackgroundDots: true, imageSize: 0.15, margin: 8 },
image: `data:image/svg+xml;utf8,<svg fill="${encodeURIComponent(
color
)}" height="16" viewBox="0 0 16 14" width="16" xmlns="http://www.w3.org/2000/svg"><path d="m15.9176 11.038-2.6413 2.7713c-.0574.0602-.1269.1082-.2041.141s-.1604.0497-.2446.0497h-12.520966c-.059744 0-.118187-.0171-.168147-.0491-.0499596-.0321-.0892609-.0777-.1130745-.1313-.02381372-.0536-.03110249-.1129-.02097081-.1705.01013171-.0576.03724251-.111.07800141-.1538l2.6432769-2.7713c.05726-.06.12651-.1079.20346-.1407s.15996-.0498.2439-.05h12.52032c.0597 0 .1182.0171.1681.0492.05.032.0893.0776.1131.1313.0238.0536.0311.1128.021.1704-.0102.0576-.0373.1111-.078.1538zm-2.6413-5.58067c-.0574-.0602-.1269-.1082-.2041-.141s-.1604-.04971-.2446-.04966h-12.520966c-.059744 0-.118187.01708-.168147.04913-.0499596.03205-.0892609.07768-.1130745.13129-.02381372.0536-.03110249.11285-.02097081.17045.01013171.05761.03724251.11106.07800141.15379l2.6432769 2.77134c.05726.06004.12651.10794.20346.14073.07695.0328.15996.04979.2439.04993h12.52032c.0597 0 .1182-.01707.1681-.04913.05-.03205.0893-.07768.1131-.13129.0238-.0536.0311-.11285.021-.17045-.0102-.05761-.0373-.11106-.078-.15379zm-12.969666-1.99066h12.520966c.0842.00004.1674-.01687.2446-.04967s.1467-.0808.2041-.141l2.6413-2.771333c.0407-.042736.0678-.096189.078-.153792.0101-.057603.0028-.116847-.021-.170453s-.0631-.0992385-.1131-.1312911c-.0499-.0320526-.1084-.04912893-.1681-.0491309h-12.52032c-.08394.00013975-.16695.0171339-.2439.0499304s-.1462.0806976-.20346.1407366l-2.6425955 2.771333c-.0407196.04269-.0678184.09609-.07797306.15363-.01015467.05754-.00292373.11673.02080606.17031.0237297.05358.0629266.09922.1127835.13132.049857.03211.108207.04928.167893.04941z"/></svg>`
};
}
// src/utils/reference.ts
function normalizeReferences(reference) {
if (!reference) return void 0;
return Array.isArray(reference) ? reference : [reference];
}
// src/encodeURL.ts
function encodeURL(fields) {
return "link" in fields ? encodeTransactionRequestURL(fields) : encodeTransferRequestURL(fields);
}
function encodeTransactionRequestURL({ link, label, message }) {
const pathname = link.search ? encodeURIComponent(String(link).replace(/\/\?/, "?")) : String(link).replace(/\/$/, "");
const url = new URL(SOLANA_PROTOCOL + pathname);
if (label) {
url.searchParams.append("label", label);
}
if (message) {
url.searchParams.append("message", message);
}
return url;
}
function encodeTransferRequestURL({
recipient,
amount,
splToken,
reference,
label,
message,
memo
}) {
const pathname = recipient;
const url = new URL(SOLANA_PROTOCOL + pathname);
if (amount != null) {
url.searchParams.append("amount", amount.toFixed(10).replace(/0+$/, "").replace(/\.$/, ""));
}
if (splToken) {
url.searchParams.append("spl-token", splToken);
}
const refs = normalizeReferences(reference);
if (refs) {
for (const ref of refs) {
url.searchParams.append("reference", ref);
}
}
if (label) {
url.searchParams.append("label", label);
}
if (message) {
url.searchParams.append("message", message);
}
if (memo) {
url.searchParams.append("memo", memo);
}
return url;
}
// src/findReference.ts
var FindReferenceError = class extends Error {
name = "FindReferenceError";
};
async function findReference(rpc, reference, { commitment, ...options } = {}) {
const signatures = await rpc.getSignaturesForAddress(reference, { ...options, ...commitment ? { commitment } : {} }).send();
const length = signatures.length;
if (!length) throw new FindReferenceError("not found");
const oldest = signatures[length - 1];
if (length < (options?.limit || 1e3)) return oldest;
try {
return await findReference(rpc, reference, {
commitment,
...options,
before: oldest.signature
});
} catch (error) {
if (error instanceof FindReferenceError) return oldest;
throw error;
}
}
// src/validateTransfer.ts
import {
decompileTransactionMessage,
getBase64Codec,
getCompiledTransactionMessageCodec,
getTransactionCodec
} from "@solana/kit";
import { parseAddMemoInstruction } from "@solana-program/memo";
import {
identifySystemInstruction,
parseTransferSolInstruction,
SYSTEM_PROGRAM_ADDRESS,
SystemInstruction
} from "@solana-program/system";
import {
findAssociatedTokenPda,
identifyTokenInstruction,
parseTransferCheckedInstruction,
parseTransferInstruction,
TOKEN_PROGRAM_ADDRESS,
TokenInstruction
} from "@solana-program/token";
// src/utils/amount.ts
function amountToBaseUnits(amount, decimals) {
if (!Number.isFinite(amount) || amount < 0) {
throw new Error(`Invalid amount: ${amount}`);
}
if (!Number.isInteger(decimals) || decimals < 0) {
throw new Error(`Invalid decimals: ${decimals}`);
}
const str = amount.toFixed(decimals);
const [whole, frac] = str.split(".");
const significantDigits = whole.replace(/^0+/, "").length + (frac?.replace(/0+$/, "").length ?? 0);
if (significantDigits > 15) {
throw new Error(
`Amount ${amount} with ${decimals} decimals exceeds safe floating-point precision (${significantDigits} significant digits > 15). Use a smaller amount or fewer decimals to avoid silent precision loss.`
);
}
return BigInt(whole + (frac ?? ""));
}
function decimalPlaces(n) {
const s = n.toString();
if (s.includes("e-")) {
const [coeff, exp] = s.split("e-");
const coeffDecimals = (coeff.split(".")[1] || "").length;
return coeffDecimals + Number(exp);
}
const dot = s.indexOf(".");
return dot === -1 ? 0 : s.length - dot - 1;
}
// src/validateTransfer.ts
var ValidateTransferError = class extends Error {
name = "ValidateTransferError";
};
function validateAmount(amount) {
if (!Number.isFinite(amount) || amount < 0) {
throw new ValidateTransferError("amount invalid");
}
}
function parseBase64Transaction(b64TransactionResponse) {
const [base64Transaction] = b64TransactionResponse;
const transactionBytes = getBase64Codec().encode(base64Transaction);
const transaction = getTransactionCodec().decode(transactionBytes);
const compiledMessage = getCompiledTransactionMessageCodec().decode(transaction.messageBytes);
const decompiledMessage = decompileTransactionMessage(compiledMessage);
const { staticAccounts } = compiledMessage;
const instructions = [...decompiledMessage.instructions];
return { instructions, staticAccounts };
}
function getMeta(meta) {
if (!meta) throw new ValidateTransferError("missing meta");
if (meta.err) throw new ValidateTransferError(JSON.stringify(meta.err));
return meta;
}
function validateInstruction(instruction) {
if (!instruction) {
throw new ValidateTransferError("missing instruction");
}
if (!instruction.accounts) {
throw new ValidateTransferError("missing instruction accounts");
}
if (!instruction.data) {
throw new ValidateTransferError("missing instruction data");
}
}
async function validateTransfer(rpc, signature, { recipient, amount, splToken, reference, memo }, options) {
validateAmount(amount);
const refs = normalizeReferences(reference);
const response = await rpc.getTransaction(signature, {
commitment: options?.commitment ?? "confirmed",
maxSupportedTransactionVersion: 0,
encoding: "base64"
}).send();
if (!response) throw new ValidateTransferError("not found");
const meta = getMeta(response.meta);
const { instructions, staticAccounts } = parseBase64Transaction(response.transaction);
const instruction = instructions.pop();
validateInstruction(instruction);
const { pre, post, decimals } = splToken ? await validateSPLTokenTransfer(instruction, staticAccounts, meta, recipient, splToken, refs) : validateSystemTransfer(instruction, staticAccounts, meta, recipient, refs);
const expected = amountToBaseUnits(amount, decimals);
if (post - pre < expected) throw new ValidateTransferError("amount not transferred");
if (memo !== void 0) {
validateMemo(instructions.pop(), memo);
}
return response;
}
function validateMemo(instruction, memo) {
if (!instruction) throw new ValidateTransferError("missing memo instruction");
if (instruction.programAddress !== MEMO_PROGRAM_ADDRESS) {
throw new ValidateTransferError("invalid memo program");
}
if (!instruction.data) throw new ValidateTransferError("invalid memo");
const parsed = parseAddMemoInstruction(instruction);
if (parsed.data.memo !== memo) throw new ValidateTransferError("invalid memo");
}
function validateProgram(instruction, validPrograms) {
if (!validPrograms.includes(instruction.programAddress)) {
throw new ValidateTransferError("invalid transfer");
}
}
function validateReferences(instruction, requiredAccounts, references) {
if (!references) return;
const extraAccounts = instruction.accounts.slice(requiredAccounts);
if (extraAccounts.length !== references.length) throw new ValidateTransferError("invalid references");
for (let i = 0; i < extraAccounts.length; i++) {
if (extraAccounts[i].address !== references[i]) throw new ValidateTransferError(`invalid reference ${i}`);
}
}
function validateSystemTransfer(instruction, staticAccounts, meta, recipient, references) {
validateProgram(instruction, [SYSTEM_PROGRAM_ADDRESS]);
const instructionType = identifySystemInstruction(instruction);
if (instructionType !== SystemInstruction.TransferSol) {
throw new ValidateTransferError("invalid transfer");
}
const parsed = parseTransferSolInstruction(instruction);
if (parsed.accounts.destination.address !== recipient) {
throw new ValidateTransferError("invalid transfer");
}
validateReferences(instruction, Object.keys(parsed.accounts).length, references);
const accountIndex = staticAccounts.indexOf(recipient);
if (accountIndex === -1) throw new ValidateTransferError("recipient not found");
const pre = meta.preBalances[accountIndex];
const post = meta.postBalances[accountIndex];
if (pre === void 0 || post === void 0) throw new ValidateTransferError("missing balance data");
return { pre, post, decimals: SOL_DECIMALS };
}
async function validateSPLTokenTransfer(instruction, staticAccounts, meta, recipient, splToken, references) {
validateProgram(instruction, [TOKEN_PROGRAM_ADDRESS, TOKEN_2022_PROGRAM_ADDRESS]);
const instructionType = identifyTokenInstruction(instruction);
const [recipientATA] = await findAssociatedTokenPda({
owner: recipient,
tokenProgram: instruction.programAddress,
mint: splToken
});
let requiredAccounts;
switch (instructionType) {
case TokenInstruction.TransferChecked: {
const parsed = parseTransferCheckedInstruction(instruction);
if (parsed.accounts.destination.address !== recipientATA) {
throw new ValidateTransferError("invalid transfer");
}
if (parsed.accounts.mint.address !== splToken) {
throw new ValidateTransferError("invalid transfer");
}
requiredAccounts = Object.keys(parsed.accounts).length;
break;
}
case TokenInstruction.Transfer: {
const parsed = parseTransferInstruction(instruction);
if (parsed.accounts.destination.address !== recipientATA) {
throw new ValidateTransferError("invalid transfer");
}
requiredAccounts = Object.keys(parsed.accounts).length;
break;
}
default:
throw new ValidateTransferError("invalid transfer instruction");
}
validateReferences(instruction, requiredAccounts, references);
const accountIndex = staticAccounts.indexOf(recipientATA);
if (accountIndex === -1) throw new ValidateTransferError("recipient not found");
const preBalance = meta.preTokenBalances?.find((x) => x.accountIndex === accountIndex);
const postBalance = meta.postTokenBalances?.find((x) => x.accountIndex === accountIndex);
if (!preBalance || !postBalance) throw new ValidateTransferError("missing balance data");
return {
pre: BigInt(preBalance.uiTokenAmount.amount),
post: BigInt(postBalance.uiTokenAmount.amount),
decimals: preBalance.uiTokenAmount.decimals
};
}
// src/watchReference.ts
async function watchReference(rpcSubscriptions, reference, options) {
const { commitment = "confirmed", abortSignal: externalSignal } = options ?? {};
if (externalSignal?.aborted) {
throw new FindReferenceError("aborted");
}
const abortController = new AbortController();
if (externalSignal) {
externalSignal.addEventListener("abort", () => abortController.abort(externalSignal.reason), { once: true });
}
try {
const subscription = await rpcSubscriptions.logsNotifications({ mentions: [reference] }, { commitment }).subscribe({ abortSignal: abortController.signal });
for await (const notification of subscription) {
return {
signature: notification.value.signature,
err: notification.value.err
};
}
} catch (error) {
if (abortController.signal.aborted) {
throw new FindReferenceError("aborted");
}
throw error;
} finally {
abortController.abort();
}
throw new FindReferenceError("not found");
}
// src/plugins/merchant.ts
function solanaPayMerchant() {
return function installMerchant(client) {
const existingPay = "pay" in client ? client.pay : {};
const pay = {
...existingPay,
encodeURL(fields) {
return encodeURL(fields);
},
createQR(url, size, background, color) {
return createQR(url, size, background, color);
},
createQROptions(url, size, background, color) {
return createQROptions(url, size, background, color);
},
async findReference(reference, options) {
return await findReference(client.rpc, reference, options);
},
async watchReference(reference, options) {
return await watchReference(client.rpcSubscriptions, reference, options);
},
async validateTransfer(signature, fields, options) {
return await validateTransfer(client.rpc, signature, fields, options);
},
buildTransaction(feePayer, instructions) {
const tx = pipe(
createTransactionMessage({ version: 0 }),
(m) => setTransactionMessageFeePayer(feePayer, m),
(m) => appendTransactionMessageInstructions(instructions, m)
);
return getBase64EncodedWireTransaction(compileTransaction(tx));
}
};
return Object.freeze({ ...client, pay });
};
}
// src/createTransfer.ts
import {
AccountRole
} from "@solana/kit";
import { getAddMemoInstruction } from "@solana-program/memo";
import { getTransferSolInstruction, SYSTEM_PROGRAM_ADDRESS as SYSTEM_PROGRAM_ADDRESS2 } from "@solana-program/system";
import {
AccountState,
fetchMint,
fetchToken,
findAssociatedTokenPda as findAssociatedTokenPda2,
getTransferCheckedInstruction,
TOKEN_PROGRAM_ADDRESS as TOKEN_PROGRAM_ADDRESS2
} from "@solana-program/token";
var CreateTransferError = class extends Error {
name = "CreateTransferError";
};
async function createTransfer(rpc, sender, { recipient, amount, splToken, reference, memo }) {
const instructions = [];
if (memo != null) {
instructions.push(getAddMemoInstruction({ memo, signers: [sender] }));
}
const transferInstruction = splToken ? await createSPLTokenInstruction(recipient, amount, splToken, sender, rpc) : await createSystemInstruction(recipient, amount, sender, rpc);
const refs = normalizeReferences(reference);
if (refs) {
const existingAccounts = transferInstruction.accounts ?? [];
const refAccounts = refs.map((ref) => ({
address: ref,
role: AccountRole.READONLY
}));
const updatedInstruction = {
...transferInstruction,
accounts: [...existingAccounts, ...refAccounts]
};
instructions.push(updatedInstruction);
} else {
instructions.push(transferInstruction);
}
return instructions;
}
async function createSystemInstruction(recipient, amount, sender, rpc) {
const {
value: [senderInfo, recipientInfo]
} = await rpc.getMultipleAccounts([sender.address, recipient], { encoding: "base64" }).send();
if (!senderInfo) throw new CreateTransferError("sender not found");
if (senderInfo.owner !== SYSTEM_PROGRAM_ADDRESS2) throw new CreateTransferError("sender owner invalid");
if (senderInfo.executable) throw new CreateTransferError("sender executable");
if (recipientInfo) {
if (recipientInfo.owner !== SYSTEM_PROGRAM_ADDRESS2) throw new CreateTransferError("recipient owner invalid");
if (recipientInfo.executable) throw new CreateTransferError("recipient executable");
}
if (decimalPlaces(amount) > SOL_DECIMALS) throw new CreateTransferError("amount decimals invalid");
const lamports = amountToBaseUnits(amount, SOL_DECIMALS);
if (lamports > senderInfo.lamports) throw new CreateTransferError("insufficient funds");
return getTransferSolInstruction({
source: sender,
destination: recipient,
amount: lamports
});
}
async function createSPLTokenInstruction(recipient, amount, splToken, sender, rpc) {
let mint;
try {
mint = await fetchMint(rpc, splToken);
} catch {
throw new CreateTransferError("mint account not found");
}
if (!mint.data.isInitialized) throw new CreateTransferError("mint not initialized");
const tokenProgram = mint.programAddress === TOKEN_2022_PROGRAM_ADDRESS ? TOKEN_2022_PROGRAM_ADDRESS : TOKEN_PROGRAM_ADDRESS2;
if (decimalPlaces(amount) > mint.data.decimals) throw new CreateTransferError("amount decimals invalid");
const tokens = amountToBaseUnits(amount, mint.data.decimals);
const [[senderATA], [recipientATA]] = await Promise.all([
findAssociatedTokenPda2({ owner: sender.address, tokenProgram, mint: splToken }),
findAssociatedTokenPda2({ owner: recipient, tokenProgram, mint: splToken })
]);
const [senderAccount, recipientAccount] = await Promise.all([
fetchToken(rpc, senderATA),
fetchToken(rpc, recipientATA)
]);
if (senderAccount.data.state === AccountState.Uninitialized)
throw new CreateTransferError("sender not initialized");
if (senderAccount.data.state === AccountState.Frozen) throw new CreateTransferError("sender frozen");
if (recipientAccount.data.state === AccountState.Uninitialized)
throw new CreateTransferError("recipient not initialized");
if (recipientAccount.data.state === AccountState.Frozen) throw new CreateTransferError("recipient frozen");
if (tokens > senderAccount.data.amount) throw new CreateTransferError("insufficient funds");
return getTransferCheckedInstruction(
{
source: senderATA,
mint: splToken,
destination: recipientATA,
authority: sender,
amount: tokens,
decimals: mint.data.decimals
},
{ programAddress: tokenProgram }
);
}
// src/fetchTransaction.ts
import { assertIsSignatureBytes, verifySignature } from "@solana/keys";
import {
address,
compileTransaction as compileTransaction2,
decompileTransactionMessage as decompileTransactionMessage2,
getAddressEncoder,
getBase64Encoder,
getCompiledTransactionMessageDecoder,
getTransactionDecoder,
setTransactionMessageFeePayer as setTransactionMessageFeePayer2,
setTransactionMessageLifetimeUsingBlockhash
} from "@solana/kit";
var FetchTransactionError = class extends Error {
name = "FetchTransactionError";
};
async function fetchTransaction(rpc, account, link, { commitment } = {}) {
let response;
try {
response = await fetch(String(link), {
method: "POST",
mode: "cors",
credentials: "omit",
headers: {
"Cache-Control": "no-cache",
Accept: "application/json",
"Content-Type": "application/json"
},
body: JSON.stringify({ account })
});
} catch (error) {
throw new FetchTransactionError(`network error: ${error instanceof Error ? error.message : String(error)}`);
}
if (!response.ok) throw new FetchTransactionError(`request failed: ${response.status}`);
let json;
try {
json = await response.json();
} catch {
throw new FetchTransactionError("response is not valid JSON");
}
if (!json?.transaction) throw new FetchTransactionError("missing transaction");
if (typeof json.transaction !== "string") throw new FetchTransactionError("invalid transaction");
let transactionBytes;
try {
transactionBytes = getBase64Encoder().encode(json.transaction);
} catch {
throw new FetchTransactionError("invalid base64 in transaction");
}
let transaction;
try {
transaction = getTransactionDecoder().decode(transactionBytes);
} catch {
throw new FetchTransactionError("failed to decode transaction wire format");
}
let compiledMessage;
try {
compiledMessage = getCompiledTransactionMessageDecoder().decode(transaction.messageBytes);
} catch {
throw new FetchTransactionError("failed to decode compiled transaction message");
}
const signatures = transaction.signatures;
const signerAddresses = Object.keys(signatures).map((addr) => address(addr));
const hasSignatures = signerAddresses.some((addr) => {
const sig = signatures[addr];
return sig != null && !sig.every((b) => b === 0);
});
if (hasSignatures) {
const feePayer = signerAddresses[0];
if (!feePayer) throw new FetchTransactionError("missing fee payer");
if (compiledMessage.staticAccounts.length === 0 || compiledMessage.staticAccounts[0] !== feePayer) {
throw new FetchTransactionError("invalid fee payer");
}
if (!compiledMessage.lifetimeToken) {
throw new FetchTransactionError("missing recent blockhash");
}
const addressEncoder = getAddressEncoder();
for (const addr of signerAddresses) {
const sig = signatures[addr];
const isNonZero = sig != null && !sig.every((b) => b === 0);
if (isNonZero) {
const publicKeyBytes = addressEncoder.encode(addr);
const cryptoKey = await crypto.subtle.importKey("raw", publicKeyBytes, { name: "Ed25519" }, false, [
"verify"
]);
assertIsSignatureBytes(sig);
const isValid = await verifySignature(cryptoKey, sig, transaction.messageBytes);
if (!isValid) throw new FetchTransactionError("invalid signature");
} else if (addr === account) {
if (signerAddresses.length === 1) {
const { value } = await rpc.getLatestBlockhash({ commitment }).send();
const msg = decompileTransactionMessage2(compiledMessage);
const updatedMsg = setTransactionMessageLifetimeUsingBlockhash(value, msg);
return compileTransaction2(updatedMsg);
}
} else {
throw new FetchTransactionError("missing signature");
}
}
return transaction;
} else {
const { value } = await rpc.getLatestBlockhash({ commitment }).send();
const msg = decompileTransactionMessage2(compiledMessage);
const withFeePayer = setTransactionMessageFeePayer2(account, msg);
const withLifetime = setTransactionMessageLifetimeUsingBlockhash(value, withFeePayer);
return compileTransaction2(withLifetime);
}
}
// src/parseURL.ts
import { address as address2 } from "@solana/kit";
var ParseURLError = class extends Error {
name = "ParseURLError";
};
function parseURL(url) {
if (typeof url === "string") {
if (url.length > 2048) throw new ParseURLError("length invalid");
url = new URL(url);
}
if (url.protocol !== SOLANA_PROTOCOL) throw new ParseURLError("protocol invalid");
if (!url.pathname) throw new ParseURLError("pathname missing");
return /[:%]/.test(url.pathname) ? parseTransactionRequestURL(url) : parseTransferRequestURL(url);
}
function parseTransactionRequestURL({ pathname, searchParams }) {
const link = new URL(decodeURIComponent(pathname));
if (link.protocol !== HTTPS_PROTOCOL) throw new ParseURLError("link invalid");
const label = searchParams.get("label") || void 0;
const message = searchParams.get("message") || void 0;
return {
link,
label,
message
};
}
function parseTransferRequestURL({ pathname, searchParams }) {
let recipient;
try {
recipient = address2(pathname);
} catch {
throw new ParseURLError("recipient invalid");
}
let amount;
const amountParam = searchParams.get("amount");
if (amountParam != null) {
if (!/^\d+(\.\d+)?$/.test(amountParam)) throw new ParseURLError("amount invalid");
amount = Number(amountParam);
if (Number.isNaN(amount)) throw new ParseURLError("amount NaN");
if (amount < 0) throw new ParseURLError("amount negative");
}
let splToken;
const splTokenParam = searchParams.get("spl-token");
if (splTokenParam != null) {
try {
splToken = address2(splTokenParam);
} catch {
throw new ParseURLError("spl-token invalid");
}
}
let reference;
const referenceParams = searchParams.getAll("reference");
if (referenceParams.length) {
try {
reference = referenceParams.map((ref) => address2(ref));
} catch {
throw new ParseURLError("reference invalid");
}
}
const label = searchParams.get("label") || void 0;
const message = searchParams.get("message") || void 0;
const memo = searchParams.get("memo") || void 0;
return {
recipient,
amount,
splToken,
reference,
label,
message,
memo
};
}
// src/plugins/wallet.ts
function solanaPayWallet() {
return function installWallet(client) {
const existingPay = "pay" in client ? client.pay : {};
const pay = {
...existingPay,
parseURL(url) {
return parseURL(url);
},
async createTransfer(fields, sender) {
const signer = sender ?? client.payer;
if (!signer) {
throw new Error("solanaPayWallet.createTransfer requires a sender or client.payer");
}
return await createTransfer(client.rpc, signer, fields);
},
async fetchTransaction(account, link, options) {
return await fetchTransaction(client.rpc, account, link, options);
}
};
return Object.freeze({ ...client, pay });
};
}
// src/client.ts
function createMerchantClient(config) {
return createClient().use(solanaRpcConnection({ rpcUrl: config.rpcUrl, rpcSubscriptionsConfig: config.rpcSubscriptionsConfig })).use(solanaPayMerchant());
}
function createWalletClient(config) {
return createClient().use(solanaRpcConnection({ rpcUrl: config.rpcUrl, rpcSubscriptionsConfig: config.rpcSubscriptionsConfig })).use(payer(config.payer)).use(rpcTransactionPlanner()).use(rpcTransactionPlanExecutor()).use(planAndSendTransactions()).use(solanaPayWallet());
}
function createSolanaPayClient(config) {
return createClient().use(solanaRpcConnection({ rpcUrl: config.rpcUrl, rpcSubscriptionsConfig: config.rpcSubscriptionsConfig })).use(payer(config.payer)).use(rpcTransactionPlanner()).use(rpcTransactionPlanExecutor()).use(planAndSendTransactions()).use(solanaPayMerchant()).use(solanaPayWallet());
}
export {
CreateTransferError,
FetchTransactionError,
FindReferenceError,
HTTPS_PROTOCOL,
MEMO_PROGRAM_ADDRESS,
ParseURLError,
SOLANA_PROTOCOL,
SOL_DECIMALS,
TOKEN_2022_PROGRAM_ADDRESS,
ValidateTransferError,
createMerchantClient,
createQR,
createQROptions,
createSolanaPayClient,
createTransfer,
createWalletClient,
encodeURL,
fetchTransaction,
findReference,
parseURL,
solanaPayMerchant,
solanaPayWallet,
validateTransfer,
watchReference
};
//# sourceMappingURL=index.js.map