UNPKG

@anuragchvn-blip/mandatekit

Version:

Production-ready Web3 autopay SDK for crypto-based recurring payments using EIP-712 mandates

261 lines 8.34 kB
/** * MandateClient - Core client for creating and managing mandates * Factory-based approach with support for both Node.js and browser environments * @module client */ import { prepareMandateTypedData, recoverMandateSigner, generateNonce, generateMandateId, } from '../utils/crypto.js'; import { validateMandate, validateSignedMandate, validateMandateActive, addressesEqual, } from '../utils/validation.js'; /** * Creates a MandateClient instance using factory pattern * Supports both Node.js (with private key) and browser (with wallet) environments * * @param config - Client configuration * @returns Composable MandateClient functions * * @example * // Node.js with private key * ```typescript * import { createMandateClient } from '@mandatekit/sdk/client'; * import { privateKeyToAccount } from 'viem/accounts'; * * const account = privateKeyToAccount('0x...'); * const client = createMandateClient({ * domain: { * name: 'MandateRegistry', * version: '1', * chainId: 1, * verifyingContract: '0x...', * }, * account, * }); * * const signedMandate = await client.signMandate({ * subscriber: account.address, * token: '0xUSDC...', * amount: '10000000', * cadence: { interval: 'monthly', count: 1 }, * recipient: '0x456...', * validAfter: Math.floor(Date.now() / 1000), * validBefore: Math.floor(Date.now() / 1000) + 31536000, * }); * ``` * * @example * // Browser with wallet * ```typescript * import { createMandateClient } from '@mandatekit/sdk/client'; * import { createWalletClient, custom } from 'viem'; * * const walletClient = createWalletClient({ * chain: mainnet, * transport: custom(window.ethereum), * }); * * const [address] = await walletClient.getAddresses(); * const client = createMandateClient({ * domain: { ... }, * walletClient, * address, * }); * ``` */ export function createMandateClient(config) { const { domain, account, walletClient, address } = config; // Determine the signer address const signerAddress = account?.address ?? address ?? (() => { throw new Error('Either account or address must be provided'); })(); /** * Sign a mandate using the configured signer */ const signMandate = async (mandateParams) => { // Create full mandate with auto-generated nonce if not provided const mandate = { ...mandateParams, nonce: mandateParams.nonce ?? generateNonce(), }; // Validate mandate structure validateMandate(mandate); // Prepare EIP-712 typed data const typedData = prepareMandateTypedData(mandate, domain); let signature; // Sign with appropriate method if (account) { // Node.js: Use account.signTypedData signature = await account.signTypedData(typedData); } else if (walletClient) { // Browser: Use wallet client signature = await walletClient.signTypedData({ account: signerAddress, ...typedData, }); } else { throw new Error('No signing method available (account or walletClient required)'); } // Return signed mandate return { ...mandate, signature, signedAt: Math.floor(Date.now() / 1000), chainId: domain.chainId, }; }; /** * Verify a signed mandate's signature and validity */ const verifyMandate = async (signedMandate) => { const errors = []; let isValid = true; try { // Validate structure validateSignedMandate(signedMandate); } catch (error) { isValid = false; errors.push(`Validation failed: ${error.message}`); } // Verify chain ID matches if (signedMandate.chainId !== domain.chainId) { isValid = false; errors.push(`Chain ID mismatch: expected ${domain.chainId}, got ${signedMandate.chainId}`); } let recoveredSigner; try { // Recover signer from signature recoveredSigner = await recoverMandateSigner(signedMandate, signedMandate.signature, domain); // Verify signer matches subscriber if (!addressesEqual(recoveredSigner, signedMandate.subscriber)) { isValid = false; errors.push(`Signature verification failed: recovered signer ${recoveredSigner} does not match subscriber ${signedMandate.subscriber}`); } } catch (error) { isValid = false; errors.push(`Signature recovery failed: ${error.message}`); } // Check if mandate is active let isActive = true; let isWithinTimeRange = true; try { validateMandateActive(signedMandate); } catch (error) { isActive = false; isWithinTimeRange = false; errors.push(`Mandate inactive: ${error.message}`); } return { isValid, isActive, isWithinTimeRange, isExecutionDue: true, // Would need execution history to determine recoveredSigner, errors, }; }; // Return client interface return { getDomain: () => domain, getAddress: () => signerAddress, signMandate, prepareTypedData: (mandate) => prepareMandateTypedData(mandate, domain), verifyMandate, getMandateId: (mandate) => generateMandateId(mandate, domain), generateNonce, }; } /** * Standalone function to verify a mandate signature without a client instance * Useful for quick verification in stateless environments * * @param mandate - Mandate to verify * @param signature - Signature to verify * @param domain - EIP-712 domain * @param expectedSigner - Expected signer address (defaults to mandate.subscriber) * @returns Verification result * * @example * ```typescript * const isValid = await verifyMandateSignature( * mandate, * signature, * domain, * mandate.subscriber * ); * ``` */ export async function verifyMandateSignature(mandate, signature, domain, expectedSigner) { const errors = []; let isValid = true; try { validateMandate(mandate); } catch (error) { isValid = false; errors.push(`Validation failed: ${error.message}`); } let recoveredSigner; try { recoveredSigner = await recoverMandateSigner(mandate, signature, domain); const expected = expectedSigner ?? mandate.subscriber; if (!addressesEqual(recoveredSigner, expected)) { isValid = false; errors.push(`Signer mismatch: recovered ${recoveredSigner}, expected ${expected}`); } } catch (error) { isValid = false; errors.push(`Signature recovery failed: ${error.message}`); } let isActive = true; let isWithinTimeRange = true; try { validateMandateActive(mandate); } catch (error) { isActive = false; isWithinTimeRange = false; errors.push(`Mandate inactive: ${error.message}`); } return { isValid, isActive, isWithinTimeRange, isExecutionDue: true, recoveredSigner, errors, }; } /** * Batch sign multiple mandates efficiently * * @param client - MandateClient instance * @param mandates - Array of mandate parameters * @returns Array of signed mandates * * @example * ```typescript * const signedMandates = await batchSignMandates(client, [ * { subscriber: '0x...', token: '0x...', ... }, * { subscriber: '0x...', token: '0x...', ... }, * ]); * ``` */ export async function batchSignMandates(client, mandates) { return Promise.all(mandates.map(m => client.signMandate(m))); } /** * Batch verify multiple signed mandates * * @param client - MandateClient instance * @param signedMandates - Array of signed mandates to verify * @returns Array of verification results */ export async function batchVerifyMandates(client, signedMandates) { return Promise.all(signedMandates.map(m => client.verifyMandate(m))); } //# sourceMappingURL=index.js.map