polkadotwallet/polkadotBlockchainAPI.js

537 lines
17 KiB
JavaScript

// API for Polkadot AssetHub (Subscan)
const polkadotAPI = (function () {
"use strict";
const SUBSCAN_API = "https://assethub-polkadot.api.subscan.io";
const NETWORK = "assethub-polkadot";
function normalizeAddress(address) {
if (!address) return address;
if (typeof polkadotCrypto !== "undefined" && polkadotCrypto.hexToSS58) {
return polkadotCrypto.hexToSS58(address, 0);
}
// Fallback: return as-is
return address;
}
async function getBalance(address) {
try {
const response = await fetch(`${SUBSCAN_API}/api/scan/account/tokens`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-API-Key": "239a9db0f7174ad6a07ee6006dbb29a7", // Add API key if needed
},
body: JSON.stringify({
address: address,
}),
});
const data = await response.json();
if (data.code === 0 && data.data) {
// Find DOT balance
const dotBalance = data.data.native?.find(
(token) => token.symbol === "DOT"
);
return {
balance: dotBalance
? parseFloat(dotBalance.balance) / Math.pow(10, dotBalance.decimals)
: 0,
address: address,
decimals: dotBalance?.decimals || 10,
};
}
throw new Error(data.message || "Failed to fetch balance");
} catch (error) {
console.error("Error fetching balance:", error);
throw error;
}
}
async function getTransactions(address, page = 0, limit = 20) {
try {
const response = await fetch(`${SUBSCAN_API}/api/v2/scan/transfers`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-API-Key": "239a9db0f7174ad6a07ee6006dbb29a7",
},
body: JSON.stringify({
address: address,
row: limit,
page: page,
}),
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
if (data.code === 0 && data.data) {
const transactions = data.data.transfers || [];
return transactions.map((tx) => ({
id: tx.hash,
hash: tx.hash,
from: normalizeAddress(tx.from),
to: normalizeAddress(tx.to),
amount: parseFloat(tx.amount || 0),
amountDot: parseFloat(tx.amount || 0),
fee: parseFloat(tx.fee || 0) / Math.pow(10, 10),
feeDot: parseFloat(tx.fee || 0) / Math.pow(10, 10),
block: tx.block_num,
timestamp: tx.block_timestamp,
success: tx.success,
type: normalizeAddress(tx.from) === address ? "sent" : "received",
module: tx.module,
asset_symbol: tx.asset_symbol || "DOT",
extrinsicIndex: tx.extrinsic_index,
}));
}
throw new Error(data.message || "Failed to fetch transactions");
} catch (error) {
console.error("Error fetching transactions:", error);
throw error;
}
}
async function getTransaction(hash) {
try {
const response = await fetch(`${SUBSCAN_API}/api/scan/extrinsic`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-API-Key": "239a9db0f7174ad6a07ee6006dbb29a7", // Add API key if needed
},
body: JSON.stringify({
hash: hash,
}),
});
const data = await response.json();
if (data.code === 0 && data.data) {
const tx = data.data;
// Extract sender address
let from = tx.account_id || tx.account?.address || "";
// Extract destination and amount based on transaction type
let to = "";
let amount = 0;
// Check if it's a regular transfer with transfer object
if (tx.transfer) {
to = tx.transfer.to || tx.transfer.destination || "";
// The transfer.amount is already in DOT (not planck), so use it directly
amount = parseFloat(tx.transfer.amount) || 0;
}
// Check if it's a balance transfer without transfer object (data in params)
else if (
tx.params &&
Array.isArray(tx.params) &&
(tx.call_module === "balances" || tx.module === "balances")
) {
// Find dest parameter
const destParam = tx.params.find((p) => p.name === "dest");
if (destParam && destParam.value) {
// Try to get SS58 address first, fallback to hex Id
to = destParam.value.address || destParam.value.Id || "";
}
// Find value parameter
const valueParam = tx.params.find((p) => p.name === "value");
if (valueParam && valueParam.value) {
// Convert from planck to DOT (1 DOT = 10^10 planck)
amount = parseFloat(valueParam.value) / Math.pow(10, 10);
}
}
// Check if it's an XCM transfer
else if (tx.params && Array.isArray(tx.params)) {
// Find beneficiary parameter
const beneficiaryParam = tx.params.find(
(p) => p.name === "beneficiary"
);
if (beneficiaryParam && beneficiaryParam.value) {
try {
// Extract AccountId32 from nested structure
const v3 = beneficiaryParam.value.V3 || beneficiaryParam.value.V4;
if (v3 && v3.interior) {
// Handle X1 as object or array
let accountId32Data = null;
if (v3.interior.X1) {
if (v3.interior.X1.AccountId32) {
accountId32Data = v3.interior.X1.AccountId32;
} else if (
Array.isArray(v3.interior.X1) &&
v3.interior.X1[0]?.AccountId32
) {
accountId32Data = v3.interior.X1[0].AccountId32;
}
}
if (accountId32Data && accountId32Data.id) {
to = accountId32Data.id;
}
}
} catch (e) {
console.error("Failed to extract beneficiary:", e);
}
}
// Find assets/amount parameter
const assetsParam = tx.params.find(
(p) => p.name === "assets" || p.name === "value"
);
if (assetsParam && assetsParam.value) {
try {
const v3 = assetsParam.value.V3 || assetsParam.value.V4;
if (v3 && Array.isArray(v3) && v3.length > 0) {
const asset = v3[0];
const fungible = asset.fun?.Fungible || asset.amount?.Fungible;
if (fungible) {
amount = parseFloat(fungible) / Math.pow(10, 10);
}
}
} catch (e) {
console.error("Failed to extract amount:", e);
}
}
}
// Fallback: If still no data, check events for Transfer
if (!to && !amount && tx.event && Array.isArray(tx.event)) {
const transferEvent = tx.event.find(
(e) => e.module_id === "balances" && e.event_id === "Transfer"
);
if (transferEvent && transferEvent.params) {
try {
const eventParams = JSON.parse(transferEvent.params);
// Find to and amount from event params
const toParam = eventParams.find((p) => p.name === "to");
const amountParam = eventParams.find((p) => p.name === "amount");
if (toParam && toParam.value) {
to = toParam.value;
}
if (amountParam && amountParam.value) {
amount = parseFloat(amountParam.value) / Math.pow(10, 10);
}
} catch (e) {
console.error("Failed to parse event params:", e);
}
}
}
// Normalize addresses (keep SS58 as-is, hex addresses remain valid for AssetHub)
from = normalizeAddress(from);
to = normalizeAddress(to);
return {
id: tx.extrinsic_hash,
hash: tx.extrinsic_hash,
from: from,
to: to,
amount: amount,
amountDot: amount,
fee: parseFloat(tx.fee || 0) / Math.pow(10, 10),
feeDot: parseFloat(tx.fee || 0) / Math.pow(10, 10),
block: tx.block_num,
blockNum: tx.block_num,
timestamp: tx.block_timestamp,
success: tx.success,
module: tx.call_module,
method: tx.call_module_function,
signature: tx.signature,
};
}
throw new Error(data.message || "Transaction not found");
} catch (error) {
console.error("Error fetching transaction:", error);
throw error;
}
}
// Build and sign transaction using direct crypto (no full API needed)
async function buildAndSignTransaction(txParams) {
const { sourceAddress, destinationAddress, amount, privateKeyHex, memo } =
txParams;
try {
// Wait for crypto to be ready
const { cryptoWaitReady, sr25519PairFromSeed, sr25519Sign } =
window.polkadotUtilCrypto || {};
if (!cryptoWaitReady) {
throw new Error(
"Polkadot crypto utilities not loaded. Please refresh the page."
);
}
await cryptoWaitReady();
// Convert hex private key to seed (first 32 bytes)
const privKeyOnly = privateKeyHex.substring(0, 64);
const { hexToU8a, u8aToHex } = window.polkadotUtil || {};
if (!hexToU8a) {
throw new Error(
"Polkadot utilities not loaded. Please refresh the page."
);
}
const seed = hexToU8a("0x" + privKeyOnly).slice(0, 32);
// Create keypair from seed using Sr25519
const keypair = sr25519PairFromSeed(seed);
// Get address from public key
const { encodeAddress } = window.polkadotUtilCrypto;
const address = encodeAddress(keypair.publicKey, 0); // 0 = Polkadot prefix
// Convert amount to planck (1 DOT = 10^10 planck)
const amountInPlanck = Math.floor(parseFloat(amount) * Math.pow(10, 10));
// Estimated fee
const estimatedFee = 0.0165;
// Return transaction data for RPC submission
return {
keypair: keypair,
destinationAddress: destinationAddress,
amountInPlanck: amountInPlanck,
fee: estimatedFee,
feeDot: estimatedFee,
sourceAddress: sourceAddress,
};
} catch (error) {
console.error("Error building transaction:", error);
throw error;
}
}
// Submit transaction using Polkadot.js API
async function submitTransaction(txData) {
try {
const { keypair, destinationAddress, amountInPlanck, sourceAddress } =
txData;
// Check if API is available
if (
!window.polkadotApi ||
!window.polkadotApi.ApiPromise ||
!window.polkadotApi.WsProvider
) {
throw new Error("Polkadot API not loaded! Please refresh the page.");
}
const { ApiPromise, WsProvider } = window.polkadotApi;
const rpcUrl = "wss://polkadot-asset-hub-rpc.polkadot.io";
// Create API instance
const provider = new WsProvider(rpcUrl);
const api = await ApiPromise.create({ provider });
// Get account nonce
const nonce = await api.rpc.system.accountNextIndex(sourceAddress);
const transfer = api.tx.balances.transferAllowDeath(
destinationAddress,
amountInPlanck
);
// Create a Keyring and add our keypair
// The Polkadot API needs a proper KeyringPair interface
// Our keypair from sr25519PairFromSeed has the right structure, but we need to add the sign method
const { u8aToHex } = window.polkadotUtil;
const signerPair = {
address: keypair.address,
addressRaw: keypair.publicKey,
publicKey: keypair.publicKey,
sign: (data) => {
const { sr25519Sign } = window.polkadotUtilCrypto;
const signature = sr25519Sign(data, keypair);
// Return signature with proper format for Sr25519: { sr25519: Uint8Array }
return { sr25519: signature };
},
type: "sr25519",
unlock: () => {}, // Required but unused for our case
lock: () => {}, // Required but unused for our case
isLocked: false,
};
return new Promise((resolve, reject) => {
let unsub;
const timeout = setTimeout(() => {
if (unsub) unsub();
api.disconnect();
reject(new Error("Transaction timeout"));
}, 60000);
transfer
.signAndSend(signerPair, { nonce }, (result) => {
if (result.status.isFinalized) {
clearTimeout(timeout);
// Check for errors
const failed = result.events.find(({ event }) =>
api.events.system.ExtrinsicFailed.is(event)
);
if (failed) {
const [dispatchError] = failed.event.data;
let errorMessage = "Transaction failed";
if (dispatchError.isModule) {
try {
const decoded = api.registry.findMetaError(
dispatchError.asModule
);
errorMessage = `${decoded.section}.${
decoded.name
}: ${decoded.docs.join(" ")}`;
} catch (e) {
errorMessage = `Module error: ${dispatchError.asModule.toHuman()}`;
}
} else if (dispatchError.isToken) {
errorMessage = `Token error: ${dispatchError.asToken.toString()}`;
} else if (dispatchError.isArithmetic) {
errorMessage = `Arithmetic error: ${dispatchError.asArithmetic.toString()}`;
}
console.error(`❌ Transaction failed: ${errorMessage}`);
if (unsub) unsub();
api.disconnect();
reject(new Error(errorMessage));
} else {
const txHash = transfer.hash.toHex();
console.log(`✅ Transaction successful! Hash: ${txHash}`);
if (unsub) unsub();
api.disconnect();
resolve({
hash: txHash,
success: true,
block: result.status.asFinalized.toHex(),
});
}
}
})
.then((unsubscribe) => {
unsub = unsubscribe;
})
.catch((error) => {
clearTimeout(timeout);
console.error(`❌ Signing/sending error: ${error.message}`);
api.disconnect();
reject(error);
});
});
} catch (error) {
console.error("❌ Error submitting transaction:", error);
throw error;
}
}
// Estimate transaction fee using Polkadot API paymentInfo
async function estimateFee(sourceAddress, destinationAddress, amount) {
try {
// Check if API is available
if (
!window.polkadotApi ||
!window.polkadotApi.ApiPromise ||
!window.polkadotApi.WsProvider
) {
console.warn("Polkadot API not loaded, using default fee estimate");
return {
fee: 0.0165,
feeDot: 0.0165,
};
}
const { ApiPromise, WsProvider } = window.polkadotApi;
const rpcUrl = "wss://polkadot-asset-hub-rpc.polkadot.io";
// Create API instance
const provider = new WsProvider(rpcUrl);
const api = await ApiPromise.create({ provider });
// Convert amount to planck
const amountInPlanck = Math.floor(parseFloat(amount) * Math.pow(10, 10));
// Create the transfer transaction
const transfer = api.tx.balances.transferAllowDeath(
destinationAddress,
amountInPlanck
);
// Get payment info (accurate fee estimation)
const paymentInfo = await transfer.paymentInfo(sourceAddress);
// Disconnect after getting fee
await api.disconnect();
// Convert fee from planck to DOT
const fee =
parseFloat(paymentInfo.partialFee.toString()) / Math.pow(10, 10);
return {
fee: fee,
feeDot: fee,
};
} catch (error) {
console.error("Error estimating fee:", error);
// Return typical DOT transfer fee if estimation fails
return {
fee: 0.0165,
feeDot: 0.0165,
};
}
}
// Check if account is active (has balance)
async function checkAccountActive(address) {
try {
const balanceData = await getBalance(address);
return {
isActive: balanceData.balance > 0,
balance: balanceData.balance,
minimumRequired: 0.01, // Minimum to activate new account
};
} catch (error) {
console.error("Error checking account status:", error);
// If we can't check, assume account needs activation
return {
isActive: false,
balance: 0,
minimumRequired: 0.01,
};
}
}
// Public API
return {
getBalance,
getTransactions,
getTransaction,
buildAndSignTransaction,
submitTransaction,
estimateFee,
checkAccountActive,
SUBSCAN_API,
NETWORK,
};
})();