@thorwallet/xchain-polkadot
Version:
Custom Polkadot client and utilities used by XChainJS clients
426 lines • 17.4 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Client = void 0;
const tslib_1 = require("tslib");
const axios_1 = tslib_1.__importDefault(require("axios"));
const xchain_util_1 = require("@thorwallet/xchain-util");
const xchainCrypto = tslib_1.__importStar(require("@thorwallet/xchain-crypto"));
const api_1 = require("@polkadot/api");
const util_1 = require("@polkadot/util");
const types_1 = require("./types");
const util_2 = require("./util");
/**
* Custom Polkadot client
*/
class Client {
/**
* Constructor
* Client is initialised with network type and phrase (optional)
*
* @param {XChainClientParams} params
*/
constructor({ network = 'testnet', rootDerivationPaths = {
mainnet: "44//354//0//0//0'",
testnet: "44//354//0//0//0'",
}, }) {
this.phrase = '';
/**
* Purge client.
*
* @returns {void}
*/
this.purgeClient = () => {
this.phrase = '';
};
/**
* Get the client url.
*
* @returns {string} The client url based on the network.
*/
this.getClientUrl = () => {
return this.network === 'testnet' ? 'https://westend.subscan.io' : 'https://polkadot.subscan.io';
};
/**
* Get the client WebSocket url.
*
* @returns {string} The client WebSocket url based on the network.
*/
this.getWsEndpoint = () => {
return this.network === 'testnet' ? 'wss://westend-rpc.polkadot.io' : 'wss://rpc.polkadot.io';
};
/**
* Get the explorer url.
*
* @returns {string} The explorer url based on the network.
*/
this.getExplorerUrl = () => {
return this.network === 'testnet' ? 'https://westend.subscan.io' : 'https://polkadot.subscan.io';
};
/**
* Get the explorer url for the given address.
*
* @param {Address} address
* @returns {string} The explorer url for the given address based on the network.
*/
this.getExplorerAddressUrl = (address) => {
return `${this.getExplorerUrl()}/account/${address}`;
};
/**
* Get the explorer url for the given transaction id.
*
* @param {string} txID The transaction id
* @returns {string} The explorer url for the given transaction id based on the network.
*/
this.getExplorerTxUrl = (txID) => {
return `${this.getExplorerUrl()}/extrinsic/${txID}`;
};
/**
* Get the SS58 format to be used for Polkadot Keyring.
*
* @returns {number} The SS58 format based on the network.
*/
this.getSS58Format = () => {
return this.network === 'testnet' ? 42 : 0;
};
/**
* Set/update a new phrase.
*
* @param {string} phrase A new phrase.
* @returns {Address} The address from the given phrase
*
* @throws {"Invalid phrase"}
* Thrown if the given phase is invalid.
*/
this.setPhrase = (phrase, walletIndex = 0) => {
if (this.phrase !== phrase) {
if (!xchainCrypto.validatePhrase(phrase)) {
throw new Error('Invalid phrase');
}
this.phrase = phrase;
}
return this.getAddress(walletIndex);
};
/**
* @private
* Private function to get Keyring pair for polkadotjs provider.
* @see https://polkadot.js.org/docs/api/start/keyring/#creating-a-keyring-instance
*
* @returns {KeyringPair} The keyring pair to be used to generate wallet address.
* */
this.getKeyringPair = (index) => {
const key = new api_1.Keyring({ ss58Format: this.getSS58Format(), type: 'ed25519' });
return key.createFromUri(`${this.phrase}//${this.getFullDerivationPath(index)}`);
};
/**
* @private
* Private function to get the polkadotjs API provider.
*
* @see https://polkadot.js.org/docs/api/start/create#api-instance
*
* @returns {ApiPromise} The polkadotjs API provider based on the network.
* */
this.getAPI = () => tslib_1.__awaiter(this, void 0, void 0, function* () {
try {
const api = new api_1.ApiPromise({ provider: new api_1.WsProvider(this.getWsEndpoint()) });
yield api.isReady;
if (!api.isConnected) {
yield api.connect();
}
return api;
}
catch (error) {
return Promise.reject(error);
}
});
/**
* Validate the given address.
* @see https://polkadot.js.org/docs/util-crypto/examples/validate-address
*
* @param {Address} address
* @returns {boolean} `true` or `false`
*/
this.validateAddress = (address) => {
try {
const key = new api_1.Keyring({ ss58Format: this.getSS58Format(), type: 'ed25519' });
return key.encodeAddress(util_1.isHex(address) ? util_1.hexToU8a(address) : key.decodeAddress(address)) === address;
}
catch (error) {
return false;
}
};
/**
* Get the current address.
*
* Generates a network-specific key-pair by first converting the buffer to a Wallet-Import-Format (WIF)
* The address is then decoded into type P2WPKH and returned.
*
* @returns {Address} The current address.
*
* @throws {"Address not defined"} Thrown if failed creating account from phrase.
*/
this.getAddress = (index = 0) => tslib_1.__awaiter(this, void 0, void 0, function* () {
const address = yield Promise.resolve(this.getKeyringPair(index).address);
return address;
});
/**
* Get the DOT balance of a given address.
*
* @param {Address} address By default, it will return the balance of the current wallet. (optional)
* @returns {Array<Balance>} The DOT balance of the address.
*/
this.getBalance = (address, assets) => tslib_1.__awaiter(this, void 0, void 0, function* () {
try {
const response = yield axios_1.default
.post(`${this.getClientUrl()}/api/open/account`, { address: address || this.getAddress() })
.then((res) => res.data);
if (!util_2.isSuccess(response)) {
throw new Error('Invalid address');
}
const account = response.data;
return account && (!assets || assets.filter((asset) => xchain_util_1.assetToString(types_1.AssetDOT) === xchain_util_1.assetToString(asset)).length)
? [
{
asset: types_1.AssetDOT,
amount: xchain_util_1.assetToBase(xchain_util_1.assetAmount(account.balance, util_2.getDecimal(this.network))),
},
]
: [];
}
catch (error) {
return Promise.reject(error);
}
});
/**
* Get transaction history of a given address with pagination options.
* By default it will return the transaction history of the current wallet.
*
* @param {TxHistoryParams} params The options to get transaction history. (optional)
* @returns {TxsPage} The transaction history.
*/
this.getTransactions = (params) => tslib_1.__awaiter(this, void 0, void 0, function* () {
var _a, _b;
const limit = (_a = params === null || params === void 0 ? void 0 : params.limit) !== null && _a !== void 0 ? _a : 10;
const offset = (_b = params === null || params === void 0 ? void 0 : params.offset) !== null && _b !== void 0 ? _b : 0;
try {
const response = yield axios_1.default
.post(`${this.getClientUrl()}/api/scan/transfers`, {
address: params === null || params === void 0 ? void 0 : params.address,
row: limit,
page: offset,
})
.then((res) => res.data);
if (!util_2.isSuccess(response) || !response.data) {
throw new Error('Failed to get transactions');
}
const transferResult = response.data;
return {
total: transferResult.count,
txs: (transferResult.transfers || []).map((transfer) => ({
asset: types_1.AssetDOT,
from: [
{
from: transfer.from,
amount: xchain_util_1.assetToBase(xchain_util_1.assetAmount(transfer.amount, util_2.getDecimal(this.network))),
},
],
to: [
{
to: transfer.to,
amount: xchain_util_1.assetToBase(xchain_util_1.assetAmount(transfer.amount, util_2.getDecimal(this.network))),
},
],
date: new Date(transfer.block_timestamp * 1000),
type: 'transfer',
hash: transfer.hash,
binanceFee: null,
confirmations: null,
ethCumulativeGasUsed: null,
ethGas: null,
ethGasPrice: null,
ethGasUsed: null,
ethTokenName: null,
ethTokenSymbol: null,
memo: null,
})),
};
}
catch (error) {
return Promise.reject(error);
}
});
/**
* Get the transaction details of a given transaction id.
*
* @param {string} txId The transaction id.
* @returns {Tx} The transaction details of the given transaction id.
*/
this.getTransactionData = (txId) => tslib_1.__awaiter(this, void 0, void 0, function* () {
try {
const response = yield axios_1.default
.post(`${this.getClientUrl()}/api/scan/extrinsic`, {
hash: txId,
})
.then((res) => res.data);
if (!util_2.isSuccess(response) || !response.data) {
throw new Error('Failed to get transactions');
}
const extrinsic = response.data;
const transfer = extrinsic.transfer;
return {
asset: types_1.AssetDOT,
from: [
{
from: transfer.from,
amount: xchain_util_1.assetToBase(xchain_util_1.assetAmount(transfer.amount, util_2.getDecimal(this.network))),
},
],
to: [
{
to: transfer.to,
amount: xchain_util_1.assetToBase(xchain_util_1.assetAmount(transfer.amount, util_2.getDecimal(this.network))),
},
],
date: new Date(extrinsic.block_timestamp * 1000),
type: 'transfer',
hash: extrinsic.extrinsic_hash,
binanceFee: null,
confirmations: null,
ethCumulativeGasUsed: null,
ethGas: null,
ethGasPrice: null,
ethGasUsed: null,
ethTokenName: null,
ethTokenSymbol: null,
memo: null,
};
}
catch (error) {
return Promise.reject(error);
}
});
/**
* Transfer DOT.
*
* @param {TxParams} params The transfer options.
* @returns {TxHash} The transaction hash.
*/
this.transfer = (params) => tslib_1.__awaiter(this, void 0, void 0, function* () {
try {
const api = yield this.getAPI();
let transaction = null;
const walletIndex = params.walletIndex || 0;
// Createing a transfer
const transfer = api.tx.balances.transfer(params.recipient, params.amount.amount().toString());
if (!params.memo) {
// Send a simple transfer
transaction = transfer;
}
else {
// Send a `utility.batch` with two Calls: i) Balance.Transfer ii) System.Remark
// Creating a remark
const remark = api.tx.system.remark(params.memo);
// Send the Batch Transaction
transaction = api.tx.utility.batch([transfer, remark]);
}
// Check balances
const paymentInfo = yield transaction.paymentInfo(this.getKeyringPair(walletIndex));
const fee = xchain_util_1.baseAmount(paymentInfo.partialFee.toString(), util_2.getDecimal(this.network));
const balances = yield this.getBalance(yield this.getAddress(walletIndex), [types_1.AssetDOT]);
if (!balances || params.amount.amount().plus(fee.amount()).isGreaterThan(balances[0].amount.amount())) {
throw new Error('insufficient balance');
}
const txHash = yield transaction.signAndSend(this.getKeyringPair(walletIndex));
yield api.disconnect();
return txHash.toString();
}
catch (error) {
return Promise.reject(error);
}
});
/**
* Get the current fee with transfer options.
*
* @see https://polkadot.js.org/docs/api/cookbook/tx/#how-do-i-estimate-the-transaction-fees
*
* @param {TxParams} params The transfer options.
* @returns {Fees} The estimated fees with the transfer options.
*/
this.estimateFees = (params) => tslib_1.__awaiter(this, void 0, void 0, function* () {
try {
const walletIndex = params.walletIndex ? params.walletIndex : 0;
const api = yield this.getAPI();
const info = yield api.tx.balances
.transfer(params.recipient, params.amount.amount().toNumber())
.paymentInfo(this.getKeyringPair(walletIndex));
const fee = xchain_util_1.baseAmount(info.partialFee.toString(), util_2.getDecimal(this.network));
yield api.disconnect();
return {
type: 'byte',
average: fee,
fast: fee,
fastest: fee,
};
}
catch (error) {
return Promise.reject(error);
}
});
/**
* Get the current fee.
*
* @returns {Fees} The current fee.
*/
this.getFees = () => tslib_1.__awaiter(this, void 0, void 0, function* () {
return yield this.estimateFees({
recipient: yield this.getAddress(),
amount: xchain_util_1.baseAmount(0, util_2.getDecimal(this.network)),
});
});
this.network = network;
this.rootDerivationPaths = rootDerivationPaths;
}
/**
* Get getFullDerivationPath
*
* @param {number} index the HD wallet index
* @returns {string} The polkadot derivation path based on the network.
*/
getFullDerivationPath(index = 0) {
// console.log(this.rootDerivationPaths[this.network])
if (index === 0) {
// this should make the tests backwards compatible
return this.rootDerivationPaths[this.network];
}
else {
return this.rootDerivationPaths[this.network] + `//${index}`;
}
}
/**
* Set/update the current network.
*
* @param {Network} network `mainnet` or `testnet`.
*
* @throws {"Network must be provided"}
* Thrown if network has not been set before.
*/
setNetwork(network) {
if (!network) {
throw new Error('Network must be provided');
}
else {
if (network !== this.network) {
this.network = network;
}
}
}
/**
* Get the current network.
*
* @returns {Network} The current network. (`mainnet` or `testnet`)
*/
getNetwork() {
return this.network;
}
}
exports.Client = Client;
//# sourceMappingURL=client.js.map