Files
pwap/shared/lib/usdt.ts
T

323 lines
9.7 KiB
TypeScript

// ========================================
// USDT Bridge Utilities
// ========================================
// Handles wUSDT minting, burning, and reserve management
import type { ApiPromise } from '@pezkuwi/api';
import { ASSET_IDS, ASSET_CONFIGS } from './wallet';
import { getMultisigMembers, createMultisigTx } from './multisig';
// ========================================
// CONSTANTS
// ========================================
export const WUSDT_ASSET_ID = ASSET_CONFIGS.WUSDT.id;
export const WUSDT_DECIMALS = ASSET_CONFIGS.WUSDT.decimals;
// Withdrawal limits and timeouts
export const WITHDRAWAL_LIMITS = {
instant: {
maxAmount: 1000, // $1,000
delay: 0, // No delay
},
standard: {
maxAmount: 10000, // $10,000
delay: 3600, // 1 hour in seconds
},
large: {
maxAmount: Infinity,
delay: 86400, // 24 hours
},
};
// ========================================
// ASSET QUERIES
// ========================================
/**
* Get wUSDT balance for an account
* @param api - Polkadot API instance
* @param address - Account address
* @returns Balance in human-readable format
*/
export async function getWUSDTBalance(api: ApiPromise, address: string): Promise<number> {
try {
const balance = await api.query.assets.account(WUSDT_ASSET_ID, address);
if (balance.isSome) {
const balanceCodec = balance.unwrap() as { toJSON: () => unknown };
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const balanceData = balanceCodec.toJSON() as any;
return Number(balanceData.balance) / Math.pow(10, WUSDT_DECIMALS);
}
return 0;
} catch (error) {
console.error('Error fetching wUSDT balance:', error);
return 0;
}
}
/**
* Get total wUSDT supply
* @param api - Polkadot API instance
* @returns Total supply in human-readable format
*/
export async function getWUSDTTotalSupply(api: ApiPromise): Promise<number> {
try {
const assetDetails = await api.query.assets.asset(WUSDT_ASSET_ID);
if (assetDetails.isSome) {
const assetCodec = assetDetails.unwrap() as { toJSON: () => unknown };
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const details = assetCodec.toJSON() as any;
return Number(details.supply) / Math.pow(10, WUSDT_DECIMALS);
}
return 0;
} catch (error) {
console.error('Error fetching wUSDT supply:', error);
return 0;
}
}
/**
* Get wUSDT asset metadata
* @param api - Polkadot API instance
* @returns Asset metadata
*/
export async function getWUSDTMetadata(api: ApiPromise) {
try {
const metadata = await api.query.assets.metadata(WUSDT_ASSET_ID);
return metadata.toJSON();
} catch (error) {
console.error('Error fetching wUSDT metadata:', error);
return null;
}
}
// ========================================
// MULTISIG OPERATIONS
// ========================================
/**
* Create multisig transaction to mint wUSDT
* @param api - Polkadot API instance
* @param beneficiary - Who will receive the wUSDT
* @param amount - Amount in human-readable format (e.g., 100.50 USDT)
* @param signerAddress - Address of the signer creating this tx
* @param specificAddresses - Addresses for non-unique multisig members
* @returns Multisig transaction
*/
export async function createMintWUSDTTx(
api: ApiPromise,
beneficiary: string,
amount: number,
signerAddress: string,
specificAddresses: Record<string, string> = {}
) {
// Convert to smallest unit
const amountBN = BigInt(Math.floor(amount * Math.pow(10, WUSDT_DECIMALS)));
// Create the mint call
const mintCall = api.tx.assets.mint(WUSDT_ASSET_ID, beneficiary, amountBN.toString());
// Get all multisig members
const allMembers = await getMultisigMembers(api, specificAddresses);
// Other signatories (excluding current signer)
const otherSignatories = allMembers.filter((addr) => addr !== signerAddress);
// Create multisig transaction
return createMultisigTx(api, mintCall, otherSignatories);
}
/**
* Create multisig transaction to burn wUSDT
* @param api - Polkadot API instance
* @param from - Who will have their wUSDT burned
* @param amount - Amount in human-readable format
* @param signerAddress - Address of the signer creating this tx
* @param specificAddresses - Addresses for non-unique multisig members
* @returns Multisig transaction
*/
export async function createBurnWUSDTTx(
api: ApiPromise,
from: string,
amount: number,
signerAddress: string,
specificAddresses: Record<string, string> = {}
) {
const amountBN = BigInt(Math.floor(amount * Math.pow(10, WUSDT_DECIMALS)));
const burnCall = api.tx.assets.burn(WUSDT_ASSET_ID, from, amountBN.toString());
const allMembers = await getMultisigMembers(api, specificAddresses);
const otherSignatories = allMembers.filter((addr) => addr !== signerAddress);
return createMultisigTx(api, burnCall, otherSignatories);
}
// ========================================
// WITHDRAWAL HELPERS
// ========================================
/**
* Calculate withdrawal delay based on amount
* @param amount - Withdrawal amount in USDT
* @returns Delay in seconds
*/
export function calculateWithdrawalDelay(amount: number): number {
if (amount <= WITHDRAWAL_LIMITS.instant.maxAmount) {
return WITHDRAWAL_LIMITS.instant.delay;
} else if (amount <= WITHDRAWAL_LIMITS.standard.maxAmount) {
return WITHDRAWAL_LIMITS.standard.delay;
} else {
return WITHDRAWAL_LIMITS.large.delay;
}
}
/**
* Get withdrawal tier name
* @param amount - Withdrawal amount
* @returns Tier name
*/
export function getWithdrawalTier(amount: number): string {
if (amount <= WITHDRAWAL_LIMITS.instant.maxAmount) return 'Instant';
if (amount <= WITHDRAWAL_LIMITS.standard.maxAmount) return 'Standard';
return 'Large';
}
/**
* Format delay time for display
* @param seconds - Delay in seconds
* @returns Human-readable format
*/
export function formatDelay(seconds: number): string {
if (seconds === 0) return 'Instant';
if (seconds < 3600) return `${Math.floor(seconds / 60)} minutes`;
if (seconds < 86400) return `${Math.floor(seconds / 3600)} hours`;
return `${Math.floor(seconds / 86400)} days`;
}
// ========================================
// RESERVE CHECKING
// ========================================
export interface ReserveStatus {
wusdtSupply: number;
offChainReserve: number; // This would come from off-chain oracle/API
collateralRatio: number;
isHealthy: boolean;
}
/**
* Check reserve health
* @param api - Polkadot API instance
* @param offChainReserve - Off-chain USDT reserve amount (from treasury)
* @returns Reserve status
*/
export async function checkReserveHealth(
api: ApiPromise,
offChainReserve: number
): Promise<ReserveStatus> {
const wusdtSupply = await getWUSDTTotalSupply(api);
const collateralRatio = wusdtSupply > 0 ? (offChainReserve / wusdtSupply) * 100 : 0;
return {
wusdtSupply,
offChainReserve,
collateralRatio,
isHealthy: collateralRatio >= 100, // At least 100% backed
};
}
// ========================================
// EVENT MONITORING
// ========================================
/**
* Subscribe to wUSDT mint events
* @param api - Polkadot API instance
* @param callback - Callback function for each mint event
*/
export function subscribeToMintEvents(
api: ApiPromise,
callback: (beneficiary: string, amount: number, txHash: string) => void
) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return api.query.system.events((events: any[]) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
events.forEach((record: any) => {
const { event } = record;
if (api.events.assets.Issued.is(event)) {
const [assetId, beneficiary, amount] = event.data;
if (assetId.toNumber() === WUSDT_ASSET_ID) {
const amountNum = Number(amount.toString()) / Math.pow(10, WUSDT_DECIMALS);
callback(beneficiary.toString(), amountNum, record.hash.toHex());
}
}
});
});
}
/**
* Subscribe to wUSDT burn events
* @param api - Polkadot API instance
* @param callback - Callback function for each burn event
*/
export function subscribeToBurnEvents(
api: ApiPromise,
callback: (account: string, amount: number, txHash: string) => void
) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return api.query.system.events((events: any[]) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
events.forEach((record: any) => {
const { event } = record;
if (api.events.assets.Burned.is(event)) {
const [assetId, account, amount] = event.data;
if (assetId.toNumber() === WUSDT_ASSET_ID) {
const amountNum = Number(amount.toString()) / Math.pow(10, WUSDT_DECIMALS);
callback(account.toString(), amountNum, record.hash.toHex());
}
}
});
});
}
// ========================================
// DISPLAY HELPERS
// ========================================
/**
* Format wUSDT amount for display
* @param amount - Amount in smallest unit or human-readable
* @param fromSmallestUnit - Whether input is in smallest unit
* @returns Formatted string
*/
export function formatWUSDT(amount: number | string, fromSmallestUnit = false): string {
const value = typeof amount === 'string' ? parseFloat(amount) : amount;
if (fromSmallestUnit) {
return (value / Math.pow(10, WUSDT_DECIMALS)).toFixed(2);
}
return value.toFixed(2);
}
/**
* Parse human-readable USDT to smallest unit
* @param amount - Human-readable amount
* @returns Amount in smallest unit (BigInt)
*/
export function parseWUSDT(amount: number | string): bigint {
const value = typeof amount === 'string' ? parseFloat(amount) : amount;
return BigInt(Math.floor(value * Math.pow(10, WUSDT_DECIMALS)));
}