shield-bridge-sdk
Version:
918 lines (917 loc) • 58.8 kB
JavaScript
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}`;