Asset deposits and withdrawals
Applications may want to enable users to:
- Deposit assets on L1 to L2 (note these pre-requisites)
- Withdraw assets from L2 to L1
For more information and use cases, see our explanatory article, Deep dive into deposits and withdrawals.
Deposit pre-requisites:
- The user depositing the asset must be registered with Immutable X
- The user must own the asset on L1
- The L1 smart contract holding the asset must be registered as a collection
Typescript SDK
1. Initialize the SDK
In order to use the SDK, you need to initialize it.
2. Generate signers
Enabling users to transfer assets requires a user's signature, so your application will need to create signers. See the guide on how to generate signers. Then setup GenericIMXProvider
3. Deposit assets onto L2
Request
The function below will deposit ETH from L1 to L2. If you wish to deposit a different asset, you can use different TokenAmount to represent ETH, ERC20 or ERC721.
(async (): Promise<void> => {
// Create deposit
const deposit = await imxProvider.deposit({
type: 'ETH', // There are three avaible types: ETH, ERC20, ERC721
amount: '50000000000000000', // Amount in wei, this currently is 0.05 ETH
});
console.log(deposit);
})().catch((e) => {
// implement error handling here
});
Example response
{
type: 2, // Transaction type, currently 2 means EIP-1559
chainId: 11155111, // The Chain ID of the network, 11155111 is the Chain ID for Sepolia.
nonce: 5, // The nonce of the transaction used
maxPriorityFeePerGas: BigNumber { _hex: '0x59682f00', _isBigNumber: true }, // Maximum fee to bribe miners into giving this transaction priority
maxFeePerGas: BigNumber { _hex: '0x5968cf94', _isBigNumber: true }, // The maximum fee per gas that the sender is willing to pay
gasPrice: null, // The gas price of the transaction determined by the network
gasLimit: BigNumber { _hex: '0x013617', _isBigNumber: true }, // The maximum amount of gas that the transaction can use
to: '0x7917eDb51ecD6CdB3F9854c3cc593F33de10c623', // The address of the recipient
value: BigNumber { _hex: '0xb1a2bc2ec50000', _isBigNumber: true }, // The amount of ETH to send in wei
data: '0x00aeef8a02f9a87a2eae83e024312ea33e2bec5df10cdef1470f6aaf5d5adb23f735a60002705737cd248ac819034b5de474c8f0368224f72a0fda9e031499d519992d9e000000000000000000000000000000000000000000000000000000000001ed7a', // Data included in this log
accessList: [], // Optional list of addresses and storage keys the transaction can access
hash: '0x357d6e1c09cf09b5746279bb77377b2353deb1c538eb18c8c9fa9e2363aa3d58', // The hash of the transaction
v: 0, // The recovery ID of the ECDSA signature
r: '0xc02c07ff0f80405f34917c9e300abff5da7dda319828fe1843a06f041cd3cbf5', // The first 32 bytes of the ECDSA signature
s: '0x5c08b76a9ab9f766b82e6f273a1503d064d1604b8e071d7267a6735c5900bb1c', // The second 32 bytes of the ECDSA signature
from: '0xF656956cA54778056f1191791876db54Aa6eb61B', // The address of the sender
confirmations: 0, // The number of confirmations that the transaction has received
wait: [Function (anonymous)] // A function that returns a promise that resolves when the transaction is mined
}
4. Withdraw assets to L1
API
Deposit
To understand what is going on under the hood with asset deposits, please see this explainer.
Steps:
1. Get details of the deposit
Javascript example of depositing ETH from L1 to L2:
import fetch from 'node-fetch';
const depositDetails = {
amount: '0.001',
token: {
type: 'ETH',
data: {
decimals: 18,
},
},
user: '0x..', // Public L1 Ethereum address
};
fetch('https://api.sandbox.x.immutable.com/v1/signable-deposit-details', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(depositDetails),
})
.then((response) => {
console.log(response);
})
.catch((err) => {
console.error(err);
});
Example response:
{
"amount": "string", // Amount this user is depositing
"asset_id": "string", // ID of the asset this user is depositing (applicable only to depositing ERC721)
"nonce": 0, // Random number generated of this transaction to verify that specific values are not reused
"stark_key": "string", // Public stark key of the depositing user
"vault_id": 0 // ID of the vault this user is depositing to
}
The following response params are used in the depositEth contract method (see Step 3):
stark_key
vault_id
2. Generate signers
Enabling users to deposit assets requires a user's signature, so your application will need to create signers. See the guide on how to generate signers.
3. Call contract method to deposit tokens
import { Wallet } from "@ethersproject/wallet";
import { x, config } from "@imtbl/sdk";
const {
Environment,
} = config;
const {
Contracts,
IMXClient,
imxClientConfig, // helper method to create a client config
} = x;
// Create Ethereum Signer
const ethSigner = new Wallet('YOUR_PRIVATE_ETH_KEY');
// Initialize client
const environment = Environment.SANDBOX; // or Environment.PRODUCTION
const client = new IMXClient(imxClientConfig({ environment }));;
const coreContractAddress = client.imxConfig.immutableXConfig.ethConfiguration.coreContractAddress;
// Get instance of core contract
const contract = Contracts.Core.connect(
coreContractAddress,
ethSigner
);
// Populate and send transaction
const populatedTransaction = await contract.populateTransaction.depositEth(
starkPublicKey, // Use `stark_key` obtained in previous step
assetType, // "ETH", "ERC20" or "ERC721"
vaultId // Use `vault_id` obtained in previous step
);
const transactionResponse = await ethSigner.sendTransaction(
populatedTransaction
);
Withdrawal
To understand what is going on under the hood with asset withdrawals, please see this explainer.
Steps:
- Get details of the withdrawal
- Generate signers
- Create withdrawal
- Call contract to complete withdrawal
1. Get details of the withdrawal
- StarkEx v3
- StarkEx v4
Javascript example of withdrawing ETH from L2 to L1:
import fetch from 'node-fetch';
const withdrawalDetails = {
amount: '0.001',
token: {
type: 'ETH',
data: {
decimals: 18,
},
},
user: '0x..', // Public L1 Ethereum address
};
fetch('https://api.sandbox.x.immutable.com/v1/signable-withdrawal-details', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(withdrawalDetails),
})
.then((response) => {
console.log(response);
})
.catch((err) => {
console.error(err);
});
Example response:
{
"amount": "string",
"asset_id": "string",
"nonce": 0,
"payload_hash": "string",
"signable_message": "string",
"stark_key": "string",
"vault_id": 0
}
Explanation:
Response param | Description | How is it used in the createWithdrawal request? (See Step 3) |
---|---|---|
amount | Amount of token to be withdrawn to L1 | As amount in the request body |
asset_id | ID of the asset this user is withdrawing (applicable only to ERC721 asset type) | As asset_id in the request body |
nonce | Random number generated of this transaction to verify that specific values are not reused | As nonce in the request body |
payload_hash | Encoded payload hash | Used to generate the stark_signature in the request body by using the Stark (L2) signer to sign the payload_hash . |
signable_message | Message to sign with L1 wallet to verity withdrawal request | Used to generate the x-imx-eth-signature header by using the Ethereum (L1) signer to sign the signable_message |
stark_key | Public stark key of the withdrawing user | As stark_key in the request body |
vault_id | The ID of the vault the asset belong to | As vault_id in the request body |
2. Generate signers
Enabling users to withdraw assets requires a user's signature, so your application will need to create signers. See the guide on how to generate signers.
3. Create withdrawal
Javascript example of withdrawing ETH from L2 to L1:
import fetch from 'node-fetch';
// See previous step for how to generate signers
const ethSignature = ethSigner.sign(signable_message);
const starkSignature = starkSigner.sign(payload_hash);
const withdrawalBody = {
amount: 'string',
asset_id: 'string',
nonce: 'string',
stark_key: 'string',
stark_signature: starkSignature,
vauld_id: 'string',
};
fetch('https://api.sandbox.x.immutable.com/v1/withdrawals', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-imx-eth-address': '0x..', // Public Ethereum address of the withdrawing user
'x-imx-eth-signature': ethSignature,
},
body: JSON.stringify(withdrawalBody),
})
.then((response) => {
console.log(response);
})
.catch((err) => {
console.error(err);
});
4. Get the assetType
value
This value is required in the next step to complete the withdrawal.
import fetch from 'node-fetch';
const assetType = 'asset'; // "mintable-asset" if you are withdrawing an ERC721 token that was minted on L2 to L1 for the first time
const encodeAssetBody = {
token: {
data: {
blueprint: 'string',
id: 'string',
token_address: 'string',
token_id: 'string',
},
type: 'ETH', // Or "ERC20" or "ERC721"
},
};
fetch(`https://api.sandbox.x.immutable.com/v1/encode/${assetType}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-imx-eth-address': '0x..', // Public Ethereum address of the withdrawing user
'x-imx-eth-signature': ethSignature,
},
body: JSON.stringify(encodeAssetBody),
})
.then((response) => {
console.log(response);
// Example response
// {
// "asset_id": "string",
// "asset_type": "string"
// }
})
.catch((err) => {
console.error(err);
});
5. Call contract to complete withdrawal
import { x, config } from "@imtbl/sdk";
import { Wallet } from "@ethersproject/wallet";
const {
Environment,
} = config;
const {
Contracts,
IMXClient,
imxClientConfig, // helper method to create a client config
} = x;
// Create Ethereum Signer
const ethSigner = new Wallet('YOUR_PRIVATE_ETH_KEY');
// Initialize client
const environment = Environment.SANDBOX; // or Environment.PRODUCTION
const client = new IMXClient(imxClientConfig({ environment }));;
const coreContractAddress = client.imxConfig.immutableXConfig.ethConfiguration.coreContractAddress;
// Get instance of core contract
const contract = Contracts.Core.connect(
coreContractAddress,
ethSigner
);
// Get instance of core contract
const contract = Contracts.Core.connect(coreContractAddress, ethSigner);
// Populate and send transaction
const populatedTransaction = await contract.populateTransaction.withdraw(
starkPublicKey, // Use `stark_key` obtained in previous step
assetType // Use the `asset_type` value returned in the response object in step 4
);
const transactionResponse = await signer.sendTransaction(
populatedTransaction
);
Javascript example of withdrawing ETH from L2 to L1:
import fetch from 'node-fetch';
const withdrawalDetails = {
amount: '0.001',
token: {
type: 'ETH',
data: {
decimals: 18,
},
},
user: '0x..', // Public L1 Ethereum address
};
fetch('https://api.sandbox.x.immutable.com/v2/signable-withdrawal-details', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(withdrawalDetails),
})
.then((response) => {
console.log(response);
})
.catch((err) => {
console.error(err);
});
Example response:
{
"amount": "string",
"asset_id": "string",
"expiration_timestamp": 0,
"nonce": 0,
"payload_hash": "string",
"quantized_amount": "string",
"readable_transaction": "string",
"receiver_stark_key": "string",
"receiver_vault_id": 0,
"sender_stark_key": "string",
"sender_vault_id": 0,
"signable_message": "string",
"verification_signature": "string"
}
Explanation:
Response param | Description | How is it used in the createWithdrawalV2 request? (See Step 3) |
---|---|---|
amount | Amount of token to be withdrawn to L1 | As amount in the request body |
asset_id | ID of the asset this user is withdrawing (applicable only to ERC721 asset type) | As asset_id in the request body |
expiration_timestamp | The expiration timestamp of the withdrawal | As expiration_timestamp in the request body |
nonce | Random number generated of this transaction to verify that specific values are not reused | As nonce in the request body |
payload_hash | Encoded payload hash | Used to generate the stark_signature in the request body by using the Stark (L2) signer to sign the payload_hash |
quantized_amount | QuantizedAmount of the asset being withdrawn | Not used |
readable_transaction | EIP-712 encoding of the StarkEx withdrawal request to be displayed to the user | Not used |
receiver_stark_key | Receiver of the withdrawal (l1 eth wallet) | As receiver_stark_key in the request body |
receiver_vault_id | ID of the vault being withdrawn to (special vault = 0 used for withdrawal operations) | As receiver_vault_id in the request body |
sender_stark_key | Sender of the withdrawal (l2 wallet) | As sender_stark_key in the request body |
sender_vault_id | ID of the vault being withdrawn from | As sender_vault_id in the request body |
signable_message | Message to sign with L1 wallet to verity withdrawal request | Used to generate the x-imx-eth-signature header by using the Ethereum (L1) signer to sign the signable_message |
verification_signature | IMX signed readable_transaction and payload_hash | Not used |
2. Generate signers
Enabling users to withdraw assets requires a user's signature, so your application will need to create signers. See the guide on how to generate signers.
3. Create v2 withdrawal
Javascript example of withdrawing ETH from L2 to L1:
import fetch from 'node-fetch';
// See previous step for how to generate signers
const ethSignature = ethSigner.sign(signable_message);
const starkSignature = starkSigner.sign(payload_hash);
const withdrawalBody = {
amount: 'string',
asset_id: 'string',
nonce: 'string',
stark_key: 'string',
stark_signature: starkSignature,
expiration_timestamp: 0,
receiver_stark_key: 'string',
receiver_vault_id: 0,
sender_stark_key: 'string',
sender_vault_id: 0,
};
fetch('https://api.sandbox.x.immutable.com/v2/withdrawals', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-imx-eth-address': '0x..', // Public Ethereum address of the withdrawing user
'x-imx-eth-signature': ethSignature,
},
body: JSON.stringify(withdrawalBody),
})
.then((response) => {
console.log(response);
})
.catch((err) => {
console.error(err);
});
4. Get the assetType
value
This value is required in the next step to complete the withdrawal.
import fetch from 'node-fetch';
const assetType = 'asset'; // "mintable-asset" if you are withdrawing an ERC721 token that was minted on L2 to L1 for the first time
const encodeAssetBody = {
token: {
data: {
blueprint: 'string',
id: 'string',
token_address: 'string',
token_id: 'string',
},
type: 'ETH', // Or "ERC20" or "ERC721"
},
};
fetch(`https://api.sandbox.x.immutable.com/v1/encode/${assetType}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-imx-eth-address': '0x..', // Public Ethereum address of the withdrawing user
'x-imx-eth-signature': ethSignature,
},
body: JSON.stringify(encodeAssetBody),
})
.then((response) => {
console.log(response);
// Example response
// {
// "asset_id": "string",
// "asset_type": "string"
// }
})
.catch((err) => {
console.error(err);
});
5. Call contract to complete withdrawal
import { x, config } from "@imtbl/sdk";
import { Wallet } from "@ethersproject/wallet";
const {
Environment,
} = config;
const {
Contracts,
IMXClient,
imxClientConfig, // helper method to create a client config
} = x;
// Create Ethereum Signer
const ethSigner = new Wallet('YOUR_PRIVATE_ETH_KEY');
// Initialize client
const environment = Environment.SANDBOX; // or Environment.PRODUCTION
const client = new IMXClient(imxClientConfig({ environment }));;
const coreContractAddress = client.imxConfig.immutableXConfig.ethConfiguration.coreContractAddress;
// Get instance of core contract
const contract = Contracts.Core.connect(
coreContractAddress,
ethSigner
);
const ownerKey = await ethSigner.getAddress();
// Populate and send transaction
const populatedTransaction = await contract.populateTransaction.withdraw(
ownerKey, // Ethereum address of the withdrawing user
assetType // Use the `asset_type` value returned in the response object in step 4
);
const transactionResponse = await ethSigner.sendTransaction(
populatedTransaction
);