import BN from "bn.js";
import { IMyriaClient, MyriaClient, WithdrawalContract } from "..";
import {
AssetAPI,
CommonAPI,
WithdrawalAPI,
WithdrawalMarketpAPI,
} from "../core/apis";
import { ContractFactory } from "../core/ContractFactory";
import { stripHexPrefix } from "../core/helpers";
import { AssetDictParams, CreateVaultERC721ByEthKeyParams, GenericERC20Network, TransactionData, TxResult } from "../types";
import { SendOptions, TokenType } from "../types/CommonTypes";
import {
WithdrawAndMintParams,
WithdrawERC721Params,
WithdrawNftCompleteParams,
WithdrawNftCompleteResponse,
WithdrawNftOffChainParams,
WithdrawNftOffChainResponse,
WithdrawOffchainParams,
WithdrawOffchainParamsV2,
WithdrawOnchainParams,
WithdrawOffchainPayloadV2,
WithdrawNftOffChainRequestAPI,
FullWithdrawalInput,
FullWithdrawalPayload,
WithdrawNftCompleteRequestAPI,
WithdrawOffchainParamsV2RequestAPI,
WithdrawOffchainParamsV2Hash,
UserWithdrawalHashType,
} from "../types/WithdrawType";
import { DEFAULT_QUANTUM } from "../utils/Constants";
import { DELAY_IN_RETRY, retryPromise, RETRY_DEFAULT } from "../utils/RetryUtils";
import { APIResponseType } from "./../types/APIResponseType";
import { CommonModule } from "./CommonModule";
/**
* Create WithdrawalModule instance object
* @class WithdrawalModule
* @param {IMyriaClient} IMyriaClient Interface of Myria Client
*/
export class WithdrawalModule {
private withdrawalContract: WithdrawalContract;
public client: MyriaClient;
private withdrawalAPI: WithdrawalAPI;
private withdrawalMarketpAPI: WithdrawalMarketpAPI;
private commonAPI: CommonAPI;
private assetAPI: AssetAPI;
private commonModule: CommonModule;
constructor(client: IMyriaClient) {
this.client = new MyriaClient(client);
this.withdrawalContract = this.getWithdrawalContract(this.client);
this.withdrawalAPI = new WithdrawalAPI(this.client.env);
this.withdrawalMarketpAPI = new WithdrawalMarketpAPI(this.client.env);
this.commonAPI = new CommonAPI(this.client.env);
this.assetAPI = new AssetAPI(this.client.env);
this.commonModule = CommonModule.getInstance(client);
}
public getWithdrawalContract(client: MyriaClient): WithdrawalContract {
const contractFactory = new ContractFactory(client);
return contractFactory.getWithdrawContract();
}
public getCustomWithdrawContract(customERC20Network: GenericERC20Network): WithdrawalContract {
const contractFactory = new ContractFactory(this.client);
return contractFactory.getCustomWithdrawContract(customERC20Network);
}
public async withdrawalOffchain(
withdrawalParams: WithdrawOffchainParams,
): Promise<any> {
if (!withdrawalParams.ethAddress) {
throw new Error("Eth address is required");
}
if (!withdrawalParams.amount) {
throw new Error("Amount is required.");
}
if (!withdrawalParams.starkKey) {
throw new Error("StarkKey is required.");
}
if (!withdrawalParams.quantum) {
withdrawalParams.quantum = DEFAULT_QUANTUM; // default quantum is 10^10
}
// Get vault ID
let vaultData;
const quantizedAmount = (new BN(withdrawalParams.amount)).div(new BN(withdrawalParams.quantum)).toString();
let nonce = withdrawalParams.nonce;
if (!nonce) {
nonce = await this.client.web3.eth.getTransactionCount(withdrawalParams.ethAddress);
}
console.log("nonce is ", nonce);
if (!withdrawalParams?.vaultId) {
try {
const vaultResponse = await this.commonAPI.createVault({
starkKey: withdrawalParams.starkKey,
tokenType: withdrawalParams?.tokenType,
tokenAddress: withdrawalParams?.tokenAddress || undefined,
quantum: withdrawalParams.quantum, // TODO might update the quantum later to respect with the input
});
if (vaultResponse?.status === "success") {
vaultData = String(vaultResponse.data.vaultId);
} else {
throw new Error(
"Fetching vaultID failure - check BE server or validation request for calling"
);
}
} catch (ex: any) {
throw new Error(ex);
}
} else {
vaultData = String(withdrawalParams.vaultId);
}
const assetDict: AssetDictParams = {
type: withdrawalParams.tokenType,
data: {
quantum: withdrawalParams.quantum,
tokenAddress: withdrawalParams.tokenAddress
}
}
const assetId = this.commonModule.generateAssetId(assetDict);
const withdrawalHashPayload: UserWithdrawalHashType = {
vaultId: vaultData,
assetId: stripHexPrefix(assetId),
quantizedAmount: quantizedAmount,
nonce: nonce,
ethAddress: withdrawalParams.ethAddress
};
console.log(
"[Core-SDK] withdrawal hash payload ->",
withdrawalHashPayload
);
const signature = await this.commonModule.generateSignatureForWithdrawal(
withdrawalHashPayload,
);
if (!signature) {
throw new Error("Stark signature generation error ");
}
// Make withdraw offchain request
let withdrawOffchainResult;
try {
if (
withdrawalParams?.tokenType === TokenType.MINTABLE_ERC721 ||
withdrawalParams?.tokenType === TokenType.ERC721
) {
withdrawOffchainResult = await this.withdrawalAPI.makeWithdrawalTransaction(
vaultData,
withdrawalParams?.starkKey?.toLowerCase(),
withdrawalParams?.amount,
assetId,
signature,
nonce
);
} else {
withdrawOffchainResult = await this.withdrawalAPI.makeWithdrawalTransaction(
vaultData,
withdrawalParams?.starkKey?.toLowerCase(),
quantizedAmount,
assetId,
signature,
nonce
);
}
console.log(
`Withdrawal offchain response -> ${JSON.stringify(
withdrawOffchainResult
)}`
);
} catch (ex) {
console.log(`Exception for withdraw offchain -> ${JSON.stringify(ex)}`);
throw new Error(
`Withdrawal failure on server with exception -> ${JSON.stringify(ex)}`
);
}
return withdrawOffchainResult;
}
/**
* @description The withdraw offchain function in V2
* @param {WithdrawOffchainParamsV2} withdrawalParams Withdraw off-chain params input
* @throws {string} Exception: Sender starkKey is required!
* @throws {string} Exception: Receiver eth address is required!
* @throws {string} Exception: Quantum is required!
* @throws {string} Exception: Amount is required!
* @throws {string} Exception: Token Type is required.
* @throws {string} Exception: Token Address is required!
* @throws {string} Exception: Token ID is required!
* @returns {TransactionData} Transactions data which indicate the transaction results, transaction details information
* (such as transactionID, transactionStatus...)
* @example <caption>Sample code on Testnet (Staging) env</caption>
const mClient: IMyriaClient = {
networkId: Network.GOERLI,
provider: web3Instance.currentProvider,
web3: web3Instance,
env: EnvTypes.STAGING,
};
const senderStarkKey = '0xfb....'; // Sample of sender stark public key
const senderEthAddress = '0x....'; // Sender wallet address
const QUANTUM_CONSTANT = 10000000000; // Quantum 10^10
const weiAmount = 1000000000000000000; // 1 ETH we can use this page to convert the ETH to wei: https://eth-converter.com/
const withdrawParamsV2: WithdrawOffchainParamsV2 = {
senderPublicKey: senderStarkKey,
senderEthAddress: senderEthAddress,
receiverPublicKey: senderEthAddress,
amount: String(weiAmount),
tokenType: TokenType.ETH, // tokenType is ETH if users want to withdraw ETH
quantum: QUANTUM_CONSTANT.toString(),
};
responseWithdraw = await withdrawModule.withdrawalOffchainV2(
withdrawParamsV2,
);
console.log('Transaction result -> ', result);
*/
public async withdrawalOffchainV2(
withdrawalParams: WithdrawOffchainParamsV2
): Promise<TransactionData | undefined> {
if (!withdrawalParams.senderPublicKey) {
throw new Error("Sender starkKey is required!");
}
if (!withdrawalParams.receiverPublicKey) {
throw new Error("Receiver eth address is required!");
}
if (!withdrawalParams.quantum) {
throw new Error("Quantum is required!");
}
if(!withdrawalParams.amount) {
throw new Error("Amount is required!");
}
if(!withdrawalParams.tokenType) {
throw new Error("Token Type is required.");
}
if(withdrawalParams.tokenType === TokenType.ERC20) {
if(!withdrawalParams.tokenAddress) {
throw new Error("Token Address is required!");
}
}
if(withdrawalParams.tokenType === TokenType.ERC721) {
if(!withdrawalParams.tokenAddress) {
throw new Error("Token Address is required!");
}
if(!withdrawalParams.tokenId) {
throw new Error("Token ID is required!");
}
}
let withdrawalOffchainResult;
try {
const senderVault = await this.commonAPI.createVault({
quantum: withdrawalParams.quantum,
starkKey: withdrawalParams.senderPublicKey,
tokenType: withdrawalParams.tokenType,
tokenAddress: withdrawalParams.tokenAddress
});
if (!senderVault || senderVault.status !== "success") {
throw new Error(
`Failed to get vault for sender ${withdrawalParams.senderPublicKey}, please retry`
);
}
const receiverVault = await this.commonAPI.createVault(
{
quantum: withdrawalParams.quantum,
starkKey: withdrawalParams.receiverPublicKey,
tokenType: withdrawalParams.tokenType,
tokenAddress: withdrawalParams.tokenAddress
}
);
if (!receiverVault || receiverVault.status !== "success") {
throw new Error(
`Failed to get vault for receiver ${withdrawalParams.receiverPublicKey}, please retry`
);
}
const nonceByStarkKey = await this.commonAPI.getNonceByStarkKey(
withdrawalParams.senderPublicKey
);
// Get nonce from our BE server
const nonce = nonceByStarkKey?.data || Math.floor(Math.random() * 10000000) + 1;
// SET EXPIRATION TO BE EXPIRE IN 12 YEARS
const expirationTimestamp = new Date();
expirationTimestamp.setFullYear(expirationTimestamp.getFullYear() + 12);
const expirationTime = Math.floor(
expirationTimestamp.getTime() / (3600 * 1000)
);
const assetDict: AssetDictParams = {
type: withdrawalParams.tokenType,
data: {
quantum: withdrawalParams.quantum,
tokenAddress: withdrawalParams.tokenAddress
}
}
const assetId = this.commonModule.generateAssetId(assetDict);
const quantizedAmount = (new BN(withdrawalParams.amount, 10)).div(new BN(withdrawalParams.quantum, 10)).toString();
const msgBody: WithdrawOffchainParamsV2RequestAPI = {
senderVaultId: senderVault.data?.vaultId,
senderPublicKey: withdrawalParams.senderPublicKey,
receiverVaultId: receiverVault?.data?.vaultId,
receiverPublicKey: withdrawalParams.receiverPublicKey,
nonce: nonce,
expirationTimestamp: expirationTime,
quantizedAmount: quantizedAmount,
token: assetId,
senderEthAddress: withdrawalParams.senderEthAddress,
};
const starkSignature = await this.commonModule.generateStarkSignatureForWithdrawal(msgBody);
if (!starkSignature) {
throw new Error("Error on signing!");
}
const requestPayload: WithdrawOffchainPayloadV2 = {
senderVaultId: senderVault.data?.vaultId,
senderPublicKey: withdrawalParams.senderPublicKey,
receiverVaultId: receiverVault?.data?.vaultId,
receiverPublicKey: withdrawalParams.receiverPublicKey,
nonce: nonce,
expirationTimestamp: expirationTime,
signature: starkSignature,
quantizedAmount: quantizedAmount,
token: assetId,
};
const response = await this.withdrawalAPI.makeWithdrawalTransactionV2(
requestPayload
);
if (response?.status === "success") {
withdrawalOffchainResult = response?.data;
} else {
throw new Error("Withdrawal failed!");
}
} catch (err) {
console.log("Error -> ", err);
}
return withdrawalOffchainResult;
}
/**
* @description Function to check if user has registered on-chain with Starkware dedicated instance
* @param {string} starkKey Public stark key of user
* @returns {TxResult | undefined} On-chain transactions results information (block confirmed, transaction hash,....)
*/
public async checkUserRegisterOnchain(starkKey: string): Promise<TxResult | undefined> {
if (!starkKey) {
throw new Error('Stark key is required');
}
let txResult;
try {
txResult = await this.withdrawalContract.getEthKey(starkKey);
} catch (ex) {
console.log('[SDK-Exception] Error -> ', ex);
}
return txResult;
}
/**
* @description The withdraw on-chain function to withdraw the Tokens available in the On-chain to User's Wallet
* @typedef {Object} WithdrawOnchainParams The payload for requesting withdraw on-chain actions (assetType and starkKey)
* @param {WithdrawOnchainParams} withdrawalParams The withdraw on-chain params where it include both of the StarkKey and AssetType
* @param {SendOptions} options The native options for withdraw on-chain options
* @returns {Promise<TxResult>} The promise with transaction results
*/
public async withdrawalOnchain(
withdrawalParams: WithdrawOnchainParams,
options?: SendOptions
): Promise<TxResult> {
let txResult;
try {
txResult = await this.withdrawalContract.withdrawal(
withdrawalParams?.starkKey,
withdrawalParams?.assetType,
options
);
console.log(`Withdrawal onchain response -> ${JSON.stringify(txResult)}`);
} catch (ex) {
console.log(
`Exception for withdraw onchain -> ${JSON.stringify(txResult)}`
);
throw new Error(
`Withdraw onchain failure with error ${JSON.stringify(ex)}`
);
}
return txResult;
}
public async fullWithdrawal(payload: FullWithdrawalInput): Promise<any> {
if (!payload.ethAddress) {
throw new Error("User address is required!");
}
if (!payload.starkKey) {
throw new Error("User starkKey is required!");
}
if (!payload.vaultId) {
throw new Error("VaultId is required!");
}
if (!payload.nonce) {
throw new Error("Nonce is required!");
}
let fullWithdrawalResult;
try {
const starkSignature = await this.commonModule.generateStarkSignatureForFullWithdrawal(
payload.vaultId,
payload.nonce,
payload.ethAddress
);
if (!starkSignature) {
throw new Error("Sigining current request failed!");
} else {
const requestPayload: FullWithdrawalPayload = {
vaultId: payload.vaultId,
nonce: payload.nonce,
starkKey: payload.starkKey,
signature: starkSignature
}
const fullWithdrawalResponse = await this.withdrawalAPI.fullWithdrawal(requestPayload);
if (fullWithdrawalResponse?.status === "success") {
fullWithdrawalResult = fullWithdrawalResponse?.data;
} else {
throw new Error("Full withdrawal failed!");
}
}
} catch (err) {
throw new Error("Full withdrawal failed!");
}
return fullWithdrawalResult;
}
public async withdrawalNFT(
withdrawalParams: WithdrawERC721Params,
options?: SendOptions
): Promise<TxResult> {
let txResult;
try {
txResult = await this.withdrawalContract.withdrawalNft(
withdrawalParams?.ownerKey,
withdrawalParams?.assetType,
withdrawalParams?.tokenId,
options
);
console.log(`Withdraw nft onchain response -> ${txResult}`);
} catch (ex) {
console.log(
`Exception for withdraw nft onchain -> ${JSON.stringify(txResult)}`
);
throw new Error(
`Withdraw nft onchain failure with error ${JSON.stringify(ex)}`
);
}
return txResult;
}
/**
* @description Withdraw and mint the assets NFTs (ERC_721) in the on-chain and send the NFTs/tokens to user
* @param {WithdrawAndMintParams} withdrawalParams Withdraw and mint params options
* @param {SendOptions?} options The native options for transaction in Web3
* @returns {Promise<TxResult>} Transaction results in the on-chain after withdraw and mint action
*/
public async withdrawAndMint(
withdrawalParams: WithdrawAndMintParams,
options?: SendOptions
): Promise<TxResult> {
let txResult;
try {
txResult = await this.withdrawalContract.withdrawAndMint(
withdrawalParams.walletAddress,
withdrawalParams.assetType,
withdrawalParams?.mintingBlob,
options
);
console.log(`Withdrawal onchain response -> ${JSON.stringify(txResult)}`);
} catch (ex) {
console.log(
`Exception for withdraw onchain -> ${JSON.stringify(txResult)}`
);
throw new Error(
`Withdraw onchain failure with error ${JSON.stringify(ex)}`
);
}
return txResult;
}
/**
* @description The function is to get the available fund for user to be withdraw in the L1 and it requests to StarkEx smart contract
* @param {string} ownerKey The owner key of users (Wallet Address / Stark Key) where it locates the fund in the on-chain
* @param {string} assetId The asset ID (hex string) to be represent for the tokens that we'd to check with the available balance for withdraw
* @returns {Promise<TxResult>} The transaction result data which is on-chain data object
*/
public async getWithdrawalBalance(
ownerKey: string,
assetId: string,
options?: SendOptions
): Promise<TxResult> {
return this.withdrawalContract.getWithdrawalBalance(
ownerKey,
assetId,
options
);
}
/**
* @description The withdraw nft off-chain actions to bring the NFTs to user's wallet (Metamask/Trust Wallet)
* @param {WithdrawNftOffChainParams} payload Withdraw NFTs Off-chain params
* @returns {APIResponseType<WithdrawNftOffChainResponse> | undefined} The withdraw NFT off-chain response including transaction details data
*/
public async withdrawNftOffChain(
payload: WithdrawNftOffChainParams
): Promise<APIResponseType<WithdrawNftOffChainResponse> | undefined> {
let result: any;
if (!payload.id) {
throw new Error("Id is required");
}
if (!payload.tokenId) {
throw new Error("TokenId is required");
}
if (!payload.tokenAddress) {
throw new Error("Token address is required");
}
if (!payload.senderPublicKey) {
throw new Error("StarkKey is required");
}
if (!payload.receiverPublicKey) {
throw new Error("Receiver public key is required");
}
if (!payload.assetId) {
throw new Error("AssetId is required");
}
if (!Number.isInteger(Number(payload.quantizedAmount))) {
throw new Error("QuantizedAmount the required");
}
if (!Number.isInteger(Number(payload.senderVaultId))) {
throw new Error("Missing the vaultId");
}
const vaultForERC721: CreateVaultERC721ByEthKeyParams = {
tokenId: payload.tokenId,
depositEthAddress: payload.receiverPublicKey,
tokenAddress: payload.tokenAddress,
starkKey: payload.receiverPublicKey,
};
const assetVaultsByEthKey = await this.assetAPI.createERC721VaultByEthAddress(vaultForERC721);
const receiverVaultId = assetVaultsByEthKey?.data?.vaultId;
const nonceByStarkKey = await this.commonAPI.getNonceByStarkKey(
payload.senderPublicKey
);
const nonceData = nonceByStarkKey?.data || Math.floor(Math.random() * 100000000) + 1;
// const nonceData = Math.floor(Math.random() * 80000000);
// SET EXPIRATION TO BE EXPIRE IN 12 YEARS
const expirationTimestamp = new Date();
expirationTimestamp.setDate(expirationTimestamp.getFullYear() + 12);
const expirationTime = Math.floor(
expirationTimestamp.getTime() / (3600 * 1000)
);
// Generate signature
const msgBody: WithdrawOffchainParamsV2Hash = {
senderVaultId: payload.senderVaultId,
senderPublicKey: payload.senderPublicKey,
receiverVaultId: receiverVaultId,
receiverPublicKey: payload.receiverPublicKey,
nonce: nonceData,
expirationTimestamp: expirationTime,
quantizedAmount: payload.quantizedAmount,
token: payload.assetId,
senderEthAddress: payload.receiverPublicKey,
};
const starkSignature = await this.commonModule.generateStarkSignatureForWithdrawal(msgBody);
if (!starkSignature) {
throw new Error("Error on signing!");
}
const requestPayload: WithdrawNftOffChainRequestAPI = {
id: payload.id,
senderVaultId: payload.senderVaultId,
senderPublicKey: payload.senderPublicKey,
receiverVaultId: receiverVaultId,
receiverPublicKey: payload.receiverPublicKey,
token: payload.assetId,
quantizedAmount: payload.quantizedAmount,
nonce: nonceData,
expirationTimestamp: expirationTime,
signature: starkSignature,
};
try {
const withdrawRes = await this.withdrawalMarketpAPI.requestWithdrawNftOffChain(
requestPayload
);
if (withdrawRes?.status === "success") {
result = withdrawRes?.data;
} else {
throw new Error("Withdraw nft onchain failure");
}
} catch (error) {
throw new Error(
`Withdraw nft onchain failure with error ${JSON.stringify(error)}`
);
}
return result;
}
/**
* @description As long as the NFTs has been completed with the on-chain withdraw and user receive the NFTs/tokens into their wallet (Metamask,etc...)
* This is one last step for tracking and notify to Myria service that this NFTs has been completed on withdraw process and
* the token is no longer existed in Myria system
* @param {WithdrawNftCompleteParams} payload The payload for requesting the NFTs withdraw completed in Myria system
* @returns {APIResponseType<WithdrawNftCompleteResponse> | undefined} The withdraw nft completion response from Myria services
*/
public async withdrawNftComplete(
payload: WithdrawNftCompleteParams
): Promise<APIResponseType<WithdrawNftCompleteResponse> | undefined> {
let result: any;
if (!payload.assetId) {
throw new Error("Asset id is required");
}
if (!payload.starkKey) {
throw new Error("StarkKey is required");
}
if (!payload.transactionHash) {
throw new Error("Transaction hash is required");
}
try {
const assetVaultDetails = await this.assetAPI.getAssetVaultDetails({
starkKey: payload.starkKey,
assetId: payload.assetId,
});
if (assetVaultDetails?.data && assetVaultDetails?.status === "success") {
const payloadWithDrawComplete: WithdrawNftCompleteRequestAPI = {
vaultId: assetVaultDetails.data?.vaultId,
starkKey: payload.starkKey,
assetId: assetVaultDetails.data?.assetId,
transactionHash: payload.transactionHash
};
const withdrawCompleteRes = await retryPromise(this.withdrawalMarketpAPI.requestWithdrawNftComplete(payloadWithDrawComplete), RETRY_DEFAULT, DELAY_IN_RETRY);
if (withdrawCompleteRes?.status === "success") {
result = withdrawCompleteRes?.data;
} else {
throw new Error("Withdraw Complete failure");
}
}
} catch (error) {
throw new Error(
`WithdrawNft Complete failure with error ${JSON.stringify(error)}`
);
}
return result;
}
}
Source