-
Notifications
You must be signed in to change notification settings - Fork 3.8k
Tutorial Exchange Deposit Withdraw
This document is targeted toward exchanges that wish to automate deposit and withdrawal of standard-conforming EOSIO token contracts. The blockchain's native token conforms to the standard.
This tutorial uses the cleos
command line tool to query a local nodeos
server
that should be connected to an eosio blockchain. nodeos
will need to be configured
with the following plugins:
- eosio::wallet_api_plugin
- eosio::history_api_plugin
- eosio::chain_api_plugin
By default, the history plugin will log the history of all accounts, but this is not the recommended configuration, as it will consume tens of gigabytes of RAM in the medium term. For a more optimized memory footprint, you should configure the history plugin to only log activity relevant to your account(s). This can be achieved with the following config param placed in your config.ini or passed on the command line.
$ nodeos --filter_on_accounts youraccount
If you have already synced the blockchain without the history plugin, then you may need to replay the blockchain to pickup any historical activity.
$ nodeos --replay --filter_on_accounts youraccount
You only need to replay once. Subsequent runs of nodeos
should not use the replay flag, as
your startup times will be unnecessarily long.
When designing this tutorial, we assume that an exchange will poll nodeos
for incoming
transactions and will want to know when a transfer is considered irreversible or final.
With eosio-based chains, finality of a transaction occurs once 2/3 + 1 of block producers have
either directly or indirectly confirmed the block. This could take from less than a second to
a couple of minutes, but either way nodeos
will keep you posted on the status.
./cleos get currency balance eosio.token scott EOS
900.0000 EOS
We will now deposit some funds to exchange:
./cleos transfer scott exchange "1.0000 EOS"
executed transaction: 5ec797175dd24612acd8fc5a8685fa44caa8646cec0a87b12568db22a3df02fb 256 bytes 8k cycles
# eosio.token <= eosio.token::transfer {"from":"scott","to":"exchange","quantity":"1.0000 EOS","memo":""}
>> transfer
# scott <= eosio.token::transfer {"from":"scott","to":"exchange","quantity":"1.0000 EOS","memo":""}
# exchange <= eosio.token::transfer {"from":"scott","to":"exchange","quantity":"1.0000 EOS","memo":""}
warning: transaction executed locally, but may not be confirmed by the network yet
This output indicates that the action eosio.token::transfer
was delivered to three accounts/contracts, ("eosio.token",
"scott", and "exchange"). The eosio token standard requires that both the sender and receiver account/contract be
notified of all transfer actions so that those accounts can run custom logic. At this time, neither scott nor exchange
has any contract set, but the transaction log still notes that they were notified.
The account history consists of all actions that were either authorized by the account or received by the account. Since the
exchange received the eosio.token::transfer
action, it is listed in the history. If you are using the console, confirmed and
irreversible transactions are printed in "green" while unconfirmed transactions are printed in "yellow". Without color, you
can tell whether a transaction is confirmed or not by the first character: '#' for irreversible and '?' for potentially reversible.
./cleos get actions exchange
# seq when contract::action => receiver trx id... args
================================================================================================================
# 0 2018-04-29T01:09:45.000 eosio.token::transfer => exchange 5ec79717... {"from":"scott","to":"exchange","quantity":"1.0000 EOS","mem...
Do a few more transfers:
./cleos get actions exchange
# seq when contract::action => receiver trx id... args
================================================================================================================
# 0 2018-04-29T01:09:45.000 eosio.token::transfer => exchange 5ec79717... {"from":"scott","to":"exchange","quantity":"1.0000 EOS","mem...
# 1 2018-04-29T01:16:25.000 eosio.token::transfer => exchange 2269828c... {"from":"scott","to":"exchange","quantity":"1.0000 EOS","mem...
? 2 2018-04-29T01:19:54.000 eosio.token::transfer => exchange 213f3797... {"from":"scott","to":"exchange","quantity":"1.0000 EOS","mem...
The last transfer is still pending, waiting on irreversibility.
The "seq" column represents the index of actions for your specific account. It will always increment as new relevant actions are added.
The cleos get actions
command allows you some control over which actions are fetched. You can view the help for this command with -h
./cleos get actions -h
Usage: ./cleos get actions [OPTIONS] account_name [pos] [offset]
Positionals:
account_name TEXT name of account to query on
pos INT sequence number of action for this account, -1 for last
offset INT get actions [pos,pos+offset] for positive offset or [pos-offset,pos) for negative offset
Options:
-j,--json print full json
--full don't truncate action json
--pretty pretty print full action json
--console print console output generated by action
To get only the last action you would do the following:
./cleos get actions exchange -1 -1
# seq when contract::action => receiver trx id... args
================================================================================================================
# 2 2018-04-29T01:19:54.000 eosio.token::transfer => exchange 213f3797... {"from":"scott","to":"exchange","quantity":"1.0000 EOS","mem...
This command says to go to the last sequence number (indicated by pos = -1) and then fetch "1" item prior to it (offset = -1). Using the example above, this will return a sequence in the range [3-1,3) or [2,3) which is only row 2. In this case, "-1" position means "one past the last sequence" and operates like an end iterator from C++ containers.
Since we presume your exchange is running a polling micro-service, it will want to fetch the "next unprocessed deposit". In this case the microservice will need to track the seq number of the "last processed seq". For the sake of this example, we will assume that "seq 0" has been processed and that we want to fetch "seq 1" if any.
We pass pos=1 and offset=0 to get the range [1,1+0] or [1,1].
./cleos get actions exchange 1 0
# seq when contract::action => receiver trx id... args
================================================================================================================
# 1 2018-04-29T01:16:25.000 eosio.token::transfer => exchange 2269828c... {"from":"scott","to":"exchange","quantity":"1.0000 EOS","mem...
We can call this in a loop, processing each confirmed action (those starting with #) until we either run out of items or we find an unconfirmed action (starting with ?).
./cleos get actions exchange 3 0
# seq when contract::action => receiver trx id... args
================================================================================================================
So far this tutorial has focused on using cleos
to fetch and display the history. However, cleos
is merely a light-weight
wrapper around a JSON-RPC interface. cleos
can dump the raw json returned from the JSON-RPC request, or you can make
your own JSON-RPC request.
Here is the JSON returned when querying sequence 2.
./cleos get actions exchange 2 0 -j
{
"actions": [{
"global_action_seq": 32856,
"account_action_seq": 2,
"block_num": 32759,
"block_time": "2018-04-29T01:19:54.000",
"action_trace": {
"receipt": {
"receiver": "exchange",
"act_digest": "00686ff415fe97951a942889dbaed2b880043e3ae6ac2d5579318bbb2d30060f",
"global_sequence": 32856,
"recv_sequence": 3,
"auth_sequence": [[
"scott",
43
]
]
},
"act": {
"account": "eosio.token",
"name": "transfer",
"authorization": [{
"actor": "scott",
"permission": "active"
}
],
"data": {
"from": "scott",
"to": "exchange",
"quantity": "1.0000 EOS",
"memo": ""
},
"hex_data": "00000000809c29c20000008a4dd35057102700000000000004454f530000000000"
},
"elapsed": 52,
"cpu_usage": 1000,
"console": "",
"total_inline_cpu_usage": 1000,
"trx_id": "213f37972498cbae5abf6bcb5aec82e09967df7f04cf90f67b7d63a6bb871d58",
"inline_traces": []
}
}
],
"last_irreversible_block": 35062
}
Given this JSON, an action is irreversible (final) if "block_num" < "last_irreversible_block"
.
You can identify irreversible deposits by the following:
actions[0].action_trace.act.account == "eosio.token" &&
actions[0].action_trace.act.name == "transfer" &&
actions[0].action_trace.act.data.quantity == "X.0000 EOS" &&
actions[0].action_trace.to == "exchange" &&
actions[0].action_trace.memo == "KEY TO IDENTIFY INTERNAL ACCOUNT" &&
actions[0].action_trace.receipt.receiver == "exchange" &&
actions[0].block_num < last_irreversible_block
In practice you should give your customers a "memo" that identifies which of your internal accounts you should credit with the deposit.
It is critical that you validate all of the conditions above, including the token symbol name. Users can create other contracts with "transfer" actions that "notify" your account. If you do not validate all of the above properties, then you may process "false deposits".
actions[0].action_trace.act.account == "eosio.token" &&
actions[0].action_trace.receipt.receiver == "exchange"
Now that we have received three deposits, we should see that the exchange has a balance of 3.0000 EOS.
./cleos get currency balance eosio.token exchange EOS
3.0000 EOS
(Note: while generating this tutorial, scott deposited another 1.0000 EOS (seq 3) for a total exchange balance of 4.0000 EOS.)
When a user requests a withdrawal from your exchange, they will need to provide you with their eosio account name and
the amount to be withdrawn. You can then run the cleos
command to perform the withdrawal. cleos
will interact with
the "unlocked" wallet running on nodeos
, which should only enable localhost connections. More advanced usage will have
a separate key-server (keosd
), but that will be covered later.
Let's assume scott wants to withdraw 1.0000 EOS
:
./cleos transfer exchange scott "1.0000 EOS"
executed transaction: 93e785202e7502bb1383ad10e786cc20f7dd738d3fd3da38712b3fb38fb9af26 256 bytes 8k cycles
# eosio.token <= eosio.token::transfer {"from":"exchange","to":"scott","quantity":"1.0000 EOS","memo":""}
>> transfer
# exchange <= eosio.token::transfer {"from":"exchange","to":"scott","quantity":"1.0000 EOS","memo":""}
# scott <= eosio.token::transfer {"from":"exchange","to":"scott","quantity":"1.0000 EOS","memo":""}
warning: transaction executed locally, but may not be confirmed by the network yet
At this stage your local nodeos
client accepted the transaction and likely broadcast it to the broader network.
We can get the history and see that there are "3" new actions listed, all with transaction id 93e78520...
, as reported
by our transfer
command. Because exchange authorized the transaction, it is informed of all accounts that processed
and accepted the transfer
. In this case the eosio.token
contract processed the transfer and updated balances, the
sender (exchange) processed it, as did the receiver (scott). All three contracts/accounts approved it and/or performed
state transitions based upon the action.
./cleos get actions exchange -1 -8
# seq when contract::action => receiver trx id... args
================================================================================================================
# 0 2018-04-29T01:09:45.000 eosio.token::transfer => exchange 5ec79717... {"from":"scott","to":"exchange","quantity":"1.0000 EOS","mem...
# 1 2018-04-29T01:16:25.000 eosio.token::transfer => exchange 2269828c... {"from":"scott","to":"exchange","quantity":"1.0000 EOS","mem...
# 2 2018-04-29T01:19:54.000 eosio.token::transfer => exchange 213f3797... {"from":"scott","to":"exchange","quantity":"1.0000 EOS","mem...
# 3 2018-04-29T01:53:57.000 eosio.token::transfer => exchange 8b7766ac... {"from":"scott","to":"exchange","quantity":"1.0000 EOS","mem...
# 4 2018-04-29T01:54:17.500 eosio.token::transfer => eosio.token 93e78520... {"from":"exchange","to":"scott","quantity":"1.0000 EOS","mem...
# 5 2018-04-29T01:54:17.500 eosio.token::transfer => exchange 93e78520... {"from":"exchange","to":"scott","quantity":"1.0000 EOS","mem...
# 6 2018-04-29T01:54:17.500 eosio.token::transfer => scott 93e78520... {"from":"exchange","to":"scott","quantity":"1.0000 EOS","mem...
By processing the history, we can be informed when our transaction was confirmed. In practice, we expect you will maintain private database state to track the withdrawal process. It might be useful to embed an exchange-specific memo in the withdraw request in order to map to your private database state. Another approach is to simply use the transaction ID for your mapping. When your account history microservice comes across seq 5 and sees it is irreversible, it can then mark your withdrawal as complete.
Sometimes network issues will cause a transaction to fail and never be included in a block. Your internal database will need to know when this has happened so that it can inform the user and/or try again. If you do not get an immediate error when you submit your local transfer, then you must wait for the transaction to expire. Every transaction has an "expiration", after which the transaction can never be applied. Once the last irreversible block has moved past the expiration time, you can safely mark your attempted withdrawal as failed and not worry about it "floating around the ether" to be applied when you least expect.
By default, cleos
sets an expiration window of 2 minutes. This is long enough to allow all 21 producers an opportunity to include the transaction.
./cleos transfer exchange scott "1.0000 EOS" -j -d
{
"expiration": "2018-04-29T01:58:12",
"ref_block_num": 37282,
"ref_block_prefix": 351570603,
"max_net_usage_words": 0,
"max_kcpu_usage": 0,
"delay_sec": 0,
"context_free_actions": [],
...
Your microservice can query the last irreversible block number and the head block time using cleos
.
./cleos get info
{
"server_version": "0812f84d",
"head_block_num": 39313,
"last_irreversible_block_num": 39298,
"last_irreversible_block_id": "000099823bfc4f0b936d8e48c70fc3f1619eb8d21989d160a9fe23655f1f5c79",
"head_block_id": "000099912473a7a3699ad682f731d1874ebddcf4b60eff79f8e6e4216077278d",
"head_block_time": "2018-04-29T02:14:31",
"head_block_producer": "producer2"
}
This tutorial shows the minimal viable deposit/withdraw handlers and assumes a single wallet which contains all keys necessary to authorize deposits and withdrawals. A security-focused exchange would take the following additional steps:
- keep vast majority of funds in a time-delayed, multi-sig controlled account
- use multi-sig on the hot wallet with several independent processes/servers double-checking all withdrawals
- deploy a custom contract that only allows withdrawals to KYC'd accounts and require multi-sig to white-list accounts
- deploy a custom contract that only accepts deposits of known tokens from KYC'd accounts
- deploy a custom contract that enforces a mandatory 24-hour waiting period for all withdrawals
- utilize hardware wallets for all signing, even automated withdrawal
Customers want immediate withdrawals, but they also want the exchange to be protected. The blockchain-enforced 24-hour period lets the customer know the money is "on the way" while also informing potential-hackers that the exchange has 24 hours to respond to unauthorized access. Furthermore, if the exchange emails/text messages users upon start of withdrawal, users have 24 hours to contact the exchange and fix any unauthorized access to their individual account.
Information on how to utilize these more advanced techniques will be available in a future document.