@zebec-fintech/silver-card-sdk
Version:
An sdk for purchasing silver card in zebec
435 lines (434 loc) • 19.4 kB
JavaScript
"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;