UNPKG

@zebec-fintech/silver-card-sdk

Version:
435 lines (434 loc) 19.4 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.ZebecCardTAOService = exports.ZebecCardService = void 0; const axios_1 = __importStar(require("axios")); const crypto_1 = __importDefault(require("crypto")); const ethers_1 = require("ethers"); const api_1 = require("@polkadot/api"); const util_1 = require("@polkadot/util"); const artifacts_1 = require("./artifacts"); const chains_1 = require("./chains"); const constants_1 = require("./constants"); const errors_1 = require("./errors"); const types_1 = require("./types"); const utils_1 = require("./utils"); class ZebecCardAPIService { apiConfig; sdkVersion = "1.0.0"; api; constructor(apiConfig, sandbox) { this.apiConfig = { ...apiConfig, apiUrl: sandbox ? constants_1.CARD_API_URL.Sandbox : constants_1.CARD_API_URL.Production, }; this.api = axios_1.default.create({ baseURL: this.apiConfig.apiUrl, }); } // Generate request signature generateSignature(method, path, timestamp, body) { const stringToSign = [ method.toUpperCase(), path, timestamp, this.apiConfig.apiKey, body ? JSON.stringify(body) : "", ].join(""); return crypto_1.default .createHmac("sha256", this.apiConfig.encryptionKey) .update(stringToSign) .digest("hex"); } // Generate request headers generateRequestHeaders(method, path, body) { const timestamp = Math.floor(Date.now() / 1000); const nonce = crypto_1.default.randomBytes(16).toString("hex"); return { "X-API-Key": this.apiConfig.apiKey, "X-Timestamp": timestamp.toString(), "X-Nonce": nonce, "X-Signature": this.generateSignature(method, path, timestamp, body), "X-SDK-Version": this.sdkVersion, "Content-Type": "application/json", }; } // Encrypt sensitive data fields encryptSensitiveData(data) { const iv = crypto_1.default.randomBytes(16); const key = crypto_1.default.pbkdf2Sync(this.apiConfig.encryptionKey, iv, 1000, 32, "sha256"); const cipher = crypto_1.default.createCipheriv("aes-256-gcm", key, iv); let encrypted = cipher.update(JSON.stringify(data), "utf8", "base64"); encrypted += cipher.final("base64"); const authTag = cipher.getAuthTag(); return `${iv.toString("base64")}:${encrypted}:${authTag.toString("base64")}`; } // Ping API status async ping() { try { await this.api.get("/ping"); return true; } catch (error) { throw new Error("Card service is down. Please try again later."); } } // Purchase Card async purchaseCard(data) { console.debug("Payload data:", data); const encryptedData = this.encryptSensitiveData(data); console.debug("Encrypted Data: %s \n", encryptedData); const method = "POST"; const path = "/orders/create"; const url = this.apiConfig.apiUrl + path; const payload = { data: encryptedData, }; const headers = this.generateRequestHeaders(method, path, payload); const response = await axios_1.default.post(url, payload, { headers, }); return response; } // Fetch quote async fetchQuote(symbol, amount) { const url = `/exchange/quotes/${symbol.toString()}_USD/${(0, utils_1.formatAmount)(amount)}`; // console.debug({ url }); const { data } = await this.api.get(url); return data; } async fetchVault(symbol) { const { data } = await this.api.get(`/exchange/vault/${symbol.toLowerCase()}`); return data?.depositVault?.address; } } class ZebecCardService { signer; zebecCard; usdcToken; chainId; apiService; constructor(signer, chainId, apiConfig, sdkOptions) { this.signer = signer; const sandbox = sdkOptions?.sandbox ? sdkOptions.sandbox : false; const isTesnetChainId = chains_1.TESTNET_CHAINIDS.includes(chainId); if ((sandbox && !isTesnetChainId) || (!sandbox && isTesnetChainId)) { throw new Error("Only testnet chains are allowed in sandbox environment"); } this.apiService = new ZebecCardAPIService(apiConfig, sandbox); this.chainId = (0, chains_1.parseSupportedChain)(chainId); const zebecCardAddress = constants_1.ZEBEC_CARD_ADDRESS[this.chainId]; const usdcAddress = constants_1.USDC_ADDRESS[this.chainId]; this.zebecCard = artifacts_1.ZebecCard__factory.connect(zebecCardAddress, signer); this.usdcToken = artifacts_1.ERC20__factory.connect(usdcAddress, signer); } /** * Fetches a quote for the given amount. * * @param {string | number} amount - The amount for which to fetch the quote. * @returns {Promise<Quote>} A promise that resolves to a Quote object. */ async fetchQuote(amount) { const res = await this.apiService.fetchQuote("USDC", amount); return res; } /** * Transfer specified amount from user's vault balance to card vault with some fee amount for card purchase. * @param params * @returns */ async purchaseCard(params) { // Check card service status await this.apiService.ping(); // Validate Quote const { expiresIn, token, targetCurrency, amountRequested, totalPrice, platformFee } = params.quote; if (token.toLowerCase() !== "usdc" || targetCurrency.toLowerCase() !== "usd" || amountRequested !== (0, utils_1.formatAmount)(params.amount)) { throw new Error("Invalid quote"); } if (expiresIn - 20000 < Date.now()) { throw new Error("Quote expired"); } const decimals = await this.usdcToken.decimals(); const totalAmount = (0, utils_1.formatAmount)(totalPrice + platformFee, Number(decimals)); const parsedAmount = ethers_1.ethers.parseUnits(totalAmount.toString(), decimals); if (!(0, utils_1.isEmailValid)(params.recipient.emailAddress)) { throw new errors_1.InvalidEmailError(params.recipient.emailAddress); } const usdcBalance = await this.usdcToken.balanceOf(this.signer); console.debug("Usdc Balance:", usdcBalance); if (parsedAmount > usdcBalance) { throw new errors_1.NotEnoughBalanceError(ethers_1.ethers.formatUnits(usdcBalance, decimals), params.amount); } let cardConfig = await this.zebecCard.cardConfig(); const minRange = cardConfig.minCardAmount; const maxRange = cardConfig.maxCardAmount; if (parsedAmount < minRange || parsedAmount > maxRange) { throw new errors_1.CardPurchaseAmountOutOfRangeError(ethers_1.ethers.formatUnits(minRange, decimals), ethers_1.ethers.formatUnits(maxRange, decimals)); } const cardPurchaseInfo = await this.zebecCard.cardPurchases(this.signer); const lastCardPurchaseDate = new Date(Number(cardPurchaseInfo.unixInRecord * 1000n)); const today = new Date(); let cardPurchaseOfDay = 0n; if ((0, utils_1.areDatesOfSameDay)(today, lastCardPurchaseDate)) { cardPurchaseOfDay = cardPurchaseInfo.totalCardBoughtPerDay + parsedAmount; } else { cardPurchaseOfDay = parsedAmount; } if (cardPurchaseOfDay > cardConfig.dailyCardBuyLimit) { throw new errors_1.DailyCardPurchaseLimitExceedError(ethers_1.ethers.formatUnits(cardConfig.dailyCardBuyLimit, decimals), ethers_1.ethers.formatUnits(cardPurchaseInfo.totalCardBoughtPerDay, decimals)); } const allowance = await this.usdcToken.allowance(this.signer, this.zebecCard); console.debug("Allowance:", allowance); if (allowance < parsedAmount) { console.debug("===== Approving token ====="); const approveResponse = await this.usdcToken.approve(this.zebecCard, parsedAmount); const approveReceipt = await approveResponse.wait(); console.debug("Approve hash: %s \n", approveReceipt?.hash); } console.debug("===== Depositing USDC ====="); const depositResponse = await this.zebecCard.depositUsdc(parsedAmount); const depositReceipt = await depositResponse.wait(); console.debug("Deposit hash: %s \n", depositReceipt?.hash); cardConfig = await this.zebecCard.cardConfig(); const purchaseCounter = Number((cardConfig.counter + 1n).toString()); const cardTypeId = "103253238082"; const emailHash = await (0, utils_1.hashSHA256)(params.recipient.emailAddress); console.debug("===== Purchasing Card ====="); const buyCardResponse = await this.zebecCard.buyCard(parsedAmount, cardTypeId, emailHash); const buyCardReceipt = await buyCardResponse.wait(); console.debug("Purchase hash: %s \n", buyCardReceipt?.hash); const usdAmount = types_1.Money.USD(params.amount); const buyer = await this.signer.getAddress(); const receipt = new types_1.Receipt(params.quote, new types_1.Deposit("USDC", Number(totalAmount), "", buyer, buyCardResponse.hash, "", this.chainId, purchaseCounter)); const payload = new types_1.OrderCardRequest(usdAmount, params.recipient, receipt); let retries = 0; let delay = 1000; // Initial delay in milliseconds (1 second) const maxRetries = 5; // Max retry default while (retries < maxRetries) { try { const response = await this.apiService.purchaseCard(payload); console.debug("API response: %o \n", response.data); return [depositResponse, buyCardResponse, response]; } catch (error) { if (error instanceof axios_1.AxiosError) { console.debug("error", error.response?.data); console.debug("error", error.message); } else { console.debug("error", error); } if (retries >= maxRetries) { throw error; } retries += 1; console.debug(`Retrying in ${delay / 1000} seconds...`); await new Promise((resolve) => setTimeout(resolve, delay)); delay *= 2; // Exponential backoff } } throw new Error("Max retries reached"); } } exports.ZebecCardService = ZebecCardService; class ZebecCardTAOService { signer; apiService; taoRPC; chainId; /** * Constructs an instance of the service. * * @param {Signer} signer - The signer which can be either a PolkadotJs Signer or a KeyringPair. * @param {APIConfig} apiConfig - The configuration object for the API. * @param sdkOptions - Optional configuration for the SDK. */ constructor(signer, apiConfig, sdkOptions) { this.signer = signer; const sandbox = sdkOptions?.sandbox ? sdkOptions.sandbox : false; this.apiService = new ZebecCardAPIService(apiConfig, sandbox); this.taoRPC = sandbox ? constants_1.TAO_RPC_URL.Sandbox : constants_1.TAO_RPC_URL.Production; this.chainId = sandbox ? chains_1.SupportedChain.BittensorTestnet : chains_1.SupportedChain.Bittensor; } /** * Fetches a quote for the given amount. * * @param {string | number} amount - The amount for which to fetch the quote. * @returns {Promise<Quote>} A promise that resolves to a Quote object. */ async fetchQuote(amount) { const res = await this.apiService.fetchQuote("TAO", amount); return res; } /** * Fetches the TAO Vault address. * * @returns {Promise<string>} A promise that resolves to the TAO Vault address. */ async fetchTAOVault() { const address = await this.apiService.fetchVault("TAO"); return address; } /** * Purchases a card by transferring TAO tokens. * * @param params - The parameters required to purchase a card. * @param params.walletAddress - The wallet address from which TAO tokens will be transferred. * @param params.amount - The amount of TAO tokens to transfer. * @param {Recipient} params.recipient - The recipient details. * @param {Quote} params.quote - The quote details. * * @returns A promise that resolves to an array containing the transaction details and the API response. * @throws {InvalidEmailError} If the recipient's email address is invalid. * @throws {Error} If the quote is invalid or expired, if there is not enough balance, or if the transaction fails. */ async purchaseCard(params) { // Check card service status await this.apiService.ping(); // Validate recipient email if (!(0, utils_1.isEmailValid)(params.recipient.emailAddress)) { throw new errors_1.InvalidEmailError(params.recipient.emailAddress); } // Validate Quote const { expiresIn, token, targetCurrency, amountRequested } = params.quote; if (token.toLowerCase() !== "tao" || targetCurrency.toLowerCase() !== "usd" || amountRequested !== (0, utils_1.formatAmount)(params.amount)) { throw new Error("Invalid quote"); } if (expiresIn - 20000 < Date.now()) { throw new Error("Quote expired"); } // Fetch deposit address const depositAddress = await this.fetchTAOVault(); console.debug("Deposit Address:", depositAddress); // Generate signature const message = params.quote.id; let signature = ""; if ("address" in this.signer) { const signatureBytes = await this.signer.sign(message, { withType: true, }); signature = (0, util_1.u8aToHex)(signatureBytes); } else { if (this.signer.signRaw) { signature = await this.signer ?.signRaw({ address: params.walletAddress, data: message, type: "payload", }) .then((result) => result.signature); } else { throw new Error("Signer does not support message signing"); } } // Transfer TAO to deposit address const provider = new api_1.WsProvider(this.taoRPC); const api = await api_1.ApiPromise.create({ provider }); const totalAmount = Math.floor(Number(params.quote.totalPrice) * 1e9); const balance = await api?.query.system.account(params.walletAddress); const freeBalance = balance.data.free.toNumber(); if (freeBalance < totalAmount) { await api.disconnect(); // release socket throw new Error(`Not enough balance, required: ${params.quote.totalPrice}, available: ${freeBalance}`); } let resolveOut; let rejectOut; const promise = new Promise((resolve, reject) => { resolveOut = resolve; rejectOut = reject; }); let blockHash = ""; let txHash = ""; const tx = api.tx.balances.transferKeepAlive(depositAddress, totalAmount); const unsub = await tx.signAndSend("address" in this.signer ? this.signer : params.walletAddress, { signer: "address" in this.signer ? undefined : this.signer, }, ({ events = [], isInBlock, isFinalized, isError, status, txHash: _txHash }) => { console.debug("Transaction status:", status.type); if (isInBlock || isFinalized) { console.debug("Included at block hash", status.asInBlock.toHex()); blockHash = status.asInBlock.toHex(); txHash = _txHash.toHex(); const isSuccess = events.every(({ event }) => !api.events.system.ExtrinsicFailed.is(event)); if (isSuccess) { unsub(); resolveOut(); } } else if (isError) { unsub(); rejectOut(new Error("Transaction failed")); } }); await promise; const amount = types_1.Money.USD(params.amount); const receipt = new types_1.Receipt(params.quote, new types_1.Deposit("TAO", totalAmount / 1e9, signature, params.walletAddress, txHash, blockHash, this.chainId)); const payload = new types_1.OrderCardRequest(amount, params.recipient, receipt); // Purchase card let retries = 0; let delay = 1000; // Initial delay in milliseconds (1 second) const maxRetries = 5; // Max retry default while (retries < maxRetries) { try { const response = await this.apiService.purchaseCard(payload); console.debug("API response: %o \n", response.data); const depositResponse = { txHash, blockHash }; await api .disconnect() .then(() => { }) .catch(() => { }); return [depositResponse, response]; } catch (error) { if (error instanceof axios_1.AxiosError) { console.debug("error", error.response?.data); console.debug("error", error.message); } else { console.debug("error", error); } if (retries >= maxRetries) { throw error; } retries += 1; console.debug(`Retrying in ${delay / 1000} seconds...`); await new Promise((resolve) => setTimeout(resolve, delay)); delay *= 2; // Exponential backoff } } await api.disconnect(); throw new Error("Max retries reached"); } } exports.ZebecCardTAOService = ZebecCardTAOService;