shield-bridge-sdk
Version:
889 lines (888 loc) • 45.7 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 { spawn, Thread, Worker } from 'threads';
import { OpKind, } from '@taquito/taquito';
import { defaults, tokensGetTokens } from '@tzkt/sdk-api';
import BigNumber from 'bignumber.js';
export const tzktApiMap = {
mainnet: 'https://api.tzkt.io',
ghostnet: 'https://api.ghostnet.tzkt.io',
};
export const saplingStateMapContract = {
mainnet: 'KT1WorWEWjfQqQ1X2BFQiCc4hE3DuDKQVH4U',
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.
* @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='KT1WorWEWjfQqQ1X2BFQiCc4hE3DuDKQVH4U'] 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 {number} [config.parallelThreads=false] Whether to spawn parallel threads for the sapling worker
* @param {string} [config.saplingSecret] The sapling secret key
* @param {string} [config.saplingMnemonic] The sapling mnemonic
* @returns {ShieldBridgeSDK} The Shield Bridge SDK instance
* @example
* 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'
* }
* ]);
*/
export class ShieldBridgeSDK {
constructor(config) {
var _a, _b, _c, _d, _e, _f;
this.config = config;
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 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* () {
var _a;
const contractStorage = yield fetch(`${defaults.baseUrl}/v1/contracts/${this.saplingStateMapContract}/storage`).then((res) => res.json());
if (contract) {
if (tokenId !== undefined) {
return (_a = contractStorage.token_fa_2.find((token) => token.key.address === contract && token.key.nat === `${tokenId}`)) === null || _a === void 0 ? void 0 : _a.value;
}
return contractStorage.token_fa_1_2[contract];
}
return contractStorage.tez;
});
/**
* @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
*/
// eslint-disable-next-line class-methods-use-this
this.getTokenMetadata = (contract, tokenId) => __awaiter(this, void 0, void 0, function* () {
const [metadata] = yield tokensGetTokens(Object.assign({ contract: {
eq: contract,
}, select: {
fields: ['metadata'],
} }, (tokenId ? { tokenId: { eq: `${tokenId}` } } : {})));
return metadata;
});
/**
* @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* () {
const { decimals } = (yield this.getTokenMetadata(contract, tokenId));
return parseInt(decimals, 10);
});
/**
* @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.tezosClient.contract.at(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) => __awaiter(this, void 0, void 0, function* () {
const dappContract = yield this.tezosClient.wallet.at(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.tezosClient.wallet.at(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);
});
}
}
return batch.send().then((op) => op.confirmation(this.minConfirmations));
});
/**
* @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) => __awaiter(this, void 0, void 0, function* () {
const dappContract = yield this.tezosClient.wallet.at(this.saplingStateMapContract);
const dappContractEstimator = yield this.tezosClient.contract.at(this.saplingStateMapContract);
const saplingWithdrawalMethodObject = saplingWithdrawals.map((saplingWithdrawal) => ({
txns: saplingWithdrawal.saplingTransactions,
contract: saplingWithdrawal.contract,
token_id: saplingWithdrawal.tokenId,
}));
const operation = dappContractEstimator.methodsObject.default(saplingWithdrawalMethodObject);
const estimate = yield this.tezosClient.estimate.contractCall(operation);
return dappContract.methodsObject
.default(saplingWithdrawalMethodObject)
.send({
gasLimit: estimate.gasLimit + this.gasLimitBuffer,
storageLimit: estimate.storageLimit + this.storageLimitBuffer,
fee: this.getEstimatedFee(estimate),
})
.then((op) => op.confirmation(this.minConfirmations));
});
/**
* @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) => __awaiter(this, void 0, void 0, function* () {
const dappContract = yield this.tezosClient.wallet.at(this.saplingStateMapContract);
const dappContractEstimator = yield this.tezosClient.contract.at(this.saplingStateMapContract);
const saplingTransferMethodObject = saplingTransfers.map((saplingTransfer) => ({
txns: saplingTransfer.saplingTransactions,
contract: saplingTransfer.contract,
token_id: saplingTransfer.tokenId,
}));
const operation = dappContractEstimator.methodsObject.default(saplingTransferMethodObject);
const estimate = yield this.tezosClient.estimate.contractCall(operation);
return dappContract.methodsObject
.default(saplingTransferMethodObject)
.send({
gasLimit: estimate.gasLimit + this.gasLimitBuffer,
storageLimit: estimate.storageLimit + this.storageLimitBuffer,
fee: this.getEstimatedFee(estimate),
})
.then((op) => op.confirmation(this.minConfirmations));
});
/**
* @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;
yield this.ready;
// eslint-disable-next-line prefer-destructuring
let saplingWorker = this.saplingWorker;
if (this.parallelThreads) {
saplingWorker = yield spawn(new Worker(workerUrl), {
timeout: 120000,
});
}
const saplingId = yield this.getSaplingId(contract, tokenId);
if (!saplingId) {
throw new Error('Sapling state not initialized for the token');
}
const skType = this.config.saplingSecret ? 'secretKey' : 'mnemonic';
yield saplingWorker.loadSaplingSecret({
sk: skType === 'secretKey'
? this.config.saplingSecret
: this.config.saplingMnemonic,
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);
}
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
* @returns The confirmation of the submitted sapling shielding transactions
*/
this.shield = (shieldParams) => __awaiter(this, void 0, void 0, function* () {
let contractParams = [];
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);
});
/**
* @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;
yield this.ready;
// eslint-disable-next-line prefer-destructuring
let saplingWorker = this.saplingWorker;
if (this.parallelThreads) {
saplingWorker = yield spawn(new Worker(workerUrl), {
timeout: 120000,
});
}
const saplingId = yield this.getSaplingId(contract, tokenId);
if (!saplingId) {
throw new Error('Sapling state not initialized for the token');
}
const skType = this.config.saplingSecret ? 'secretKey' : 'mnemonic';
yield saplingWorker.loadSaplingSecret({
sk: skType === 'secretKey'
? this.config.saplingSecret
: this.config.saplingMnemonic,
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);
}
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
* @returns The confirmation of the submitted sapling unshielding transactions
*/
this.unshield = (unshieldParams) => __awaiter(this, void 0, void 0, function* () {
let contractParams = [];
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);
});
/**
* @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;
yield this.ready;
// eslint-disable-next-line prefer-destructuring
let saplingWorker = this.saplingWorker;
if (this.parallelThreads) {
saplingWorker = yield spawn(new Worker(workerUrl), {
timeout: 120000,
});
}
const saplingId = yield this.getSaplingId(contract, tokenId);
if (!saplingId) {
throw new Error('Sapling state not initialized for the token');
}
const skType = this.config.saplingSecret ? 'secretKey' : 'mnemonic';
yield saplingWorker.loadSaplingSecret({
sk: skType === 'secretKey'
? this.config.saplingSecret
: this.config.saplingMnemonic,
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);
}
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
* @returns The confirmation of the submitted sapling transfer transactions
*/
this.transfer = (transferParams) => __awaiter(this, void 0, void 0, function* () {
let contractParams = [];
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);
});
/**
* @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, }) {
let saplingIdQuery = saplingId;
if (!saplingIdQuery) {
saplingIdQuery = yield this.getSaplingId(contract, tokenId);
}
const skType = this.config.saplingSecret ? 'secretKey' : 'mnemonic';
yield this.ready;
// eslint-disable-next-line prefer-destructuring
let saplingWorker = this.saplingWorker;
if (this.parallelThreads) {
saplingWorker = yield spawn(new Worker(workerUrl), {
timeout: 120000,
});
}
yield saplingWorker.loadSaplingSecret({
sk: skType === 'secretKey'
? this.config.saplingSecret
: this.config.saplingMnemonic,
skType,
saplingDetails: {
contractAddress: this.saplingStateMapContract,
memoSize: 8,
saplingId: `${saplingIdQuery}`,
},
rpcUrl: this.tezosClient.rpc.getRpcUrl(),
});
const balance = (yield saplingWorker.getSaplingBalance());
let tokenDecimals = 6;
if (contract) {
tokenDecimals = yield this.getTokenDecimals(contract, tokenId);
}
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(`${tzktApiMap.ghostnet}/v1/contracts/${this.saplingStateMapContract}/storage`).then((res) => res.json());
const saplingIds = [
{
saplingId: contractStorage.tez,
},
];
contractStorage.token_fa_2.forEach(({ key, value }) => {
saplingIds.push({
saplingId: value,
contract: key.address,
tokenId: parseInt(key.nat, 10),
});
});
Object.entries(contractStorage.token_fa_1_2).forEach(([contract, saplingId]) => {
saplingIds.push({ saplingId, contract });
});
if (!includeMetadata) {
return saplingIds;
}
const withMetadata = saplingIds.map((saplingId) => {
if (saplingId.contract) {
return this.getTokenMetadata(saplingId.contract, saplingId.tokenId).then((tokenMetadata) => (Object.assign(Object.assign({}, saplingId), { metadata: tokenMetadata })));
}
return saplingId;
});
return Promise.all(withMetadata);
});
/**
* @description Get the shielded sapling token balances for all the sapling tokens
* @returns The shielded sapling token balances for all the sapling tokens
*/
this.getAllShieldedBalances = () => __awaiter(this, void 0, void 0, function* () {
const saplingIds = yield this.getAllShieldedAssets();
const balances = [];
for (let i = 0; i < saplingIds.length; i += 1) {
const saplingToken = saplingIds[i];
// eslint-disable-next-line no-await-in-loop
const balance = yield this.getShieldedBalance(saplingToken);
balances.push(Object.assign(Object.assign({}, saplingToken), { balance }));
}
return balances;
});
/**
* @description Get the shielded incoming and outgoing transactions for the specified sapling contract and token id
* @param {string} [contract] Sapling contract address
* @param {number} [tokenId] Token id
* @returns The shielded incoming and outgoing transactions for the specified sapling contract and token id
*/
this.getShieldedTransactions = (contract, tokenId) => __awaiter(this, void 0, void 0, function* () {
const saplingId = yield this.getSaplingId(contract, tokenId);
if (!saplingId) {
throw new Error('Sapling state not initialized for the token');
}
let tokenDecimals = 6;
if (contract) {
tokenDecimals = yield this.getTokenDecimals(contract, tokenId);
}
const skType = this.config.saplingSecret ? 'secretKey' : 'mnemonic';
yield this.ready;
// eslint-disable-next-line prefer-destructuring
let saplingWorker = this.saplingWorker;
if (this.parallelThreads) {
saplingWorker = yield spawn(new Worker(workerUrl), {
timeout: 120000,
});
}
yield saplingWorker.loadSaplingSecret({
sk: skType === 'secretKey'
? this.config.saplingSecret
: this.config.saplingMnemonic,
skType,
saplingDetails: {
contractAddress: this.saplingStateMapContract,
memoSize: 8,
saplingId: `${saplingId}`,
},
rpcUrl: this.tezosClient.rpc.getRpcUrl(),
});
const transactions = yield saplingWorker.getSaplingTransactions();
if (this.parallelThreads) {
yield Thread.terminate(saplingWorker);
}
return {
incoming: transactions.incoming.map((transaction) => {
if (this.useBaseUnits) {
return transaction;
}
const value = new BigNumber(transaction.value)
.dividedBy(new BigNumber(10).exponentiatedBy(tokenDecimals))
.toNumber();
return Object.assign(Object.assign({}, transaction), { value });
}),
outgoing: transactions.outgoing.map((transaction) => {
if (this.useBaseUnits) {
return transaction;
}
const value = new BigNumber(transaction.value)
.dividedBy(new BigNumber(10).exponentiatedBy(tokenDecimals))
.toNumber();
return Object.assign(Object.assign({}, transaction), { value });
}),
};
});
/**
* @description Get the sapling payment address of the currently loaded sapling key
* @returns The sapling payment address
*/
this.getShieldedAddress = () => __awaiter(this, void 0, void 0, function* () {
const saplingId = yield this.getSaplingId();
if (!saplingId) {
throw new Error('Sapling state not initialized for the token');
}
const skType = this.config.saplingSecret ? 'secretKey' : 'mnemonic';
yield this.ready;
// eslint-disable-next-line prefer-destructuring
let saplingWorker = this.saplingWorker;
if (this.parallelThreads) {
saplingWorker = yield spawn(new Worker(workerUrl), {
timeout: 120000,
});
}
yield saplingWorker.loadSaplingSecret({
sk: skType === 'secretKey'
? this.config.saplingSecret
: this.config.saplingMnemonic,
skType,
saplingDetails: {
contractAddress: this.saplingStateMapContract,
memoSize: 8,
saplingId: `${saplingId}`,
},
rpcUrl: this.tezosClient.rpc.getRpcUrl(),
});
const saplingPaymentAddress = yield saplingWorker.getPaymentAddress();
if (this.parallelThreads) {
yield Thread.terminate(saplingWorker);
}
return saplingPaymentAddress.address;
});
/**
* @description Initialize the sapling pool for the specified token contract and token id
* @param {string} contract The token contract address
* @param {number} [tokenId] The token id
* @returns The confirmation of the initialized sapling pool
*/
this.initTokenSaplingPool = (contract, tokenId) => __awaiter(this, void 0, void 0, function* () {
const dappContract = yield this.tezosClient.wallet.at(this.saplingStateMapContract);
return dappContract.methodsObject
.init_token_sapling_pool({
contract,
token_id: tokenId,
})
.send()
.then((op) => op.confirmation(this.minConfirmations));
});
this.tezosClient = config.client;
this.minConfirmations = (_a = config.minConfirmations) !== null && _a !== void 0 ? _a : 1;
this.saplingStateMapContract =
(_b = config.saplingStateMapContract) !== null && _b !== void 0 ? _b : saplingStateMapContract.mainnet;
this.gasLimitBuffer = (_c = config.gasLimitBuffer) !== null && _c !== void 0 ? _c : 2000;
this.storageLimitBuffer = (_d = config.storageLimitBuffer) !== null && _d !== void 0 ? _d : 500;
this.useBaseUnits = (_e = config.useBaseUnits) !== null && _e !== void 0 ? _e : false;
this.parallelThreads = (_f = config.parallelThreads) !== null && _f !== void 0 ? _f : false;
// This prevents multiple instances with a separate baseUrl since the SDK is a singleton
defaults.baseUrl = tzktApiMap[this.config.tzktApi || 'mainnet'];
this.ready = this.initializeSaplingWorker();
}
}