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
Core SDK
1. Initialize the Core SDK
In order to use the Core 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.
3. Deposit assets onto L2
- Typescript Core SDK
The function below will deposit ETH from L1 to L2. If you wish to deposit a different asset, you can change type
to either ERC20
or ERC721
and specify the asset address as an additional parameter.
(async (): Promise<void> => {
// Create deposit
const deposit = await client.deposit(ethSigner, {
// ethSigner obtained from generating signer
type: 'ETH', // There are three avaible types: ETH, ERC20, ERC721
amount: '50000000000000000', // Amount in wei, this currently is 0.05 ETH
});
// console.log(deposit); // Uncomment to see the deposit object
})().catch((e) => {
log.error(component, e);
});
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
- Typescript Core SDK
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
);