UNPKG

pipegate-sdk

Version:

A TypeScript client-side payment authentication SDK for stablecoins used with axios

349 lines (348 loc) 13.6 kB
import { channelFactoryABI } from "./abi/channelFactory.js"; import "dotenv/config"; import { createPublicClient, createWalletClient, decodeEventLog, encodePacked, erc20Abi, http, keccak256, parseUnits, toBytes, toHex, } from "viem"; import { formatAxiosError } from "./utils/index.js"; import axios from "axios"; import { privateKeyToAccount } from "viem/accounts"; import { baseSepolia } from "viem/chains"; import { ChannelFactoryAddress } from "./constants/address.js"; export class ClientInterceptor { nonceMap = new Map(); channelStates = new Map(); account; constructor() { const privateKey = process.env.WALLET_PRIVATE_KEY; if (!privateKey) { throw new Error("WALLET_PRIVATE_KEY environment variable is required"); } //Intialisaing Account with viem this.account = privateKeyToAccount(privateKey); console.log("Account connected with address", this.account.address); } /** * Create a new payment channel * @param params CreateChannelParams - recipient, duration, tokenAddress, amount * @returns CreateChannelResponse - channelId, channelAddress, sender, recipient, duration, tokenAddress, amount, price, timestamp */ async createPaymentChannel(params) { try { console.log("Creating payment channel with params:", params); const publicClient = createPublicClient({ chain: baseSepolia, transport: http(), }); const walletClient = createWalletClient({ chain: baseSepolia, transport: http(), account: this.account, }); const tokenDecimals = await publicClient.readContract({ address: params.tokenAddress, abi: erc20Abi, functionName: "decimals", }); const approveTxHash = await walletClient.writeContract({ abi: erc20Abi, address: params.tokenAddress, functionName: "approve", args: [ ChannelFactoryAddress, parseUnits(params.amount.toString(), tokenDecimals), ], }); await publicClient.waitForTransactionReceipt({ hash: approveTxHash, }); // wait for 2 seconds after the approve transaction has went through await new Promise((resolve) => setTimeout(resolve, 2000)); const data = await publicClient.simulateContract({ account: this.account, address: ChannelFactoryAddress, abi: channelFactoryABI, functionName: "createChannel", args: [ params.recipient, BigInt(params.duration), params.tokenAddress, parseUnits(params.amount.toString(), tokenDecimals), ], }); const txHash = await walletClient.writeContract(data.request); console.log("Transaction sent:", txHash); const receipt = await publicClient.waitForTransactionReceipt({ hash: txHash, }); const event = receipt.logs.find((log) => log.address.toLowerCase() == ChannelFactoryAddress.toLowerCase()); if (!event) { throw new Error("Channel creation event not found"); } const eventTopics = decodeEventLog({ abi: channelFactoryABI, data: event.data, topics: event.topics, }); if (eventTopics.eventName != "channelCreated") { throw new Error("Channel ID not found in event logs"); } console.log("Channel created:", eventTopics.args); return eventTopics.args; } catch (err) { if (axios.isAxiosError(err)) { console.error(formatAxiosError(err)); } else { // @ts-ignore console.error("Error:", err.message); } throw err; } } /** * * @param channelId channel id * @param channelState channel state */ addNewChannel(channelId, channelState) { this.channelStates.set(channelId, { address: channelState.channelAddress, sender: channelState.sender, recipient: channelState.recipient, balance: channelState.amount.toString(), nonce: "0", expiration: (channelState.timestamp + channelState.duration).toString(), channel_id: channelId, }); } /** * gets the state of a payment channel * @param channelId * @returns */ getChannelState(channelId) { return this.channelStates.get(channelId); } getNonce(channelId) { const currentNonce = this.nonceMap.get(channelId) || 0; this.nonceMap.set(channelId, currentNonce + 1); return currentNonce.toString(); } /** * signs a request with channel details * @param paymentChannel PaymentChannelResponse * @param rawBody Body of the request * @returns SignedRequest */ async signPaymentChannelRequest(paymentChannel, rawBody) { try { console.log("Raw body", rawBody); // Convert raw body to proper format // Use the actual request body instead of headers // Empty uint8 array if no body is present const bodyBytes = rawBody == undefined ? new Uint8Array(0) : toBytes(typeof rawBody === "string" ? rawBody : JSON.stringify(rawBody)); console.log("Body Bytes:", bodyBytes); // Concatenate all parts const encodedMessage = keccak256(encodePacked(["uint256", "uint256", "uint256", "bytes"], [ BigInt(paymentChannel.channel_id), BigInt(paymentChannel.balance), BigInt(this.getNonce(paymentChannel.channel_id)), toHex(bodyBytes), ])); console.log("\nMessage Components:"); console.log("Channel ID:", paymentChannel.channel_id); console.log("Balance:", paymentChannel.balance); console.log("Nonce:", this.getNonce(paymentChannel.channel_id)); console.log("Body (hex):", toHex(bodyBytes)); console.log("Final Message:", encodedMessage); // @ts-ignore const signature = await this.account?.signMessage({ message: { raw: encodedMessage }, }); console.log("Signature:", signature); return { message: encodedMessage, signature, timestamp: Math.floor(Date.now() / 1000).toString(), }; } catch (err) { console.error("Sign Request Error:", err); throw err; } } /** * signs a request with one time payment details * @param txHash Transaction Hash * @returns SignedRequest */ async signOneTimePaymentRequest(txHash) { try { // Concatenate all parts const encodedMessage = keccak256(encodePacked(["bytes"], [txHash])); console.log("\nMessage Components:"); console.log("Tx Hash:", txHash); console.log("Final Message:", encodedMessage); // @ts-ignore const signature = await this.account?.signMessage({ message: { raw: encodedMessage }, }); console.log("Signature:", signature); return { message: encodedMessage, signature, timestamp: Math.floor(Date.now() / 1000).toString(), }; } catch (err) { console.error("Sign Request Error:", err); throw err; } } /** * signs a request with stream details * @param sender Sender Address * @returns SignedRequest */ async signStreamRequest(sender) { try { // Concatenate all parts const encodedMessage = keccak256(encodePacked(["address"], [sender])); console.log("\nMessage Components:"); console.log("Sender:", sender); console.log("Final Message:", encodedMessage); // @ts-ignore const signature = await this.account?.signMessage({ message: { raw: encodedMessage }, }); console.log("Signature:", signature); return { message: encodedMessage, signature, timestamp: Math.floor(Date.now() / 1000).toString(), }; } catch (err) { console.error("Sign Request Error:", err); throw err; } } /** * creates an interceptor for HTTP clients (axios, fetch) * @param channelId */ createPaymentChannelRequestInterceptor(channelId) { return { request: async (config) => { try { const channelState = this.channelStates.get(channelId); if (!channelState) { throw new Error(`No payment channel found for ID: ${channelId}`); } const signedRequest = await this.signPaymentChannelRequest(channelState, config.data); console.log("Adding headers to request:"); config.headers.set({ "x-Message": signedRequest.message, "x-Signature": signedRequest.signature, "x-Timestamp": signedRequest.timestamp, "x-Payment": JSON.stringify(channelState), }); return config; } catch (err) { throw err; } }, }; } /** * creates a one time payment request interceptor for HTTP clients (axios, fetch) * @param txHash */ createOneTimePaymentRequestInterceptor(txHash) { return { request: async (config) => { try { const signedRequest = await this.signOneTimePaymentRequest(txHash); console.log("Adding headers to request:"); config.headers.set({ "X-Signature": signedRequest.signature, "X-Transaction": txHash, "X-Timestamp": signedRequest.timestamp, }); return config; } catch (err) { throw err; } }, }; } /** * creates a stream based requests interceptor for HTTP clients (axios, fetch) * @param sender */ createStreamRequestInterceptor(sender) { return { request: async (config) => { try { const signedRequest = await this.signStreamRequest(sender); console.log("Adding headers to request:"); config.headers.set({ "X-Signature": signedRequest.signature, "X-Sender": sender, "X-Timestamp": signedRequest.timestamp, }); return config; } catch (err) { throw err; } }, }; } /** * creates an response interceptor and extracts payment channel state */ createPaymentChannelResponseInterceptor() { return { response: (response) => { console.log("Response Status:", response.status); console.log("Response Headers:", response.headers); console.log("Response Data:", response.data); // Proceed with channel state extraction try { const paymentChannelStr = response.headers["x-Payment"] || response.headers["x-payment"]; if (!paymentChannelStr) { console.error("No payment channel found in response headers"); return response; } const paymentChannel = JSON.parse(paymentChannelStr); const channelId = paymentChannel.channel_id; // Update nonce const nextNonce = Number(paymentChannel.nonce) + 1; paymentChannel.nonce = nextNonce.toString(); // Update channel state this.channelStates.set(channelId, paymentChannel); this.nonceMap.set(channelId, Number(nextNonce)); return response; } catch (err) { throw err; } }, }; } /** * helper method to extract channelId from event logs */ getChannelIdFromLogs(logs) { // todo: add more events based on contract spec const event = logs.find((log) => log.eventName === "channelCreated"); if (!event) { throw new Error("Channel creation event not found in logs"); } return event.args.channelId.toString(); } }