@anuragchvn-blip/mandatekit
Version:
Production-ready Web3 autopay SDK for crypto-based recurring payments using EIP-712 mandates
261 lines • 8.34 kB
JavaScript
/**
* 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