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.

Nonce and deadline for a new conversionjavascript
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.

Domain and Issuance typetypescript
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#

Redemption typetypescript
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.

Wallet authorization typestypescript
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" }
  ]
};
Sign the issuer action with viemjavascript
// 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.