UNPKG

shield-bridge-sdk

Version:
918 lines (917 loc) 58.8 kB
var __awaiter = (this && this.__awaiter) || function (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()); }); }; var __rest = (this && this.__rest) || function (s, e) { var t = {}; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) t[p] = s[p]; if (s != null && typeof Object.getOwnPropertySymbols === "function") for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) { if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) t[p[i]] = s[p[i]]; } return t; }; import { Buffer } from 'buffer'; import { spawn, Thread, Worker } from 'threads'; import { OpKind, } from '@taquito/taquito'; import { defaults, tokensGetTokens, } from '@tzkt/sdk-api'; import BigNumber from 'bignumber.js'; // Make Buffer available globally for @taquito dependencies if (typeof window !== 'undefined' && !window.Buffer) { window.Buffer = Buffer; } export const tzktApiMap = { mainnet: 'https://api.tzkt.io', ghostnet: 'https://api.ghostnet.tzkt.io', }; export const saplingStateMapContract = { mainnet: 'KT1RYEs6rfXgHqeb2XzfHKRii5NsNyKbS6WM', ghostnet: 'KT1WorWEWjfQqQ1X2BFQiCc4hE3DuDKQVH4U', }; var OperationIndex; (function (OperationIndex) { OperationIndex[OperationIndex["UPDATE_OPERATORS_ADD_INDEX"] = 0] = "UPDATE_OPERATORS_ADD_INDEX"; OperationIndex[OperationIndex["APPROVE_INDEX"] = 1] = "APPROVE_INDEX"; OperationIndex[OperationIndex["DEFAULT_INDEX"] = 2] = "DEFAULT_INDEX"; OperationIndex[OperationIndex["UPDATE_OPERATORS_REMOVE_INDEX"] = 3] = "UPDATE_OPERATORS_REMOVE_INDEX"; })(OperationIndex || (OperationIndex = {})); const MINIMAL_FEE_MUTEZ = 100; const MINIMAL_FEE_PER_BYTE_MUTEZ = 1; const MINIMAL_FEE_PER_GAS_MUTEZ = 0.1; const isBrowser = typeof window !== 'undefined' && typeof window.document !== 'undefined'; // Default to loading the unbundled worker let workerUrl = './worker'; if (isBrowser) { // Load the worker bundle in the browser environment workerUrl = new URL('./workerBundle.js', import.meta.url).href; } /** * ShieldBridgeSDK provides an abstraction to interact with the Shield Bridge smart contract * to shield, unshield, and transfer sapling tokens. * * The SDK supports two modes of operation: * * 1. **Full Access Mode** (with spending key or mnemonic): * - Can perform all operations: shield, unshield, transfer * - Can query balances and transactions * - Can export viewing keys for read-only access * * 2. **View-Only Mode** (with viewing key): * - Can only query balances and transactions * - Cannot perform transaction operations * - Useful for auditing, monitoring, and compliance * * @class * @param {ShieldBridgeSDKConfig} config The configuration object for the Shield Bridge SDK * @param {TezosToolkit} config.client The TezosToolkit instance * @param {'mainnet' | 'ghostnet'} [config.tzktApi='mainnet'] The tzkt API to use * @param {number} [config.minConfirmations=1] The minimum number of confirmations for the transaction * @param {string} [config.saplingStateMapContract='KT1RYEs6rfXgHqeb2XzfHKRii5NsNyKbS6WM'] The sapling state map contract address * @param {number} [config.gasLimitBuffer=2_000] The buffer to add to the estimated gas limit * @param {number} [config.storageLimitBuffer=500] The buffer to add to the estimated storage limit * @param {boolean} [config.useBaseUnits=false] Whether to use base unit for the token amounts (mutez or token units with decimals) * @param {boolean} [config.parallelThreads=false] Whether to spawn parallel threads for the sapling worker * @param {string} [config.saplingSecret] The sapling secret key (for full access mode) * @param {string} [config.saplingMnemonic] The sapling mnemonic (for full access mode) * @param {string} [config.saplingViewingKey] The sapling viewing key (for view-only mode) * @returns {ShieldBridgeSDK} The Shield Bridge SDK instance * * @example * // Full access mode with secret key * const tezos = new TezosToolkit('https://mainnet.api.tez.ie'); * const signerProvider = await InMemorySigner.fromSecretKey('edsk...'); * tezos.setSignerProvider(signerProvider); * const shieldBridge = new ShieldBridgeSDK({ * client: tezos, * saplingSecret: 'sask...' * }); * await shieldBridge.shield([ * { * amount: 1, * contract: 'KT1...', * tokenId: 0, * memo: 'abcdefgh' * } * ]); * * @example * // Export viewing key for read-only access * const viewingKey = await shieldBridge.getViewingKey(); * * @example * // View-only mode with viewing key * const viewOnlySdk = new ShieldBridgeSDK({ * client: tezos, * saplingViewingKey: 'abc123...' * }); * const balance = await viewOnlySdk.getShieldedBalance(); * console.log('View-only mode:', viewOnlySdk.isViewOnlyMode); // true */ export class ShieldBridgeSDK { constructor(config) { var _a, _b, _c, _d, _e, _f; this.config = config; // Cache for saplingIds, token decimals, and token metadata this.saplingIdCache = new Map(); this.tokenDecimalsCache = new Map(); this.tokenMetadataCache = new Map(); // Cache for contract instances this.walletContractCache = new Map(); this.estimatorContractCache = new Map(); this.initializeSaplingWorker = () => __awaiter(this, void 0, void 0, function* () { // Wait for the worker to be ready yield new Promise((resolve) => { setTimeout(resolve, 2000); }); try { this.saplingWorker = yield spawn(new Worker(workerUrl), { timeout: 120000, }); return true; } catch (err) { console.log(err.message); throw new Error('Failed to initialize Sapling worker'); } }); /** * @description Get cached wallet contract or fetch and cache it * @param contractAddress The contract address */ this.getWalletContract = (contractAddress) => __awaiter(this, void 0, void 0, function* () { if (this.walletContractCache.has(contractAddress)) { return this.walletContractCache.get(contractAddress); } const contract = yield this.tezosClient.wallet.at(contractAddress); this.walletContractCache.set(contractAddress, contract); return contract; }); /** * @description Get cached estimator contract or fetch and cache it * @param contractAddress The contract address */ this.getEstimatorContract = (contractAddress) => __awaiter(this, void 0, void 0, function* () { if (this.estimatorContractCache.has(contractAddress)) { return this.estimatorContractCache.get(contractAddress); } const contract = yield this.tezosClient.contract.at(contractAddress); this.estimatorContractCache.set(contractAddress, contract); return contract; }); /** * @description Helper method to initialize a sapling worker with the sapling secret and state * @param contract The token contract address (optional) * @param tokenId The token id (optional) * @param providedSaplingId The sapling id if already known (optional, to avoid redundant API call) * @returns The initialized sapling worker and sapling id */ this.initializeSaplingWorkerWithState = (contract, tokenId, providedSaplingId) => __awaiter(this, void 0, void 0, function* () { try { yield this.ready; // eslint-disable-next-line prefer-destructuring let saplingWorker = this.saplingWorker; if (this.parallelThreads) { saplingWorker = yield spawn(new Worker(workerUrl), { timeout: 120000, }); } // Use provided saplingId if available, otherwise fetch it const saplingId = providedSaplingId !== null && providedSaplingId !== void 0 ? providedSaplingId : (yield this.getSaplingId(contract, tokenId)); if (!saplingId) { const tokenInfo = contract ? `contract ${contract}${tokenId !== undefined ? ` tokenId ${tokenId}` : ''}` : 'tez'; throw new Error(`Sapling state not initialized for ${tokenInfo}`); } // Determine the key type and value based on what's provided in the config let skType; let sk; if (this.config.saplingSecret) { skType = 'secretKey'; sk = this.config.saplingSecret; } else if (this.config.saplingViewingKey) { skType = 'viewingKey'; sk = this.config.saplingViewingKey; } else { skType = 'mnemonic'; sk = this.config.saplingMnemonic; } yield saplingWorker.loadSaplingSecret({ sk, skType, saplingDetails: { contractAddress: this.saplingStateMapContract, memoSize: 8, saplingId: `${saplingId}`, }, rpcUrl: this.tezosClient.rpc.getRpcUrl(), }); // Default token decimals let tokenDecimals = 6; if (contract) { tokenDecimals = yield this.getTokenDecimals(contract, tokenId); } return { saplingWorker, saplingId, tokenDecimals }; } catch (error) { const tokenInfo = contract ? `contract ${contract}${tokenId !== undefined ? ` tokenId ${tokenId}` : ''}` : 'tez'; throw new Error(`Failed to initialize sapling worker for ${tokenInfo}: ${error.message}`); } }); /** * @description Get the sapling id for the token contract and token id if provided * @param {string} [contract] The token contract address * @param {number} [tokenId] The token id * @returns The sapling id for the token contract and token id if provided */ this.getSaplingId = (contract, tokenId) => __awaiter(this, void 0, void 0, function* () { // Create cache key const cacheKey = contract ? `${contract}${tokenId !== undefined ? `:${tokenId}` : ''}` : 'tez'; // Check cache first and return the promise if it exists if (this.saplingIdCache.has(cacheKey)) { return this.saplingIdCache.get(cacheKey); } // Create and cache the promise to prevent duplicate concurrent requests const saplingIdPromise = (() => __awaiter(this, void 0, void 0, function* () { var _a; try { const contractStorage = yield fetch(`${defaults.baseUrl}/v1/contracts/${this.saplingStateMapContract}/storage`).then((res) => { if (!res.ok) { throw new Error(`Failed to fetch contract storage: ${res.status} ${res.statusText}`); } return res.json(); }); let saplingId; if (contract) { if (tokenId !== undefined) { saplingId = (_a = contractStorage.token_fa_2.find((token) => token.key.address === contract && token.key.nat === `${tokenId}`)) === null || _a === void 0 ? void 0 : _a.value; } else { saplingId = contractStorage.token_fa_1_2[contract]; } } else { saplingId = contractStorage.tez; } return saplingId; } catch (error) { // Remove from cache on error so it can be retried this.saplingIdCache.delete(cacheKey); throw new Error(`Failed to get sapling ID for ${cacheKey}: ${error.message}`); } }))(); // Cache the promise immediately before any await this.saplingIdCache.set(cacheKey, saplingIdPromise); return saplingIdPromise; }); /** * @description Get the metadata for the token contract and token id if provided * @param {string} contract The token contract address * @param {number} [tokenId] The token id * @returns The metadata for the token contract and token id if provided */ this.getTokenMetadata = (contract, tokenId) => __awaiter(this, void 0, void 0, function* () { // Create cache key const cacheKey = `${contract}${tokenId !== undefined ? `:${tokenId}` : ''}`; // Check cache first and return the promise if it exists if (this.tokenMetadataCache.has(cacheKey)) { return this.tokenMetadataCache.get(cacheKey); } // Create and cache the promise to prevent duplicate concurrent requests const metadataPromise = (() => __awaiter(this, void 0, void 0, function* () { try { const [metadata] = yield tokensGetTokens(Object.assign(Object.assign({ contract: { eq: contract, }, select: { fields: ['metadata'], } }, (tokenId !== undefined ? { tokenId: { eq: `${tokenId}` } } : {})), { limit: 1 })); return metadata; } catch (error) { // Remove from cache on error so it can be retried this.tokenMetadataCache.delete(cacheKey); throw new Error(`Failed to get token metadata for ${cacheKey}: ${error.message}`); } }))(); // Cache the promise immediately before any await this.tokenMetadataCache.set(cacheKey, metadataPromise); return metadataPromise; }); /** * @description Get the number of decimals for the token contract and token id if provided * @param {string} contract The token contract address * @param {number} [tokenId] The token id * @returns The number of decimals for the token contract and token id if provided */ this.getTokenDecimals = (contract, tokenId) => __awaiter(this, void 0, void 0, function* () { // Create cache key const cacheKey = `${contract}${tokenId !== undefined ? `:${tokenId}` : ''}`; // Check cache first and return the promise if it exists if (this.tokenDecimalsCache.has(cacheKey)) { return this.tokenDecimalsCache.get(cacheKey); } // Create and cache the promise to prevent duplicate concurrent requests const decimalsPromise = (() => __awaiter(this, void 0, void 0, function* () { try { const { decimals } = (yield this.getTokenMetadata(contract, tokenId)); return parseInt(decimals, 10); } catch (error) { // Remove from cache on error so it can be retried this.tokenDecimalsCache.delete(cacheKey); throw new Error(`Failed to get token decimals for ${cacheKey}: ${error.message}`); } }))(); // Cache the promise immediately before any await this.tokenDecimalsCache.set(cacheKey, decimalsPromise); return decimalsPromise; }); /** * @description Get the total shielded pool balances for the sapling state map contract * @returns The total shielded pool balances */ this.getTotalShieldedPoolBalances = () => __awaiter(this, void 0, void 0, function* () { const poolBalances = yield fetch(`${defaults.baseUrl}/v1/tokens/balances?account=${this.saplingStateMapContract}&sort.desc=balanceValue&limit=100&offset=0`).then((res) => res.json()); return poolBalances.map((token) => { var _a, _b; let unitAmount = token.balance; if (!this.useBaseUnits) { unitAmount = new BigNumber(unitAmount) .dividedBy(new BigNumber(10).exponentiatedBy(((_b = (_a = token.token) === null || _a === void 0 ? void 0 : _a.metadata) === null || _b === void 0 ? void 0 : _b.decimals) || 6)) .toNumber(); } return Object.assign(Object.assign({}, token), { balance: unitAmount }); }); }); /** * @description Get the sapling state map contract data * @returns The sapling state map contract data */ this.getContractData = () => __awaiter(this, void 0, void 0, function* () { const contract = yield fetch(`${defaults.baseUrl}/v1/accounts/${this.saplingStateMapContract}`).then((res) => res.json()); if (!this.useBaseUnits) { let unitAmount = contract.balance; unitAmount = new BigNumber(unitAmount) .dividedBy(new BigNumber(10).exponentiatedBy(6)) .toNumber(); return Object.assign(Object.assign({}, contract), { balance: unitAmount }); } return contract; }); /** * @description Estimate the gas and storage limits for the transaction list of shielding transactions * @param {OrderedTransactionList} transactionList The constructed transaction list * @returns The estimated gas and storage limits for the transaction list */ this.estimateShieldTransactionLimits = (transactionList) => __awaiter(this, void 0, void 0, function* () { const contractEstimator = yield this.getEstimatorContract(this.saplingStateMapContract); const batch = []; for (let index = 0; index < transactionList.length; index += 1) { /** * These transactions are not yet formatted for the contract call. The default * index transactions can be submitted in a single call with a list of transactions. * This is being done to optimize the number of operations in the transaction. */ if (index === OperationIndex.DEFAULT_INDEX) { const nonTezTransactions = []; // eslint-disable-next-line no-restricted-syntax for (const transaction of transactionList[index]) { const { amount } = transaction, rest = __rest(transaction, ["amount"]); // amount is only present for tez deposits if (!amount) { nonTezTransactions.push(rest); // eslint-disable-next-line no-continue continue; } batch.push([ contractEstimator.methodsObject.default([rest]), { amount, mutez: true, }, ]); } // If token deposits are present, batch them separately from tez deposits if (nonTezTransactions.length) { batch.push([ contractEstimator.methodsObject.default(nonTezTransactions), {}, ]); } } else { // These transactions are already formatted to be included in the batch call transactionList[index].forEach((transaction) => { batch.push([transaction, {}]); }); } } const estimateBatch = batch.map(([operation, params = {}]) => (Object.assign({ kind: OpKind.TRANSACTION }, operation.toTransferParams(params)))); return this.tezosClient.estimate.batch(estimateBatch); }); /** * @description Get the estimated fee for the transaction * @param {Estimate} estimate The estimate object * @returns The estimated fee for the transaction */ this.getEstimatedFee = (estimate) => { const operationFeeMutez = (estimate.gasLimit + this.gasLimitBuffer) * MINIMAL_FEE_PER_GAS_MUTEZ + Number(estimate.opSize) * MINIMAL_FEE_PER_BYTE_MUTEZ; return Math.ceil(Number(operationFeeMutez + MINIMAL_FEE_MUTEZ * 1.2)); }; /** * @description Submit sapling deposits/shielding transactions * @param {SaplingDeposits} saplingDeposits Sapling deposits/shielding transactions to be submitted * @param {number} saplingDeposits.amount The amount to be shielded * @param {string[]} saplingDeposits.saplingTransactions The sapling transactions to be submitted * @param {string} [saplingDeposits.contract] The token contract address * @param {number} [saplingDeposits.tokenId] The token id * @param {string} [saplingDeposits.owner] The shielded address to apply the shielded tokens * @returns The confirmation of the submitted sapling deposits/shielding transactions */ this.submitSaplingShieldTransaction = (saplingDeposits, callbacks) => __awaiter(this, void 0, void 0, function* () { var _a; const dappContract = yield this.getWalletContract(this.saplingStateMapContract); const transactionList = [[], [], [], []]; // eslint-disable-next-line no-restricted-syntax for (const saplingDeposit of saplingDeposits) { const { owner, amount, saplingTransactions, contract, tokenId } = saplingDeposit; if (contract) { // eslint-disable-next-line no-await-in-loop const tokenContract = yield this.getWalletContract(contract); if (tokenId !== undefined) { // FA2 update_operators add_operator transactionList[OperationIndex.UPDATE_OPERATORS_ADD_INDEX].push(tokenContract.methodsObject.update_operators([ { add_operator: { owner, operator: this.saplingStateMapContract, token_id: tokenId, }, }, ])); // Sapling State Contract default transactionList[OperationIndex.DEFAULT_INDEX].push({ txns: saplingTransactions, contract, token_id: tokenId, }); // FA2 update_operators remove_operator transactionList[OperationIndex.UPDATE_OPERATORS_REMOVE_INDEX].push(tokenContract.methodsObject.update_operators([ { remove_operator: { owner, operator: this.saplingStateMapContract, token_id: tokenId, }, }, ])); } else { // FA1.2 approve transactionList[OperationIndex.APPROVE_INDEX].push(tokenContract.methodsObject.approve({ value: amount, spender: this.saplingStateMapContract, })); // Sapling State Contract default transactionList[OperationIndex.DEFAULT_INDEX].push({ txns: saplingTransactions, contract, }); } } else { // Tez transaction transactionList[OperationIndex.DEFAULT_INDEX].push({ txns: saplingTransactions, amount, }); } } const estimates = yield this.estimateShieldTransactionLimits(transactionList); const batch = this.tezosClient.wallet.batch(); for (let index = 0; index < transactionList.length; index += 1) { /** * These transactions are not yet formatted for the contract call. The default * index transactions can be submitted in a single call with a list of transactions. * This is being done to optimize the number of operations in the transaction. */ if (index === OperationIndex.DEFAULT_INDEX) { const nonTezTransactions = []; // eslint-disable-next-line no-restricted-syntax for (const transaction of transactionList[index]) { const { amount } = transaction, rest = __rest(transaction, ["amount"]); // amount is only present for tez deposits if (!amount) { nonTezTransactions.push(rest); // eslint-disable-next-line no-continue continue; } const estimate = estimates.shift(); batch.withContractCall(dappContract.methodsObject.default([rest]), { // @ts-ignore string is an acceptible type for amount amount, mutez: true, gasLimit: estimate.gasLimit + this.gasLimitBuffer, storageLimit: estimate.storageLimit + this.storageLimitBuffer, fee: this.getEstimatedFee(estimate), }); } // If token deposits are present, batch them separately from tez deposits if (nonTezTransactions.length) { const estimate = estimates.shift(); batch.withContractCall(dappContract.methodsObject.default(nonTezTransactions), { gasLimit: estimate.gasLimit + this.gasLimitBuffer, storageLimit: estimate.storageLimit + this.storageLimitBuffer, fee: this.getEstimatedFee(estimate), }); } } else { // These transactions are already formatted to be included in the batch call transactionList[index].forEach((transaction) => { estimates.shift(); batch.withContractCall(transaction); }); } } (_a = callbacks === null || callbacks === void 0 ? void 0 : callbacks.onSigning) === null || _a === void 0 ? void 0 : _a.call(callbacks); return batch.send().then((op) => __awaiter(this, void 0, void 0, function* () { var _a, _b; (_a = callbacks === null || callbacks === void 0 ? void 0 : callbacks.onSubmitting) === null || _a === void 0 ? void 0 : _a.call(callbacks, { opHash: op.opHash }); const confirmation = yield op.confirmation(this.minConfirmations); (_b = callbacks === null || callbacks === void 0 ? void 0 : callbacks.onConfirmed) === null || _b === void 0 ? void 0 : _b.call(callbacks, { opHash: op.opHash, block: confirmation }); return Object.assign(Object.assign({}, confirmation), { opHash: op.opHash }); })); }); /** * @description Submit sapling transactions (shared implementation for unshield and transfer) * @param {SaplingTransactions} saplingTransactions Sapling transactions to be submitted * @param {string[]} saplingTransactions.saplingTransactions The sapling transactions to be submitted * @param {string} [saplingTransactions.contract] The token contract address * @param {number} [saplingTransactions.tokenId] The token id * @returns The confirmation of the submitted sapling transactions */ this.submitSaplingTransaction = (saplingTransactions, callbacks) => __awaiter(this, void 0, void 0, function* () { var _a; const dappContract = yield this.getWalletContract(this.saplingStateMapContract); const dappContractEstimator = yield this.getEstimatorContract(this.saplingStateMapContract); const saplingMethodObject = saplingTransactions.map((saplingTransaction) => ({ txns: saplingTransaction.saplingTransactions, contract: saplingTransaction.contract, token_id: saplingTransaction.tokenId, })); const operation = dappContractEstimator.methodsObject.default(saplingMethodObject); const estimate = yield this.tezosClient.estimate.contractCall(operation); (_a = callbacks === null || callbacks === void 0 ? void 0 : callbacks.onSigning) === null || _a === void 0 ? void 0 : _a.call(callbacks); return dappContract.methodsObject .default(saplingMethodObject) .send({ gasLimit: estimate.gasLimit + this.gasLimitBuffer, storageLimit: estimate.storageLimit + this.storageLimitBuffer, fee: this.getEstimatedFee(estimate), }) .then((op) => __awaiter(this, void 0, void 0, function* () { var _a, _b; (_a = callbacks === null || callbacks === void 0 ? void 0 : callbacks.onSubmitting) === null || _a === void 0 ? void 0 : _a.call(callbacks, { opHash: op.opHash }); const confirmation = yield op.confirmation(this.minConfirmations); (_b = callbacks === null || callbacks === void 0 ? void 0 : callbacks.onConfirmed) === null || _b === void 0 ? void 0 : _b.call(callbacks, { opHash: op.opHash, block: confirmation }); return Object.assign(Object.assign({}, confirmation), { opHash: op.opHash }); })); }); /** * @description Submit sapling withdrawals/unshielding transactions * @param {SaplingTransactions} saplingWithdrawals Sapling withdrawals/unshielding transactions to be submitted * @param {string[]} saplingWithdrawals.saplingTransactions The sapling transactions to be submitted * @param {string} [saplingWithdrawals.contract] The token contract address * @param {number} [saplingWithdrawals.tokenId] The token id * @returns The confirmation of the submitted sapling withdrawals/unshielding transactions */ this.submitSaplingUnshieldTransaction = (saplingWithdrawals, callbacks) => __awaiter(this, void 0, void 0, function* () { return this.submitSaplingTransaction(saplingWithdrawals, callbacks); }); /** * @description Submit sapling transfers transactions * @param {SaplingTransactions} saplingTransfers Sapling transfers to be submitted * @param {string[]} saplingTransfers.saplingTransactions The sapling transactions to be submitted * @param {string} [saplingTransfers.contract] The token contract address * @param {number} [saplingTransfers.tokenId] The token id * @returns The confirmation of the submitted sapling transfers */ this.submitSaplingTransferTransaction = (saplingTransfers, callbacks) => __awaiter(this, void 0, void 0, function* () { return this.submitSaplingTransaction(saplingTransfers, callbacks); }); /** * @description Construct the sapling parameters for the shielded transaction * @param shieldParam The sapling shielding parameters * @param {number} shieldParam.amount The amount to be shielded * @param {string} [shieldParam.shieldedAddress] The shielded address to apply the shielded tokens * @param {string} [shieldParam.contract] The token contract address * @param {number} [shieldParam.tokenId] The token id * @param {string} [shieldParam.memo] The memo to be included in the sapling transaction * @returns The sapling parameters for the shielded transaction */ this.constructShieldTokenParams = (shieldParam) => __awaiter(this, void 0, void 0, function* () { const { amount, shieldedAddress, contract, tokenId, memo } = shieldParam; // Validate inputs ShieldBridgeSDK.validateAmount(amount, 'Shield amount'); if (contract) { ShieldBridgeSDK.validateAddress(contract, 'Contract address'); } if (shieldedAddress) { // Shielded addresses have different format - basic validation if (!shieldedAddress || typeof shieldedAddress !== 'string') { throw new Error('Shielded address must be a valid string'); } } const { saplingWorker, tokenDecimals } = yield this.initializeSaplingWorkerWithState(contract, tokenId); let unitAmount = amount; if (!this.useBaseUnits) { unitAmount = new BigNumber(10) .exponentiatedBy(tokenDecimals) .times(amount) .toString(); } let to = shieldedAddress; // If no shielded address is provided, default to the loaded sapling payment address if (!to) { const saplingPaymentAddress = yield saplingWorker.getPaymentAddress(); to = saplingPaymentAddress.address; } const saplingTxn = yield saplingWorker.prepareShieldedTransaction([ { to, // @ts-ignore string is an acceptible type for amount amount: unitAmount, memo, mutez: true, }, ]); if (this.parallelThreads) { yield Thread.terminate(saplingWorker); } const owner = yield this.tezosClient.wallet.pkh(); return { saplingTransactions: [saplingTxn], owner, amount: unitAmount, contract, tokenId, }; }); /** * @description Shield the specified amount of unshielded tokens to the sapling address * @param {ShieldParams} shieldParams Sapling shielding parameters to be constructed into sapling transactions * @param {number} shieldParams.amount The amount to be shielded * @param {string} [shieldParams.shieldedAddress] The shielded address to apply the shielded tokens * @param {string} [shieldParams.contract] The token contract address * @param {number} [shieldParams.tokenId] The token id * @param {string} [shieldParams.memo] The memo to be included in the sapling transaction * @param {TransactionProgressCallbacks} [callbacks] Optional callbacks for operation progress updates * @returns The confirmation of the submitted sapling shielding transactions * @throws {Error} If called in view-only mode (with a viewing key) */ this.shield = (shieldParams, callbacks) => __awaiter(this, void 0, void 0, function* () { var _a; if (this.isViewOnlyMode) { throw new Error('Cannot shield tokens in view-only mode. A spending key is required for transaction operations. ' + 'Initialize the SDK with saplingSecret or saplingMnemonic instead of saplingViewingKey.'); } let contractParams = []; (_a = callbacks === null || callbacks === void 0 ? void 0 : callbacks.onGenerating) === null || _a === void 0 ? void 0 : _a.call(callbacks, shieldParams); if (this.parallelThreads) { const shieldParamPromises = shieldParams.map((shieldParam) => this.constructShieldTokenParams(shieldParam)); contractParams = yield Promise.all(shieldParamPromises); } else { for (let i = 0; i < shieldParams.length; i += 1) { const shieldParam = shieldParams[i]; const contractParam = // eslint-disable-next-line no-await-in-loop yield this.constructShieldTokenParams(shieldParam); contractParams.push(contractParam); } } return this.submitSaplingShieldTransaction(contractParams, callbacks); }); /** * @description Construct the sapling parameters for the unshielded transaction * @param unshieldParam The sapling unshielding parameters * @param {number} unshieldParam.amount The amount to be unshielded * @param {string} [unshieldParam.unshieldedAddress] The unshielded address to apply the unshielded tokens * @param {string} [unshieldParam.contract] The token contract address * @param {number} [unshieldParam.tokenId] The token id * @returns The sapling parameters for the unshielded transaction */ this.constructUnshieldTokenParams = (unshieldParam) => __awaiter(this, void 0, void 0, function* () { const { amount, unshieldedAddress, contract, tokenId } = unshieldParam; // Validate inputs ShieldBridgeSDK.validateAmount(amount, 'Unshield amount'); if (contract) { ShieldBridgeSDK.validateAddress(contract, 'Contract address'); } if (unshieldedAddress) { ShieldBridgeSDK.validateAddress(unshieldedAddress, 'Unshielded address'); } const { saplingWorker, tokenDecimals } = yield this.initializeSaplingWorkerWithState(contract, tokenId); let unitAmount = amount; if (!this.useBaseUnits) { unitAmount = new BigNumber(10) .exponentiatedBy(tokenDecimals) .times(amount) .toString(); } let to = unshieldedAddress; // If no unshielded address is provided, default to the wallet public key hash if (!to) { to = yield this.tezosClient.wallet.pkh(); } const saplingTxn = yield saplingWorker.prepareUnshieldedTransaction({ to, // @ts-ignore string is an acceptible type for amount amount: unitAmount, mutez: true, }); if (this.parallelThreads) { yield Thread.terminate(saplingWorker); } return { saplingTransactions: [saplingTxn], contract, tokenId, }; }); /** * @description Unshield the specified amount of shielded tokens from the sapling address * @param {UnshieldParams} unshieldParams Sapling unshielding parameters to be constructed into sapling transactions * @param {number} unshieldParams.amount The amount to be unshielded * @param {string} [unshieldParams.unshieldedAddress] The unshielded address to apply the unshielded tokens * @param {string} [unshieldParams.contract] The token contract address * @param {number} [unshieldParams.tokenId] The token id * @param {TransactionProgressCallbacks} [callbacks] Optional callbacks for operation progress updates * @returns The confirmation of the submitted sapling unshielding transactions * @throws {Error} If called in view-only mode (with a viewing key) */ this.unshield = (unshieldParams, callbacks) => __awaiter(this, void 0, void 0, function* () { var _a; if (this.isViewOnlyMode) { throw new Error('Cannot unshield tokens in view-only mode. A spending key is required for transaction operations. ' + 'Initialize the SDK with saplingSecret or saplingMnemonic instead of saplingViewingKey.'); } let contractParams = []; (_a = callbacks === null || callbacks === void 0 ? void 0 : callbacks.onGenerating) === null || _a === void 0 ? void 0 : _a.call(callbacks, unshieldParams); if (this.parallelThreads) { const unshieldParamPromises = unshieldParams.map((unshieldParam) => this.constructUnshieldTokenParams(unshieldParam)); contractParams = yield Promise.all(unshieldParamPromises); } else { for (let i = 0; i < unshieldParams.length; i += 1) { const unshieldParam = unshieldParams[i]; const contractParam = // eslint-disable-next-line no-await-in-loop yield this.constructUnshieldTokenParams(unshieldParam); contractParams.push(contractParam); } } return this.submitSaplingUnshieldTransaction(contractParams, callbacks); }); /** * @description Construct the sapling parameters for the transfer transaction * @param transferParam The sapling transfer parameters * @param {string} [transferParam.contract] The token contract address * @param {number} [transferParam.tokenId] The token id * @param {object} transferParam.transfers The transfers to be made * @returns The sapling parameters for the transfer transaction */ this.constructTransferTokenParams = (transferParam) => __awaiter(this, void 0, void 0, function* () { const { contract, tokenId, transfers } = transferParam; // Validate inputs if (!transfers || !Array.isArray(transfers) || transfers.length === 0) { throw new Error('Transfers array must not be empty'); } if (contract) { ShieldBridgeSDK.validateAddress(contract, 'Contract address'); } transfers.forEach((transfer, index) => { ShieldBridgeSDK.validateAmount(transfer.amount, `Transfer[${index}] amount`); if (!transfer.to || typeof transfer.to !== 'string') { throw new Error(`Transfer[${index}] recipient address must be a valid string`); } }); const { saplingWorker, tokenDecimals } = yield this.initializeSaplingWorkerWithState(contract, tokenId); const saplingTransfers = transfers.map(({ amount, to, memo }) => { let unitAmount = amount; if (!this.useBaseUnits) { unitAmount = new BigNumber(10) .exponentiatedBy(tokenDecimals) .times(amount) .toString(); } return { to, amount: unitAmount, memo, mutez: true, }; }); const saplingTxn = // @ts-ignore string is an acceptible type for amount yield saplingWorker.prepareSaplingTransaction(saplingTransfers); if (this.parallelThreads) { yield Thread.terminate(saplingWorker); } return { saplingTransactions: [saplingTxn], contract, tokenId, }; }); /** * @description Transfer the specified amount of shielded tokens to the specified shielded address * @param {TransferParams[]} transferParams Sapling transfer parameters to be constructed into sapling transactions * @param {string} [transferParams.contract] The token contract address * @param {number} [transferParams.tokenId] The token id * @param {object} transferParams.transfers The transfers to be made * @param {TransactionProgressCallbacks} [callbacks] Optional callbacks for operation progress updates * @returns The confirmation of the submitted sapling transfer transactions * @throws {Error} If called in view-only mode (with a viewing key) */ this.transfer = (transferParams, callbacks) => __awaiter(this, void 0, void 0, function* () { var _a; if (this.isViewOnlyMode) { throw new Error('Cannot transfer tokens in view-only mode. A spending key is required for transaction operations. ' + 'Initialize the SDK with saplingSecret or saplingMnemonic instead of saplingViewingKey.'); } let contractParams = []; (_a = callbacks === null || callbacks === void 0 ? void 0 : callbacks.onGenerating) === null || _a === void 0 ? void 0 : _a.call(callbacks, transferParams); if (this.parallelThreads) { const unshieldParamPromises = transferParams.map((transferParam) => this.constructTransferTokenParams(transferParam)); contractParams = yield Promise.all(unshieldParamPromises); } else { for (let i = 0; i < transferParams.length; i += 1) { const transferParam = transferParams[i]; const contractParam = // eslint-disable-next-line no-await-in-loop yield this.constructTransferTokenParams(transferParam); contractParams.push(contractParam); } } return this.submitSaplingTransferTransaction(contractParams, callbacks); }); /** * @description Get the shielded sapling token balance for the currently loaded shielded address * @param {SaplingTokenInfo} saplingTokenInfo The sapling token information * @param {number} [saplingTokenInfo.saplingId] The sapling id * @param {string} [saplingTokenInfo.contract] The token contract address * @param {number} [saplingTokenInfo.tokenId] The token id * @returns The shielded sapling token balance for the currently loaded shielded address */ this.getShieldedBalance = (_a) => __awaiter(this, [_a], void 0, function* ({ saplingId, contract, tokenId, }) { // Pass the known saplingId to avoid redundant API call const { saplingWorker, tokenDecimals } = yield this.initializeSaplingWorkerWithState(contract, tokenId, saplingId); const balance = (yield saplingWorker.getSaplingBalance()); if (this.parallelThreads) { yield Thread.terminate(saplingWorker); } if (this.useBaseUnits) { return balance; } return new BigNumber(balance) .dividedBy(new BigNumber(10).exponentiatedBy(tokenDecimals)) .toNumber(); }); /** * @description Get all the shielded sapling tokens * @param includeMetadata Include the metadata for the shielded sapling tokens * @returns The shielded sapling tokens */ this.getAllShieldedAssets = (...args_1) => __awaiter(this, [...args_1], void 0, function* (includeMetadata = false) { const contractStorage = yield fetch(`${defaults.baseUrl}/v1/contracts/${this.saplingStateMapContract}/storage`).then((res) => res.json()); const saplingIds = [ { saplingId: contractStorage.tez, }, ]; // Cache tez sapling ID this.saplingIdCache.set('tez', Promise.resolve(contractStorage.tez)); contractStorage.token_fa_2.forEach(({ key, value }) => { const tokenId = parseInt(key.nat, 10); saplingIds.push({ saplingId: value, contract: key.address, tokenId, }); // Cache FA2 token sapling ID const cacheKey = `${key.address}:${tokenId}`;