Docs/Protocol reference/Signing
Signing
How brand wallets sign issuance, redemption, and admin messages before anyone can submit them.
Domain#
The signature is bound to Loyfin, one chain, and one verifying contract.
Prepare and persist the action on your backend before signing. Nonces are unique per issuer across both issuance and redemption. An HTTP retry must reuse the stored nonce, deadline, and signed payload.
import { randomBytes } from "node:crypto";
// Generate once for a NEW conversion, then persist before signing.
const nonce = "0x" + randomBytes(32).toString("hex");
const deadline = String(Math.floor(Date.now() / 1000) + 15 * 60);
// Fifteen minutes is an example policy. Reuse these stored values on HTTP retry.
// expiresAt is the points' expiry; deadline is this signed action's execution limit.expiresAt is the points' expiry. deadline is the latest time this action can execute. Both use Unix seconds. A value of 0 disables the corresponding expiry; a zero deadline cannot use deadline-based reservation release.
const domain = {
name: "Loyfin",
version: "1",
chainId: 8453,
verifyingContract: loyfinFactoryAddress
};
const types = {
Metadata: [
{ name: "loyaltyId", type: "bytes32" },
{ name: "name", type: "string" },
{ name: "symbol", type: "string" },
{ name: "media", type: "string" },
{ name: "description", type: "string" }
],
Issuance: [
{ name: "issuer", type: "address" },
{ name: "loyaltyId", type: "bytes32" },
{ name: "amount", type: "uint256" },
{ name: "expiresAt", type: "uint256" },
{ name: "deadline", type: "uint256" },
{ name: "nonce", type: "bytes32" },
{ name: "chainId", type: "uint256" },
{ name: "verifyingContract", type: "address" },
{ name: "operationHash", type: "bytes32" },
{ name: "metadata", type: "Metadata" },
{ name: "data", type: "bytes" }
]
};Issuance type#
Issuance signatures include metadata because the first tokenization can create the Loyalty Token. Issuance and redemption signatures also include deadline; 0 means the signed action does not expire.
Redemption type#
const types = {
Redemption: [
{ name: "issuer", type: "address" },
{ name: "loyaltyId", type: "bytes32" },
{ name: "amount", type: "uint256" },
{ name: "expiresAt", type: "uint256" },
{ name: "deadline", type: "uint256" },
{ name: "nonce", type: "bytes32" },
{ name: "chainId", type: "uint256" },
{ name: "verifyingContract", type: "address" },
{ name: "operationHash", type: "bytes32" },
{ name: "data", type: "bytes" }
]
};Recipient and holder authorization#
Checkout sessions use two signatures. The issuer signs the issuance or redemption terms without choosing the customer wallet. The customer then signs an IssuanceRecipientAuthorization or RedemptionHolderAuthorization. Its actionDigest is the EIP-712 digest of the issuer-signed action, binding the selected wallet to those exact terms, chain, factory, and deadline.
const issuanceRecipientAuthorizationTypes = {
IssuanceRecipientAuthorization: [
{ name: "to", type: "address" },
{ name: "actionDigest", type: "bytes32" },
{ name: "chainId", type: "uint256" },
{ name: "verifyingContract", type: "address" },
{ name: "deadline", type: "uint256" }
]
};
const redemptionHolderAuthorizationTypes = {
RedemptionHolderAuthorization: [
{ name: "from", type: "address" },
{ name: "actionDigest", type: "bytes32" },
{ name: "chainId", type: "uint256" },
{ name: "verifyingContract", type: "address" },
{ name: "deadline", type: "uint256" }
]
};// Server-side viem Wallet Client backed by your issuer signer.
// action is the issuance or redemption object; types is its EIP-712 type map above.
export async function signAction(issuerSigner, action, types, primaryType) {
return issuerSigner.signTypedData({
account: issuerSigner.account,
domain: {
name: "Loyfin", version: "1",
chainId: Number(action.chainId), verifyingContract: action.verifyingContract,
},
types,
primaryType, // "Issuance" or "Redemption"
message: {
...action, amount: BigInt(action.amount), expiresAt: BigInt(action.expiresAt),
deadline: BigInt(action.deadline), chainId: BigInt(action.chainId),
},
});
}
// POST the original JSON action plus the returned signature.
// Keep the issuer signer and Loyfin API key on your backend.For wallet selection, signer changes, and pending signatures, see Issuer keys and rotation.