@xchainjs/xchain-radix
Version:
Custom Radix client and utilities used by XChainJS clients
906 lines (893 loc) • 42.4 kB
JavaScript
import { NetworkId, ManifestBuilder, decimal, address, bucket, enumeration, generateRandomNonce, RadixEngineToolkit, Convert, str, PrivateKey, PublicKey, LTSRadixEngineToolkit, TransactionBuilder } from '@radixdlt/radix-engine-toolkit';
import { Network, BaseXChainClient, singleFee, FeeType, TxType } from '@xchainjs/xchain-client';
import { getSeed } from '@xchainjs/xchain-crypto';
import { AssetType, assetToBase, assetAmount, eqAsset, baseAmount } from '@xchainjs/xchain-util';
import { bech32m } from '@scure/base';
import { HDKey } from '@scure/bip32';
import slip10 from 'micro-key-producer/slip10.js';
/******************************************************************************
Copyright (c) Microsoft Corporation.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
PERFORMANCE OF THIS SOFTWARE.
***************************************************************************** */
/* global Reflect, Promise, SuppressedError, Symbol, Iterator */
function __awaiter(thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
}
typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
var e = new Error(message);
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
};
/**
* Chain identifier for Radix.
* This constant represents the identifier for the Radix Chain.
*/
const RadixChain = 'XRD';
const MAINNET_GATEWAY_URL = 'https://mainnet.radixdlt.com';
const STOKENET_GATEWAY_URL = 'https://stokenet.radixdlt.com';
const XRD_DECIMAL = 18;
const RADIX_ASSET_RESOURCE = 'resource_rdx1tknxxxxxxxxxradxrdxxxxxxxxx009923554798xxxxxxxxxradxrd';
const AssetXRD = {
symbol: `XRD`,
ticker: 'XRD',
type: AssetType.NATIVE,
chain: RadixChain,
};
const xrdRootDerivationPaths = {
[Network.Mainnet]: "m/44'/1022'/1'/525'/1460'",
[Network.Stagenet]: "m/44'/1022'/2'/525'/1460'",
[Network.Testnet]: "m/44'/1022'/2'/525'/1460'",
};
const bech32Networks = {
1: 'rdx',
2: 'tdx',
3: 'tdx',
};
const bech32Lengths = {
1: 66,
2: 69,
3: 69,
};
const feesEstimationPublicKeys = {
[NetworkId.Mainnet]: {
from: 'account_rdx12xh48d5s9u7me5t49z25lrm4h73wclqpjvumd49ctf0ggnyazc62m8',
to: 'account_rdx1685t40mreptjhs9g3pg9lgf7k7rgppzjeknjgrpc7d0sumcjrsw6kj',
resourceAddress: RADIX_ASSET_RESOURCE,
publicKey: 'a47e22f21e16d80374f16d66224b56f6eda82a6db8279de267a74a49f0291e8b',
},
[NetworkId.Stokenet]: {
from: 'account_tdx_2_12927ya6vxtmhu8w0qkwtumw8kjmlv930agjzezfgg6yp3j6agn3gfc',
to: 'account_tdx_2_12xnfu4evyseqeq57rzhrh8ls6wy76vvc4jnw2kzx3l5ka7wyddxh3l',
resourceAddress: RADIX_ASSET_RESOURCE,
publicKey: '3ce4d36fd8bf40fa6f9b0cf2ef8d11853d088589ebdc79055f5a0af55bf7e758',
},
};
/**
* Lightweight Radix Gateway API client replacing the heavy
* @radixdlt/babylon-gateway-api-sdk (~2.7 MB).
*
* Only implements the endpoints actually used by xchain-radix.
*/
// #endregion Types
// #region API Client
const NETWORK_URLS = {
1: 'https://mainnet.radixdlt.com',
2: 'https://stokenet.radixdlt.com',
};
class RadixGatewayApi {
constructor(networkId) {
const url = NETWORK_URLS[networkId];
if (!url)
throw new Error(`Unsupported Radix network ID: ${networkId}`);
this.baseUrl = url;
}
post(path, body) {
return __awaiter(this, void 0, void 0, function* () {
const response = yield fetch(`${this.baseUrl}${path}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
});
if (!response.ok) {
throw new Error(`Radix Gateway API error: ${response.status} ${response.statusText}`);
}
return response.json();
});
}
getStatus() {
return __awaiter(this, void 0, void 0, function* () {
return this.post('/status/gateway-status', {});
});
}
getEntityDetailsVaultAggregated(addresses) {
return __awaiter(this, void 0, void 0, function* () {
const response = yield this.post('/state/entity/details', {
addresses,
aggregation_level: 'Vault',
});
return response.items;
});
}
getEntityFungiblesPage(request) {
return __awaiter(this, void 0, void 0, function* () {
return this.post('/state/entity/page/fungibles/', request);
});
}
getEntityNonFungiblesPage(request) {
return __awaiter(this, void 0, void 0, function* () {
return this.post('/state/entity/page/non-fungibles/', request);
});
}
previewTransaction(request) {
return __awaiter(this, void 0, void 0, function* () {
return this.post('/transaction/preview', request);
});
}
submitTransaction(notarizedTransactionHex) {
return __awaiter(this, void 0, void 0, function* () {
return this.post('/transaction/submit', {
notarized_transaction_hex: notarizedTransactionHex,
});
});
}
getTransactionDetails(request) {
return __awaiter(this, void 0, void 0, function* () {
const response = yield this.post('/transaction/committed-details', request);
if (response.transaction.confirmed_at) {
response.transaction.confirmed_at = new Date(response.transaction.confirmed_at);
}
return response;
});
}
getStreamTransactions(request) {
return __awaiter(this, void 0, void 0, function* () {
const response = yield this.post('/stream/transactions', request);
for (const item of response.items) {
if (item.confirmed_at) {
item.confirmed_at = new Date(item.confirmed_at);
}
}
return response;
});
}
getTransactionStatus(intentHash) {
return __awaiter(this, void 0, void 0, function* () {
return this.post('/transaction/status', { intent_hash: intentHash });
});
}
}
// #endregion API Client
/**
* The main client for the Radix network which is then wrapped by the {Client} adapting it to have
* a {BaseXChainClient} interface.
*/
class RadixSpecificClient {
constructor(networkId) {
this.innerNetwork = networkId;
this.innerGateway = new RadixGatewayApi(networkId);
}
// #region Getters & Setters
set networkId(networkId) {
this.innerNetwork = networkId;
this.innerGateway = new RadixGatewayApi(networkId);
}
get networkId() {
return this.innerNetwork;
}
get gateway() {
return this.innerGateway;
}
// #endregion
// #region Public Methods
currentEpoch() {
return __awaiter(this, void 0, void 0, function* () {
return this.innerGateway.getStatus().then((status) => status.ledger_state.epoch);
});
}
currentStateVersion() {
return __awaiter(this, void 0, void 0, function* () {
return this.innerGateway.getStatus().then((status) => status.ledger_state.state_version);
});
}
fetchBalances(address) {
return __awaiter(this, void 0, void 0, function* () {
const fungibleResources = yield this.fetchFungibleResources(address);
const fungibleBalances = this.convertResourcesToBalances(fungibleResources);
return fungibleBalances;
});
}
fetchNFTBalances(address) {
return __awaiter(this, void 0, void 0, function* () {
const nonFungibleResources = yield this.fetchNonFungibleResources(address);
const nonFungibleBalances = this.convertResourcesToBalances(nonFungibleResources);
return nonFungibleBalances;
});
}
convertResourcesToBalances(resources) {
return __awaiter(this, void 0, void 0, function* () {
const balances = [];
const BATCH_SIZE = 50;
// Split resources into batches of up to 50 items
const resourceBatches = [];
for (let i = 0; i < resources.length; i += BATCH_SIZE) {
resourceBatches.push(resources.slice(i, i + BATCH_SIZE));
}
for (const batch of resourceBatches) {
const addresses = batch.map((item) => item.resource_address);
const response = yield this.gateway.getEntityDetailsVaultAggregated(addresses);
const divisibilities = new Map();
response.forEach((result) => {
var _a;
if (result.details !== undefined) {
if (result.details.type === 'FungibleResource') {
divisibilities.set(result.address, (_a = result.details.divisibility) !== null && _a !== void 0 ? _a : 0);
}
}
});
batch.forEach((item) => {
if (item.aggregation_level === 'Global') {
const asset = item.resource_address === RADIX_ASSET_RESOURCE
? AssetXRD
: {
chain: RadixChain,
symbol: item.resource_address,
ticker: item.resource_address,
type: AssetType.TOKEN,
};
const divisibility = divisibilities.get(item.resource_address) || 0;
// We need to do this because item.amount can be either string or number
// depending on the type of resource
const amount = typeof item.amount === 'string' ? parseFloat(item.amount) : item.amount;
balances.push({
asset,
amount: assetToBase(assetAmount(amount, divisibility)),
});
}
});
}
// Iterate through resources
return balances;
});
}
fetchNonFungibleResources(address) {
return __awaiter(this, void 0, void 0, function* () {
let hasNextPage = true;
let nextCursor = undefined;
const stateVersion = yield this.currentStateVersion();
let nonFungibleResources = [];
while (hasNextPage) {
const stateEntityNonFungiblesPageRequest = {
address: address,
limit_per_page: 5,
cursor: nextCursor,
at_ledger_state: {
state_version: stateVersion,
},
};
const stateEntityNonFungiblesPageResponse = yield this.gateway.getEntityNonFungiblesPage(stateEntityNonFungiblesPageRequest);
nonFungibleResources = nonFungibleResources.concat(stateEntityNonFungiblesPageResponse.items);
if (stateEntityNonFungiblesPageResponse.next_cursor) {
nextCursor = stateEntityNonFungiblesPageResponse.next_cursor;
}
else {
hasNextPage = false;
}
}
return nonFungibleResources;
});
}
fetchFungibleResources(address) {
return __awaiter(this, void 0, void 0, function* () {
let hasNextPage = true;
let nextCursor = undefined;
let fungibleResources = [];
const stateVersion = yield this.currentStateVersion();
while (hasNextPage) {
const stateEntityFungiblesPageRequest = {
address: address,
limit_per_page: 100,
cursor: nextCursor,
at_ledger_state: {
state_version: stateVersion,
},
};
const stateEntityFungiblesPageResponse = yield this.gateway.getEntityFungiblesPage(stateEntityFungiblesPageRequest);
fungibleResources = fungibleResources.concat(stateEntityFungiblesPageResponse.items);
if (stateEntityFungiblesPageResponse.next_cursor) {
nextCursor = stateEntityFungiblesPageResponse.next_cursor;
}
else {
hasNextPage = false;
}
}
return fungibleResources;
});
}
constructTransferIntent(from, to, resourceAddress, amount, notaryPublicKey, message, methodsToCall) {
return __awaiter(this, void 0, void 0, function* () {
// This nonce will be used for preview and also when constructing the final transaction
const nonce = generateRandomNonce();
// Construct the intent with a random fee lock, say 5 XRD and then create a transaction intent
// from it.
const manifestWithHardcodedFee = methodsToCall
? RadixSpecificClient.createCustomTransferManifest(from, resourceAddress, amount, 5, methodsToCall)
: RadixSpecificClient.createSimpleTransferManifest(from, to, resourceAddress, amount, 5);
const intentWithHardcodedFee = yield this.constructIntent(manifestWithHardcodedFee, message === null || message === undefined
? { kind: 'None' }
: {
kind: 'PlainText',
value: { mimeType: 'text/plain', message: { kind: 'String', value: message } },
}, nonce, notaryPublicKey);
const previewReceipt = (yield this.previewIntent(intentWithHardcodedFee));
// Ensure that the preview was successful.
if (previewReceipt.receipt.status !== 'Succeeded') {
throw new Error(`Preview for fees was not successful.`);
}
// Calculate the total fees
const totalFees = [
previewReceipt.receipt.fee_summary.xrd_total_execution_cost,
previewReceipt.receipt.fee_summary.xrd_total_finalization_cost,
previewReceipt.receipt.fee_summary.xrd_total_royalty_cost,
previewReceipt.receipt.fee_summary.xrd_total_storage_cost,
previewReceipt.receipt.fee_summary.xrd_total_tipping_cost,
]
.map(parseFloat)
.reduce((acc, item) => acc + item, 0);
// We need to add another 10% to the fees as the preview response does not include everything needed
// to actually submit the transaction, ie: signature validation
const totalFeesPlus10Percent = totalFees * 1.1;
// Construct a new intent with the calculated fees.
const manifest = methodsToCall
? RadixSpecificClient.createCustomTransferManifest(from, resourceAddress, amount, totalFeesPlus10Percent, methodsToCall)
: RadixSpecificClient.createSimpleTransferManifest(from, to, resourceAddress, amount, totalFeesPlus10Percent);
const intent = yield this.constructIntent(manifest, message === null || message === undefined
? { kind: 'None' }
: {
kind: 'PlainText',
value: { mimeType: 'text/plain', message: { kind: 'String', value: message } },
}, nonce, notaryPublicKey);
return {
intent,
fees: totalFees,
};
});
}
submitTransaction(notarizedTransaction) {
return __awaiter(this, void 0, void 0, function* () {
const intentHash = yield RadixEngineToolkit.NotarizedTransaction.intentHash(notarizedTransaction);
const transactionHex = yield RadixEngineToolkit.NotarizedTransaction.compile(notarizedTransaction).then(Convert.Uint8Array.toHexString);
const response = yield this.innerGateway.submitTransaction(transactionHex);
return [response, intentHash];
});
}
// #endregion Public Methods
// #region Private Methods
static createSimpleTransferManifest(from, to, resourceAddress, amount, amountToLockForFees) {
return new ManifestBuilder()
.callMethod(from, 'lock_fee', [decimal(amountToLockForFees)])
.callMethod(from, 'withdraw', [address(resourceAddress), decimal(amount)])
.takeFromWorktop(resourceAddress, decimal(amount).value, (builder, bucketId) => {
return builder.callMethod(to, 'try_deposit_or_abort', [bucket(bucketId), enumeration(0)]);
})
.build();
}
static createCustomTransferManifest(from, resourceAddress, amount, amountToLockForFees, methodsToCall) {
const simpletTx = new ManifestBuilder()
.callMethod(from, 'lock_fee', [decimal(amountToLockForFees)])
.callMethod(from, 'withdraw', [address(resourceAddress), decimal(amount)])
.takeFromWorktop(resourceAddress, decimal(amount).value, (builder) => {
return builder.callMethod(methodsToCall.address, methodsToCall.methodName, methodsToCall.params);
});
return simpletTx.build();
}
constructIntent(manifest, message, nonce, notaryPublicKey) {
return __awaiter(this, void 0, void 0, function* () {
const epoch = yield this.currentEpoch();
return {
header: {
networkId: this.networkId,
startEpochInclusive: epoch,
endEpochExclusive: epoch + 10,
nonce,
notaryPublicKey,
notaryIsSignatory: true,
tipPercentage: 0,
},
manifest,
message,
};
});
}
previewIntent(intent) {
return __awaiter(this, void 0, void 0, function* () {
// Translate the RET models to the gateway models for preview.
const request = {
manifest: yield RadixEngineToolkit.Instructions.convert(intent.manifest.instructions, this.networkId, 'String').then((instructions) => instructions.value),
blobs_hex: [],
start_epoch_inclusive: intent.header.startEpochInclusive,
end_epoch_exclusive: intent.header.endEpochExclusive,
notary_public_key: RadixSpecificClient.retPublicKeyToGatewayPublicKey(intent.header.notaryPublicKey),
notary_is_signatory: intent.header.notaryIsSignatory,
tip_percentage: intent.header.tipPercentage,
nonce: intent.header.nonce,
signer_public_keys: [RadixSpecificClient.retPublicKeyToGatewayPublicKey(intent.header.notaryPublicKey)],
// TODO: Add message
flags: {
assume_all_signature_proofs: false,
skip_epoch_check: false,
use_free_credit: false,
},
};
return this.innerGateway.previewTransaction(request);
});
}
static retPublicKeyToGatewayPublicKey(publicKey) {
switch (publicKey.curve) {
case 'Secp256k1':
return {
key_type: 'EcdsaSecp256k1',
key_hex: publicKey.hex(),
};
case 'Ed25519':
return {
key_type: 'EddsaEd25519',
key_hex: publicKey.hex(),
};
}
}
}
/**
* Returns the resource id of an asset
* @param {Asset | TokenAsset} asset asset
* @returns Resource id
*/
const getAssetResource = (asset) => {
if (eqAsset(asset, AssetXRD))
return RADIX_ASSET_RESOURCE;
return asset.symbol.slice(asset.ticker.length + 1);
};
/**
* Generates a address param for a call method
* @param {Address} addr Address to transform to Radix Address parameter
* @returns the address in the Radix Address parameter format
*/
const generateAddressParam = (addr) => address(addr);
/**
* Generates a string param for a call method
* @param {string} s Address to transform to Radix string parameter
* @returns the string in the Radix String parameter format
*/
const generateStringParam = (s) => str(s);
/**
* Generates a bucket param for a call method
* @param {number} value Value to transform to Radix Bucket parameter
* @returns the value in the Radix Bucket parameter format
*/
const generateBucketParam = (value) => bucket(value);
const xChainJsNetworkToRadixNetworkId = (network) => {
switch (network) {
case Network.Mainnet:
case Network.Stagenet:
return NetworkId.Mainnet;
case Network.Testnet:
return NetworkId.Stokenet;
}
};
/**
* Custom Radix client
*/
class Client extends BaseXChainClient {
constructor({ network = Network.Mainnet, phrase, rootDerivationPaths = xrdRootDerivationPaths, feeBounds = {
lower: 0,
upper: 3,
}, curve = 'Ed25519', }) {
super(RadixChain, {
network: network,
phrase: phrase,
rootDerivationPaths: rootDerivationPaths,
feeBounds: feeBounds,
});
this.curve = curve;
this.radixSpecificClient = new RadixSpecificClient(xChainJsNetworkToRadixNetworkId(network));
}
get radixClient() {
return this.radixSpecificClient;
}
setNetwork(network) {
super.setNetwork(network);
this.radixSpecificClient.networkId = xChainJsNetworkToRadixNetworkId(network);
}
/**
* Get an estimated fee for a test transaction that involves sending
* XRD from one account to another
*
* @returns {Fee} An estimated fee
*/
getFees() {
return __awaiter(this, void 0, void 0, function* () {
// TODO: This can fail if we use it on stokenet, we need to replace these with network aware
// addresses.
const feesInXrd = yield this.radixSpecificClient
.constructTransferIntent(feesEstimationPublicKeys[this.getRadixNetwork()].from, feesEstimationPublicKeys[this.getRadixNetwork()].to, feesEstimationPublicKeys[this.getRadixNetwork()].resourceAddress, 0, new PublicKey.Ed25519(Convert.HexString.toUint8Array(feesEstimationPublicKeys[this.getRadixNetwork()].publicKey)), 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa')
.then((result) => result.fees);
// We need to add another 10% to the fees as the preview response does not include everything needed
// to actually submit the transaction, ie: signature validation
const feeRate1e18 = feesInXrd * 1.1 * Math.pow(10, XRD_DECIMAL);
// Create the fee amount with the adjusted fee rate
const fee = baseAmount(feeRate1e18, XRD_DECIMAL);
return singleFee(FeeType.FlatFee, fee);
});
}
getRadixNetwork() {
return xChainJsNetworkToRadixNetworkId(this.getNetwork());
}
getPrivateKey(index) {
const seed = getSeed(this.phrase);
let derivationPath;
if (this.rootDerivationPaths) {
derivationPath = this.rootDerivationPaths[this.getNetwork()];
}
else {
derivationPath = xrdRootDerivationPaths[this.getNetwork()];
}
const updatedDerivationPath = derivationPath.replace(/\/$/, '') + `/${index}'`;
if (this.curve === 'Ed25519') {
const masterKey = slip10.fromMasterSeed(seed);
const childKey = masterKey.derive(updatedDerivationPath);
if (!childKey.privateKey)
throw new Error('child does not have a privateKey');
return Buffer.from(childKey.privateKey);
}
else {
const node = HDKey.fromMasterSeed(seed);
const child = node.derive(updatedDerivationPath);
if (!child.privateKey)
throw new Error('child does not have a privateKey');
return Buffer.from(child.privateKey);
}
}
getRadixPrivateKey(index) {
const privateKey = this.getPrivateKey(index);
const privateKeyBytes = Uint8Array.from(privateKey);
if (this.curve === 'Ed25519') {
return new PrivateKey.Ed25519(privateKeyBytes);
}
else {
return new PrivateKey.Secp256k1(privateKeyBytes);
}
}
/**
* Get the address for a given account.
* @deprecated Use getAddressAsync instead.
*/
getAddress() {
throw new Error('getAddress is synchronous and cannot retrieve addresses directly. Use getAddressAsync instead.');
}
/**
* Get the current address asynchronously for a given account.
* @returns {Address} A promise resolving to the current address.
* A phrase is needed to create a wallet and to derive an address from it.
*/
getAddressAsync() {
return __awaiter(this, arguments, void 0, function* (index = 0) {
const networkId = this.getRadixNetwork();
const radixPrivateKey = this.getRadixPrivateKey(index);
const address = yield LTSRadixEngineToolkit.Derive.virtualAccountAddress(radixPrivateKey.publicKey(), networkId);
return address.toString();
});
}
/**
* Get the explorer URL based on the network.
*
* @returns {string} The explorer URL based on the network.
*/
getExplorerUrl() {
switch (this.getRadixNetwork()) {
case NetworkId.Mainnet:
return 'https://dashboard.radixdlt.com';
case NetworkId.Stokenet:
return 'https://stokenet-dashboard.radixdlt.com';
default:
throw new Error('Unsupported network');
}
}
/**
* Get the explorer URL for a given account address based on the network.
* @param {Address} address The address to generate the explorer URL for.
* @returns {string} The explorer URL for the given address.
*/
getExplorerAddressUrl(address) {
return `${this.getExplorerUrl()}/account/${address}`;
}
/**
* Get the explorer URL for a given transaction ID based on the network.
* @param {string} txID The transaction ID to generate the explorer URL for.
* @returns {string} The explorer URL for the given transaction ID.
*/
getExplorerTxUrl(txID) {
return `${this.getExplorerUrl()}/transaction/${txID}`;
}
/**
* Validate the given address.
* @param {Address} address The address to validate.
* @returns {boolean} `true` if the address is valid, `false` otherwise.
*/
validateAddressAsync(address) {
return __awaiter(this, void 0, void 0, function* () {
try {
yield RadixEngineToolkit.Address.decode(address);
return true;
}
catch (_a) {
return false;
}
});
}
validateAddress(address) {
try {
const decodedAddress = bech32m.decode(address);
if (!decodedAddress.prefix.startsWith('account_')) {
return false;
}
const network = decodedAddress.prefix.split('_')[1];
if (bech32Networks[this.getRadixNetwork()] !== network) {
return false;
}
if (address.length !== bech32Lengths[this.getRadixNetwork()]) {
return false;
}
return true;
}
catch (_error) {
return false;
}
}
/**
* Retrieves the balances of a given address.
* @param {Address} address - The address to retrieve the balance for.
* @param {Asset[]} assets - Assets to retrieve the balance for (optional).
* @returns {Promise<Balance[]>} An array containing the balance of the address.
*/
getBalance(address, assets) {
return __awaiter(this, void 0, void 0, function* () {
const balances = yield this.radixSpecificClient.fetchBalances(address);
// Return native asset even if there is no balance
if (balances.findIndex((balance) => eqAsset(balance.asset, this.getAssetInfo().asset)) === -1) {
balances.push({
asset: this.getAssetInfo().asset,
amount: baseAmount(0, this.getAssetInfo().decimal),
});
}
// If assets is undefined, return all balances
if (!assets) {
return balances;
}
const filteredBalances = balances.filter((balance) => eqAsset(balance.asset, this.getAssetInfo().asset) ||
assets.some((asset) => balance.asset.symbol === asset.symbol));
return filteredBalances;
});
}
/**
* Get transaction history of a given address with pagination options.
* @param {TxHistoryParams} params The options to get transaction history. (optional)
* @returns {TxsPage} The transaction history.
*/
getTransactions(params) {
return __awaiter(this, void 0, void 0, function* () {
const { address, offset = 0, limit, asset } = params;
let hasNextPage = true;
let nextCursor = undefined;
let committedTransactions = [];
const txList = { txs: [], total: 0 };
while (hasNextPage) {
const response = yield this.radixSpecificClient.gateway.getStreamTransactions({
affected_global_entities_filter: [address],
limit_per_page: limit && limit > 100 ? 100 : limit,
from_ledger_state: {
state_version: offset,
},
manifest_resources_filter: asset ? [asset] : undefined,
opt_ins: {
raw_hex: true,
},
cursor: nextCursor,
});
committedTransactions = committedTransactions.concat(response.items);
if (response.next_cursor) {
nextCursor = response.next_cursor;
}
else {
hasNextPage = false;
}
}
for (const txn of committedTransactions) {
try {
if (txn.raw_hex !== undefined &&
txn.confirmed_at !== null &&
txn.intent_hash !== undefined &&
txn.confirmed_at !== undefined) {
const transaction = yield this.convertTransactionFromHex(txn.raw_hex, txn.intent_hash, txn.confirmed_at);
txList.txs.push(transaction);
}
}
catch (_error) { }
}
txList.total = txList.txs.length;
return txList;
});
}
/**
* Get the transaction details of a given transaction id.
* This method uses LTSRadixEngineToolkit.Transaction.summarizeTransaction
* to convert a transaction hex to a transaction summary. If the transaction was not built with
* the SimpleTransactionBuilder, the method will fail to get the transaction data
* @param {string} txId The transaction id.
* @returns {Tx} The transaction details of the given transaction id.
*/
getTransactionData(txId) {
return __awaiter(this, void 0, void 0, function* () {
try {
const transactionCommittedDetailsResponse = yield this.radixSpecificClient.gateway.getTransactionDetails({
intent_hash: txId,
opt_ins: {
raw_hex: true,
},
});
if (transactionCommittedDetailsResponse.transaction.raw_hex !== undefined &&
transactionCommittedDetailsResponse.transaction.confirmed_at !== null &&
transactionCommittedDetailsResponse.transaction.confirmed_at !== undefined &&
transactionCommittedDetailsResponse.transaction.intent_hash !== undefined) {
const transaction = yield this.convertTransactionFromHex(transactionCommittedDetailsResponse.transaction.raw_hex, transactionCommittedDetailsResponse.transaction.intent_hash, transactionCommittedDetailsResponse.transaction.confirmed_at);
return transaction;
}
else {
throw new Error('Incomplete transaction data received');
}
}
catch (_error) {
throw new Error('Failed to fetch transaction data');
}
});
}
/**
* Helper function to convert a transaction in hex, returned by the gateway to a Tx type
* @param transaction_hex - The raw_hex returned by the gateway for a transaction id
* @param confirmed_at - The confirmed_at date for the transaction
* @param intent_hash - The transaction intent hash
* @returns a transaction in Tx type
*/
convertTransactionFromHex(transaction_hex, intent_hash, confirmed_at) {
return __awaiter(this, void 0, void 0, function* () {
const transactionBinary = Convert.HexString.toUint8Array(transaction_hex);
try {
const transactionSummary = yield LTSRadixEngineToolkit.Transaction.summarizeTransaction(transactionBinary);
const from = [];
const to = [];
// Iterate over withdraws
for (const withdrawAccount in transactionSummary.withdraws) {
for (const withdrawResource in transactionSummary.withdraws[withdrawAccount]) {
const withdrawAmount = transactionSummary.withdraws[withdrawAccount][withdrawResource].toNumber();
from.push({
from: withdrawAccount,
amount: baseAmount(withdrawAmount),
asset: { symbol: withdrawResource, ticker: withdrawResource, type: AssetType.TOKEN, chain: RadixChain },
});
}
}
// Iterate over deposits
for (const depositAccount in transactionSummary.deposits) {
for (const depositResource in transactionSummary.deposits[depositAccount]) {
const depositAmount = transactionSummary.deposits[depositAccount][depositResource].toNumber();
to.push({
to: depositAccount,
amount: baseAmount(depositAmount),
asset: { symbol: depositResource, ticker: depositResource, type: AssetType.TOKEN, chain: RadixChain },
});
}
}
const transaction = {
from: from,
to: to,
date: confirmed_at,
type: TxType.Transfer,
hash: intent_hash,
asset: { symbol: '', ticker: '', type: AssetType.TOKEN, chain: RadixChain },
};
return transaction;
}
catch (_error) {
return {
from: [],
to: [],
asset: AssetXRD,
date: confirmed_at,
type: TxType.Unknown,
hash: intent_hash,
};
}
});
}
/**
* Creates a transaction using the SimpleTransactionBuilder, signs it with the
* private key and returns the signed hex
* @param params - The transactions params
* @returns A signed transaction hex
*/
transfer(params) {
return __awaiter(this, void 0, void 0, function* () {
var _a;
const walletIndex = (_a = params.walletIndex) !== null && _a !== void 0 ? _a : 0;
const intent = yield this.prepareTx(params)
.then((response) => response.rawUnsignedTx)
.then(Convert.HexString.toUint8Array)
.then(RadixEngineToolkit.Intent.decompile);
const notarizedTransaction = yield TransactionBuilder.new().then((builder) => {
return builder
.header(intent.header)
.message(intent.message)
.manifest(intent.manifest)
.notarize(this.getRadixPrivateKey(walletIndex));
});
const notarizedTransactionBytes = yield RadixEngineToolkit.NotarizedTransaction.compile(notarizedTransaction);
const transactionId = yield this.broadcastTx(Convert.Uint8Array.toHexString(notarizedTransactionBytes));
return transactionId;
});
}
/**
* Submits a transaction
* @param txHex - The transaction hex build with the transfer method
* @returns - The response from the gateway
*/
broadcastTx(txHex) {
return __awaiter(this, void 0, void 0, function* () {
const notarizedTransaction = yield RadixEngineToolkit.NotarizedTransaction.decompile(Convert.HexString.toUint8Array(txHex));
const response = yield this.radixSpecificClient.submitTransaction(notarizedTransaction);
return response[1].id;
});
}
/**
* Prepares a transaction to be used by the transfer method
* It will include a non signed transaction
* @param params - The transaction params
* @returns a PreparedTx
*/
prepareTx(params) {
return __awaiter(this, void 0, void 0, function* () {
var _a;
const walletIndex = (_a = params.walletIndex) !== null && _a !== void 0 ? _a : 0;
const from = yield this.getAddressAsync();
const transferAmmount = params.amount.amount().toNumber() / Math.pow(10, XRD_DECIMAL);
const intent = yield this.radixSpecificClient
.constructTransferIntent(from, params.recipient, getAssetResource(params.asset || this.getAssetInfo().asset), transferAmmount, this.getRadixPrivateKey(walletIndex).publicKey(), params.memo, params.methodToCall)
.then((response) => response.intent);
const compiledIntent = yield RadixEngineToolkit.Intent.compile(intent);
return {
rawUnsignedTx: Convert.Uint8Array.toHexString(compiledIntent),
};
});
}
/**
* Get asset information.
* @returns Asset information.
*/
getAssetInfo() {
return {
asset: AssetXRD,
decimal: XRD_DECIMAL,
};
}
}
export { AssetXRD, Client, MAINNET_GATEWAY_URL, RADIX_ASSET_RESOURCE, RadixChain, STOKENET_GATEWAY_URL, XRD_DECIMAL, bech32Lengths, bech32Networks, feesEstimationPublicKeys, generateAddressParam, generateBucketParam, generateStringParam, getAssetResource, xrdRootDerivationPaths };