solana/docs/src/integrations/exchange.md

753 lines
27 KiB
Markdown

---
title: Add Solana to Your Exchange
---
This guide describes how to add Solana's native token SOL to your cryptocurrency
exchange.
## Node Setup
We highly recommend setting up at least two nodes on high-grade computers/cloud
instances, upgrading to newer versions promptly, and keeping an eye on service
operations with a bundled monitoring tool.
This setup enables you:
- to have a trusted gateway to the Solana mainnet-beta cluster to get data and
submit withdrawal transactions
- to have full control over how much historical block data is retained
- to maintain your service availability even if one node fails
Solana nodes demand relatively high computing power to handle our fast blocks
and high TPS. For specific requirements, please see
[hardware recommendations](../running-validator/validator-reqs.md).
To run an api node:
1. [Install the Solana command-line tool suite](../cli/install-solana-cli-tools.md)
2. Start the validator with at least the following parameters:
```bash
solana-validator \
--ledger <LEDGER_PATH> \
--entrypoint <CLUSTER_ENTRYPOINT> \
--expected-genesis-hash <EXPECTED_GENESIS_HASH> \
--rpc-port 8899 \
--no-voting \
--enable-rpc-transaction-history \
--limit-ledger-size \
--trusted-validator <VALIDATOR_ADDRESS> \
--no-untrusted-rpc
```
Customize `--ledger` to your desired ledger storage location, and `--rpc-port` to the port you want to expose.
The `--entrypoint` and `--expected-genesis-hash` parameters are all specific to the cluster you are joining.
[Current parameters for Mainnet Beta](../clusters.md#example-solana-validator-command-line-2)
The `--limit-ledger-size` parameter allows you to specify how many ledger
[shreds](../terminology.md#shred) your node retains on disk. If you do not
include this parameter, the validator will keep the entire ledger until it runs
out of disk space. The default value attempts to keep the ledger disk usage
under 500GB. More or less disk usage may be requested by adding an argument to
`--limit-ledger-size` if desired. Check `solana-validator --help` for the
default limit value used by `--limit-ledger-size`. More information about
selecting a custom limit value is [available
here](https://github.com/solana-labs/solana/blob/583cec922b6107e0f85c7e14cb5e642bc7dfb340/core/src/ledger_cleanup_service.rs#L15-L26).
Specifying one or more `--trusted-validator` parameters can protect you from booting from a malicious snapshot. [More on the value of booting with trusted validators](../running-validator/validator-start.md#trusted-validators)
Optional parameters to consider:
- `--private-rpc` prevents your RPC port from being published for use by other nodes
- `--rpc-bind-address` allows you to specify a different IP address to bind the RPC port
### Automatic Restarts and Monitoring
We recommend configuring each of your nodes to restart automatically on exit, to
ensure you miss as little data as possible. Running the solana software as a
systemd service is one great option.
For monitoring, we provide
[`solana-watchtower`](https://github.com/solana-labs/solana/blob/master/watchtower/README.md),
which can monitor your validator and detect with the `solana-validator` process
is unhealthy. It can directly be configured to alert you via Slack, Telegram,
Discord, or Twillio. For details, run `solana-watchtower --help`.
```bash
solana-watchtower --validator-identity <YOUR VALIDATOR IDENTITY>
```
#### New Software Release Announcements
We release new software frequently (around 1 release / week).
Sometimes newer versions include incompatible protocol changes, which
necessitate timely software update to avoid errors in processing blocks.
Our official release announcements for all kinds of releases (normal and
security) are communicated via a discord channel called
[`#mb-announcement`](https://discord.com/channels/428295358100013066/669406841830244375)
(`mb` stands for `mainnet-beta`).
Like staked validators, we expect any exchange-operated validators to be updated
at your earliest convenience within a business day or two after a normal release
announcement. For security-related releases, more urgent action may be needed.
### Ledger Continuity
By default, each of your nodes will boot from a snapshot provided by one of your
trusted validators. This snapshot reflects the current state of the chain, but
does not contain the complete historical ledger. If one of your node exits and
boots from a new snapshot, there may be a gap in the ledger on that node. In
order to prevent this issue, add the `--no-snapshot-fetch` parameter to your
`solana-validator` command to receive historical ledger data instead of a
snapshot.
Do not pass the `--no-snapshot-fetch` parameter on your initial boot as it's not
possible to boot the node all the way from the genesis block. Instead boot from
a snapshot first and then add the `--no-snapshot-fetch` parameter for reboots.
It is important to note that the amount of historical ledger available to your
nodes from the rest of the network is limited at any point in time. Once
operational if your validators experience significant downtime they may not be
able to catch up to the network and will need to download a new snapshot from a
trusted validator. In doing so your validators will now have a gap in its
historical ledger data that cannot be filled.
### Minimizing Validator Port Exposure
The validator requires that various UDP and TCP ports be open for inbound
traffic from all other Solana validators. While this is the most efficient mode of
operation, and is strongly recommended, it is possible to restrict the
validator to only require inbound traffic from one other Solana validator.
First add the `--restricted-repair-only-mode` argument. This will cause the
validator to operate in a restricted mode where it will not receive pushes from
the rest of the validators, and instead will need to continually poll other
validators for blocks. The validator will only transmit UDP packets to other
validators using the _Gossip_ and _ServeR_ ("serve repair") ports, and only
receive UDP packets on its _Gossip_ and _Repair_ ports.
The _Gossip_ port is bi-directional and allows your validator to remain in
contact with the rest of the cluster. Your validator transmits on the _ServeR_
to make repair requests to obtaining new blocks from the rest of the network,
since Turbine is now disabled. Your validator will then receive repair
responses on the _Repair_ port from other validators.
To further restrict the validator to only requesting blocks from one or more
validators, first determine the identity pubkey for that validator and add the
`--gossip-pull-validator PUBKEY --repair-validator PUBKEY` arguments for each
PUBKEY. This will cause your validator to be a resource drain on each validator
that you add, so please do this sparingly and only after consulting with the
target validator.
Your validator should now only be communicating with the explicitly listed
validators and only on the _Gossip_, _Repair_ and _ServeR_ ports.
## Setting up Deposit Accounts
Solana accounts do not require any on-chain initialization; once they contain
some SOL, they exist. To set up a deposit account for your exchange, simply
generate a Solana keypair using any of our [wallet tools](../wallet-guide/cli.md).
We recommend using a unique deposit account for each of your users.
Solana accounts are charged [rent](developing/programming-model/accounts.md#rent) on creation and once per
epoch, but they can be made rent-exempt if they contain 2-years worth of rent in
SOL. In order to find the minimum rent-exempt balance for your deposit accounts,
query the
[`getMinimumBalanceForRentExemption` endpoint](developing/clients/jsonrpc-api.md#getminimumbalanceforrentexemption):
```bash
curl -X POST -H "Content-Type: application/json" -d '{"jsonrpc": "2.0","id":1,"method":"getMinimumBalanceForRentExemption","params":[0]}' localhost:8899
{"jsonrpc":"2.0","result":890880,"id":1}
```
### Offline Accounts
You may wish to keep the keys for one or more collection accounts offline for
greater security. If so, you will need to move SOL to hot accounts using our
[offline methods](../offline-signing.md).
## Listening for Deposits
When a user wants to deposit SOL into your exchange, instruct them to send a
transfer to the appropriate deposit address.
### Poll for Blocks
To track all the deposit accounts for your exchange, poll for each confirmed
block and inspect for addresses of interest, using the JSON-RPC service of your
Solana API node.
- To identify which blocks are available, send a [`getConfirmedBlocks` request](developing/clients/jsonrpc-api.md#getconfirmedblocks),
passing the last block you have already processed as the start-slot parameter:
```bash
curl -X POST -H "Content-Type: application/json" -d '{"jsonrpc": "2.0","id":1,"method":"getConfirmedBlocks","params":[5]}' localhost:8899
{"jsonrpc":"2.0","result":[5,6,8,9,11],"id":1}
```
Not every slot produces a block, so there may be gaps in the sequence of integers.
- For each block, request its contents with a [`getConfirmedBlock` request](developing/clients/jsonrpc-api.md#getconfirmedblock):
```bash
curl -X POST -H "Content-Type: application/json" -d '{"jsonrpc": "2.0","id":1,"method":"getConfirmedBlock","params":[5, "json"]}' localhost:8899
{
"jsonrpc": "2.0",
"result": {
"blockhash": "2WcrsKSVANoe6xQHKtCcqNdUpCQPQ3vb6QTgi1dcE2oL",
"parentSlot": 4,
"previousBlockhash": "7ZDoGW83nXgP14vnn9XhGSaGjbuLdLWkQAoUQ7pg6qDZ",
"rewards": [],
"transactions": [
{
"meta": {
"err": null,
"fee": 5000,
"postBalances": [
2033973061360,
218099990000,
42000000003
],
"preBalances": [
2044973066360,
207099990000,
42000000003
],
"status": {
"Ok": null
}
},
"transaction": {
"message": {
"accountKeys": [
"Bbqg1M4YVVfbhEzwA9SpC9FhsaG83YMTYoR4a8oTDLX",
"47Sbuv6jL7CViK9F2NMW51aQGhfdpUu7WNvKyH645Rfi",
"11111111111111111111111111111111"
],
"header": {
"numReadonlySignedAccounts": 0,
"numReadonlyUnsignedAccounts": 1,
"numRequiredSignatures": 1
},
"instructions": [
{
"accounts": [
0,
1
],
"data": "3Bxs3zyH82bhpB8j",
"programIdIndex": 2
}
],
"recentBlockhash": "7GytRgrWXncJWKhzovVoP9kjfLwoiuDb3cWjpXGnmxWh"
},
"signatures": [
"dhjhJp2V2ybQGVfELWM1aZy98guVVsxRCB5KhNiXFjCBMK5KEyzV8smhkVvs3xwkAug31KnpzJpiNPtcD5bG1t6"
]
}
}
]
},
"id": 1
}
```
The `preBalances` and `postBalances` fields allow you to track the balance
changes in every account without having to parse the entire transaction. They
list the starting and ending balances of each account in
[lamports](../terminology.md#lamport), indexed to the `accountKeys` list. For
example, if the deposit address if interest is
`47Sbuv6jL7CViK9F2NMW51aQGhfdpUu7WNvKyH645Rfi`, this transaction represents a
transfer of 218099990000 - 207099990000 = 11000000000 lamports = 11 SOL
If you need more information about the transaction type or other specifics, you
can request the block from RPC in binary format, and parse it using either our
[Rust SDK](https://github.com/solana-labs/solana) or
[Javascript SDK](https://github.com/solana-labs/solana-web3.js).
### Address History
You can also query the transaction history of a specific address. This is
generally _not_ a viable method for tracking all your deposit addresses over all
slots, but may be useful for examining a few accounts for a specific period of
time.
- Send a [`getConfirmedSignaturesForAddress2`](developing/clients/jsonrpc-api.md#getconfirmedsignaturesforaddress2)
request to the api node:
```bash
curl -X POST -H "Content-Type: application/json" -d '{"jsonrpc": "2.0","id":1,"method":"getConfirmedSignaturesForAddress2","params":["6H94zdiaYfRfPfKjYLjyr2VFBg6JHXygy84r3qhc3NsC", {"limit": 3}]}' localhost:8899
{
"jsonrpc": "2.0",
"result": [
{
"err": null,
"memo": null,
"signature": "35YGay1Lwjwgxe9zaH6APSHbt9gYQUCtBWTNL3aVwVGn9xTFw2fgds7qK5AL29mP63A9j3rh8KpN1TgSR62XCaby",
"slot": 114
},
{
"err": null,
"memo": null,
"signature": "4bJdGN8Tt2kLWZ3Fa1dpwPSEkXWWTSszPSf1rRVsCwNjxbbUdwTeiWtmi8soA26YmwnKD4aAxNp8ci1Gjpdv4gsr",
"slot": 112
},
{
"err": null,
"memo": null,
"signature": "dhjhJp2V2ybQGVfELWM1aZy98guVVsxRCB5KhNiXFjCBMK5KEyzV8smhkVvs3xwkAug31KnpzJpiNPtcD5bG1t6",
"slot": 108
}
],
"id": 1
}
```
- For each signature returned, get the transaction details by sending a
[`getConfirmedTransaction`](developing/clients/jsonrpc-api.md#getconfirmedtransaction) request:
```bash
curl -X POST -H "Content-Type: application/json" -d '{"jsonrpc": "2.0","id":1,"method":"getConfirmedTransaction","params":["dhjhJp2V2ybQGVfELWM1aZy98guVVsxRCB5KhNiXFjCBMK5KEyzV8smhkVvs3xwkAug31KnpzJpiNPtcD5bG1t6", "json"]}' localhost:8899
// Result
{
"jsonrpc": "2.0",
"result": {
"slot": 5,
"transaction": {
"message": {
"accountKeys": [
"Bbqg1M4YVVfbhEzwA9SpC9FhsaG83YMTYoR4a8oTDLX",
"47Sbuv6jL7CViK9F2NMW51aQGhfdpUu7WNvKyH645Rfi",
"11111111111111111111111111111111"
],
"header": {
"numReadonlySignedAccounts": 0,
"numReadonlyUnsignedAccounts": 1,
"numRequiredSignatures": 1
},
"instructions": [
{
"accounts": [
0,
1
],
"data": "3Bxs3zyH82bhpB8j",
"programIdIndex": 2
}
],
"recentBlockhash": "7GytRgrWXncJWKhzovVoP9kjfLwoiuDb3cWjpXGnmxWh"
},
"signatures": [
"dhjhJp2V2ybQGVfELWM1aZy98guVVsxRCB5KhNiXFjCBMK5KEyzV8smhkVvs3xwkAug31KnpzJpiNPtcD5bG1t6"
]
},
"meta": {
"err": null,
"fee": 5000,
"postBalances": [
2033973061360,
218099990000,
42000000003
],
"preBalances": [
2044973066360,
207099990000,
42000000003
],
"status": {
"Ok": null
}
}
},
"id": 1
}
```
## Sending Withdrawals
To accommodate a user's request to withdraw SOL, you must generate a Solana
transfer transaction, and send it to the api node to be forwarded to your
cluster.
### Synchronous
Sending a synchronous transfer to the Solana cluster allows you to easily ensure
that a transfer is successful and finalized by the cluster.
Solana's command-line tool offers a simple command, `solana transfer`, to
generate, submit, and confirm transfer transactions. By default, this method
will wait and track progress on stderr until the transaction has been finalized
by the cluster. If the transaction fails, it will report any transaction errors.
```bash
solana transfer <USER_ADDRESS> <AMOUNT> --allow-unfunded-recipient --keypair <KEYPAIR> --url http://localhost:8899
```
The [Solana Javascript SDK](https://github.com/solana-labs/solana-web3.js)
offers a similar approach for the JS ecosystem. Use the `SystemProgram` to build
a transfer transaction, and submit it using the `sendAndConfirmTransaction`
method.
### Asynchronous
For greater flexibility, you can submit withdrawal transfers asynchronously. In
these cases, it is your responsibility to verify that the transaction succeeded
and was finalized by the cluster.
**Note:** Each transaction contains a [recent
blockhash](developing/programming-model/transactions.md#blockhash-format) to
indicate its liveness. It is **critical** to wait until this blockhash expires
before retrying a withdrawal transfer that does not appear to have been
confirmed or finalized by the cluster. Otherwise, you risk a double spend. See
more on [blockhash expiration](#blockhash-expiration) below.
First, get a recent blockhash using the [`getFees` endpoint](developing/clients/jsonrpc-api.md#getfees)
or the CLI command:
```bash
solana fees --url http://localhost:8899
```
In the command-line tool, pass the `--no-wait` argument to send a transfer
asynchronously, and include your recent blockhash with the `--blockhash` argument:
```bash
solana transfer <USER_ADDRESS> <AMOUNT> --no-wait --allow-unfunded-recipient --blockhash <RECENT_BLOCKHASH> --keypair <KEYPAIR> --url http://localhost:8899
```
You can also build, sign, and serialize the transaction manually, and fire it off to
the cluster using the JSON-RPC [`sendTransaction` endpoint](developing/clients/jsonrpc-api.md#sendtransaction).
#### Transaction Confirmations & Finality
Get the status of a batch of transactions using the
[`getSignatureStatuses` JSON-RPC endpoint](developing/clients/jsonrpc-api.md#getsignaturestatuses).
The `confirmations` field reports how many
[confirmed blocks](../terminology.md#confirmed-block) have elapsed since the
transaction was processed. If `confirmations: null`, it is [finalized](../terminology.md#finality).
```bash
curl -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0", "id":1, "method":"getSignatureStatuses", "params":[["5VERv8NMvzbJMEkV8xnrLkEaWRtSz9CosKDYjCJjBRnbJLgp8uirBgmQpjKhoR4tjF3ZpRzrFmBV6UjKdiSZkQUW", "5j7s6NiJS3JAkvgkoc18WVAsiSaci2pxB2A6ueCJP4tprA2TFg9wSyTLeYouxPBJEMzJinENTkpA52YStRW5Dia7"]]}' http://localhost:8899
{
"jsonrpc": "2.0",
"result": {
"context": {
"slot": 82
},
"value": [
{
"slot": 72,
"confirmations": 10,
"err": null,
"status": {
"Ok": null
}
},
{
"slot": 48,
"confirmations": null,
"err": null,
"status": {
"Ok": null
}
}
]
},
"id": 1
}
```
#### Blockhash Expiration
You can check whether a particular blockhash is still valid by sending a
[`getFeeCalculatorForBlockhash`](developing/clients/jsonrpc-api.md#getfeecalculatorforblockhash)
request with the blockhash as a parameter. If the response value is `null`, the
blockhash is expired, and the withdrawal transaction using that blockhash should
never succeed.
### Validating User-supplied Account Addresses for Withdrawals
As withdrawals are irreversible, it may be a good practice to validate a
user-supplied account address before authorizing a withdrawal in order to
prevent accidental loss of user funds.
#### Basic verfication
Solana addresses a 32-byte array, encoded with the bitcoin base58 alphabet. This
results in an ASCII text string matching the following regular expression:
```
[1-9A-HJ-NP-Za-km-z]{32,44}
```
This check is insufficient on its own as Solana addresses are not checksummed, so
typos cannot be detected. To further validate the user's input, the string can be
decoded and the resulting byte array's length confirmed to be 32. However, there
are some addresses that can decode to 32 bytes despite a typo such as a single
missing character, reversed characters and ignored case
#### Advanced verification
Due to the vulnerability to typos described above, it is recommended that the
balance be queried for candidate withdraw addresses and the user prompted to
confirm their intentions if a non-zero balance is discovered.
#### Valid ed25519 pubkey check
The address of a normal account in Solana is a Base58-encoded string of a
256-bit ed25519 public key. Not all bit patterns are valid public keys for the
ed25519 curve, so it is possible to ensure user-supplied account addresses are
at least correct ed25519 public keys.
#### Java
Here is a Java example of validating a user-supplied address as a valid ed25519
public key:
The following code sample assumes you're using the Maven.
`pom.xml`:
```xml
<repositories>
...
<repository>
<id>spring</id>
<url>https://repo.spring.io/libs-release/</url>
</repository>
</repositories>
...
<dependencies>
...
<dependency>
<groupId>io.github.novacrypto</groupId>
<artifactId>Base58</artifactId>
<version>0.1.3</version>
</dependency>
<dependency>
<groupId>cafe.cryptography</groupId>
<artifactId>curve25519-elisabeth</artifactId>
<version>0.1.0</version>
</dependency>
<dependencies>
```
```java
import io.github.novacrypto.base58.Base58;
import cafe.cryptography.curve25519.CompressedEdwardsY;
public class PubkeyValidator
{
public static boolean verifyPubkey(String userProvidedPubkey)
{
try {
return _verifyPubkeyInternal(userProvidedPubkey);
} catch (Exception e) {
return false;
}
}
public static boolean _verifyPubkeyInternal(String maybePubkey) throws Exception
{
byte[] bytes = Base58.base58Decode(maybePubkey);
return !(new CompressedEdwardsY(bytes)).decompress().isSmallOrder();
}
}
```
## Supporting the SPL Token Standard
[SPL Token](https://spl.solana.com/token) is the standard for wrapped/synthetic
token creation and exchange on the Solana blockchain.
The SPL Token workflow is similar to that of native SOL tokens, but there are a
few differences which will be discussed in this section.
### Token Mints
Each _type_ of SPL Token is declared by creating a _mint_ account. This account
stores metadata describing token features like the supply, number of decimals, and
various authorities with control over the mint. Each SPL Token account references
its associated mint and may only interact with SPL Tokens of that type.
### Installing the `spl-token` CLI Tool
SPL Token accounts are queried and modified using the `spl-token` command line
utility. The examples provided in this section depend upon having it installed
on the local system.
`spl-token` is distributed from Rust [crates.io](https://crates.io/crates/spl-token)
via the Rust `cargo` command line utility. The latest version of `cargo` can be
installed using a handy one-liner for your platform at [rustup.rs](https://rustup.rs).
Once `cargo` is installed, `spl-token` can be obtained with the following command:
```
cargo install spl-token-cli
```
You can then check the installed version to verify
```
spl-token --version
```
Which should result in something like
```text
spl-token-cli 2.0.1
```
### Account Creation
SPL Token accounts carry additional requirements that native System Program
accounts do not:
1. SPL Token accounts must be created before an amount of tokens can be
deposited. Token accounts can be created explicitly with the
`spl-token create-account` command, or implicitly by the
`spl-token transfer --fund-recipient ...` command.
1. SPL Token accounts must remain [rent-exempt](developing/programming-model/accounts.md#rent-exemption)
for the duration of their existence and therefore require a small amount of
native SOL tokens be deposited at account creation. For SPL Token v2 accounts,
this amount is 0.00203928 SOL (2,039,280 lamports).
#### Command Line
To create an SPL Token account with the following properties:
1. Associated with the given mint
1. Owned by the funding account's keypair
```
spl-token create-account <TOKEN_MINT_ADDRESS>
```
#### Example
```
$ spl-token create-account AkUFCWTXb3w9nY2n6SFJvBV6VwvFUCe4KBMCcgLsa2ir
Creating account 6VzWGL51jLebvnDifvcuEDec17sK6Wupi4gYhm5RzfkV
Signature: 4JsqZEPra2eDTHtHpB4FMWSfk3UgcCVmkKkP7zESZeMrKmFFkDkNd91pKP3vPVVZZPiu5XxyJwS73Vi5WsZL88D7
```
Or to create an SPL Token account with a specific keypair:
```
$ solana-keygen new -o token-account.json
$ spl-token create-account AkUFCWTXb3w9nY2n6SFJvBV6VwvFUCe4KBMCcgLsa2ir token-account.json
Creating account 6VzWGL51jLebvnDifvcuEDec17sK6Wupi4gYhm5RzfkV
Signature: 4JsqZEPra2eDTHtHpB4FMWSfk3UgcCVmkKkP7zESZeMrKmFFkDkNd91pKP3vPVVZZPiu5XxyJwS73Vi5WsZL88D7
```
### Checking an Account's Balance
#### Command Line
```
spl-token balance <TOKEN_ACCOUNT_ADDRESS>
```
#### Example
```
$ solana balance 6VzWGL51jLebvnDifvcuEDec17sK6Wupi4gYhm5RzfkV
0
```
### Token Transfers
The source account for a transfer is the actual token account that contains the
amount.
The recipient address however can be a normal wallet account. If an associated
token account for the given mint does not yet exist for that wallet, the
transfer will create it provided that the `--fund-recipient` argument as
provided.
#### Command Line
```
spl-token transfer <SENDER_ACCOUNT_ADDRESS> <AMOUNT> <RECIPIENT_WALLET_ADDRESS> --fund-recipient
```
#### Example
```
$ spl-token transfer 6B199xxzw3PkAm25hGJpjj3Wj3WNYNHzDAnt1tEqg5BN 1 6VzWGL51jLebvnDifvcuEDec17sK6Wupi4gYhm5RzfkV
Transfer 1 tokens
Sender: 6B199xxzw3PkAm25hGJpjj3Wj3WNYNHzDAnt1tEqg5BN
Recipient: 6VzWGL51jLebvnDifvcuEDec17sK6Wupi4gYhm5RzfkV
Signature: 3R6tsog17QM8KfzbcbdP4aoMfwgo6hBggJDVy7dZPVmH2xbCWjEj31JKD53NzMrf25ChFjY7Uv2dfCDq4mGFFyAj
```
### Depositing
Since each `(user, mint)` pair requires a separate account on chain, it is
recommended that an exchange create batches of token accounts in advance and assign them
to users on request. These accounts should all be owned by exchange-controlled
keypairs.
Monitoring for deposit transactions should follow the [block polling](#poll-for-blocks)
method described above. Each new block should be scanned for successful transactions
issuing SPL Token [Transfer](https://github.com/solana-labs/solana-program-library/blob/096d3d4da51a8f63db5160b126ebc56b26346fc8/token/program/src/instruction.rs#L92)
or [Transfer2](https://github.com/solana-labs/solana-program-library/blob/096d3d4da51a8f63db5160b126ebc56b26346fc8/token/program/src/instruction.rs#L252)
instructions referencing user accounts, then querying the
[token account balance](developing/clients/jsonrpc-api.md#gettokenaccountbalance)
updates.
[Considerations](https://github.com/solana-labs/solana/issues/12318) are being
made to exend the `preBalance` and `postBalance` transaction status metadata
fields to include SPL Token balance transfers.
### Withdrawing
The withdrawal address a user provides should be the same address used for
regular SOL withdrawal.
Before executing a withdrawal [transfer](#token-transfers),
the exchange should check the address as
[described above](#validating-user-supplied-account-addresses-for-withdrawals).
From the withdrawal address, the associated token account for the correct mint
determined and the transfer issued to that account. Note that it's possible
that the associated token account does not yet exist, at which point the
exchange should fund the account on behalf of the user. For SPL Token v2
accounts, funding the withdrawal account will require 0.00203928 SOL (2,039,280
lamports).
Template `spl-token transfer` command for a withdrawal:
```
$ spl-token transfer --fund-recipient <exchange token account> <withdrawal amount> <withdrawal address>
```
### Other Considerations
#### Freeze Authority
For regulatory compliance reasons, an SPL Token issuing entity may optionally
choose to hold "Freeze Authority" over all accounts created in association with
its mint. This allows them to [freeze](https://spl.solana.com/token#freezing-accounts)
the assets in a given account at will, rendering the account unusable until thawed.
If this feature is in use, the freeze authority's pubkey will be registered in
the SPL Token's mint account.
## Testing the Integration
Be sure to test your complete workflow on Solana devnet and testnet
[clusters](../clusters.md) before moving to production on mainnet-beta. Devnet
is the most open and flexible, and ideal for initial development, while testnet
offers more realistic cluster configuration. Both devnet and testnet support a faucet,
run `solana airdrop 1` to obtain some devnet or testnet SOL for developement and testing.