-
Notifications
You must be signed in to change notification settings - Fork 24
User deposit address sweeping to internal exchange addresses
Sweeps are transfers from user deposit addresses to cold storage or hot wallet. They allow you to safely credit users, after moving funds from their deposit addresses. Sweeps can be sent periodically after scanning addresses for balance.
Sweeps are crucial to securely credit users as it is only secure to hold funds in unspent addresses.
In case a user deposits to an already swept address, an adversary might try to forge a signature and move the funds out of this address if the exchange has already swept from it before. Therefore, funds should be swept to your cold storage or hot wallet periodically.
Crediting should only be performed after sweep transactions confirm, and the funds are in your possession.
You should absolutely inform the user of the risk of total loss should he send to a deposit address provided by the exchange more than once.
In this example we define the addresses and related metadata used to create sweep transfers and credit the users.
First step is to determine a set of deposit addresses, fetch their balances with iota.api.getBalances()
and filter out any empty addresses.
const addresses = [
'ZRMBFCFLJSEIHYSXGDMOJOCORBWSSGJDJMVZGWUYGXTJYUJQELKOXNWOYBYTMYEIHP9PPPKWDEVOYJPZH',
'TXPETAIGWJGETC9Y9FKKLHIBZHO9KQESOHZBKEQBEAKJJKDQCSD9CRPKSZIXIGTPMLBVYXLHNNDMZFTSH'
]
// Address metadata
// hashes represent any unresolved sweeps
const metadata = [
{security: 2, keyIndex: 1, user: 'Chris', hashes: []},
{security: 3, keyIndex: 2, user: 'Dom', hashes: ['']}
]
iota.api.getBalances(addresses, 100, (err, res) => {
if (err) {
// handle error
console.log(err)
return
}
// Parse balances to int
balances = res.balances.map(balance => parseInt(balance))
// Construct sweep objects
const sweeps = metadata.map((data, i) => {
address: addresses[i],
security: metadata[i].security,
keyIndex: metadata[i].keyIndex,
hashes: metadata[i].hashes,
balance: balances[i]
})
// Do not sweep deposit addresses with 0 balance
sweeps.filter(sweep => sweep.balance > 0)
// Check for on-going sweeps related to each deposit address
// [...]
})
To dermine if previous sweeps of any deposit address are on-going we use isReattachable()
on sweep tail transaction hashes.
// Extract first tail hash out of each addresses
const hashes = []
sweeps.forEach(sweep => hashes.push(sweep.hashes[0]))
// Check for on-going sweeps related to each address
iota.api.isReattachable({hashes: hashes}, (err, res) => {
if (err) {
// handle error
console.log(err)
return
}
// Do not sweep addresses that were used as inputs in pending sweeps
sweeps = sweeps.filter(sweep => {
const index = hashes.indexOf(hash => hash === sweep.hashes[0])
if (index === -1) {
return true
}
return !res[index];
})
// Construct inputs
const inputs = sweeps.map(sweep => {
address: sweep.address,
security: sweep.security,
keyIndex: sweep.keyIndex,
balance: sweep.balance
})
// Send the sweep transfer
// [...]
})
Warning: If any addresses have been previously swept, do not sweep them again, unless previous sweep transfer has been confirmed.
Then use sendTransfer()
and provide the selected addresses as inputs to move their balance to an unspent exchange owned address. In this example we aggregate inputs assigned to different users in a single transaction, improving performance by reducing amount of Proof-of-Work.
// Define an unused, exchange owned address as output of the sweep
// Make sure that no other transfer will use this address as input
const destinationAddress = 'KTLACMXXOJYIIEYJJYSUXXOJUPSVNLQHZSSLWSBOSPALSDAZQNPZBPNMCFZDVS9RGJRKQHBDEZZGGWUQE'
// Define options by passing inputs and remainder address
const options = {
inputs: inputs,
// Remainder address must always be an unused, exchange owned address when sending sweep transfers.
// In this example we simply use above `destinationAddress`
address: destinationAddress
}
// Sum up all swept balances
const totalValue = sweeps.reduce((sum, sweep) => sum + sweep.balance, 0)
// Define transfers using the destination address and the sum of input balances as the value
const transfers = [{
'address': destinationAddress,
'value': totalValue,
'message': '',
'tag': ''
}]
// Send transfer and obtain the tail hash
iota.api.sendTransfer(seed, 4, 15, transfers, options, (err, transactions) => {
if (err) {
// handle error
console.log(err)
return
}
// Extract the tail transaction hash from the response
const tailHash = transactions[0].hash
console.log(tailHash)
// Save tail hash to be used for properly whitelisting addresses to be swept and for checking confirmation of the sweep
inputs.forEach(input => {
const index = metadata.indexOf(data => data.address === input.address)
// Update latest sweep transaction tail hash
metadata[index].hashes[0] = tailHash
})
metadata.push(tailHash)
})
Notice: While aggregating sweeps improves performance, it is advised to keep bundle size limited to 10 transactions on average for having faster confirmations. This is achieved by aggregating 4 inputs in a bundle on average.
Warning: All input addresses passed to a signle
sendTransfer()
call must be generated from the same seed. You might want to cache addresses belonging to a seed together with their key index because address generation is computationally expensive.
Warning: Make sure to provide the correct security level and key index for each input.
Warning: Wait for sweep transfers to confirm before spending their outputs.
Warning: Always define an unspent exchange owned remainder address. Ideally, the remainder address should be same as the output address.
Last step to ensure secure deposit of funds to exchange addresses is to check the inclusion states of the sweep transactions. Make use of the getLatestInclusion()
and pass the tail transaction hashes. The result determines which transactions are confirmed and accepted by the network.
// Extract tail hashes, obtained by `sendTrasnfer()`, from metadata
const hashes = metadata.map(data => data.hashes)
.reduce((hashes, hash) => hashes.concat(hash), [])
// Check inclusion states
iota.api.getLatestInclusion(hashes, (err, states) => {
if (err) {
// handle error
console.log(err)
return
}
states.forEach((confirmed, i) => {
if (confirmed) {
console.log('Transaction with tail hash:', hashes[i], 'confirmed')
// credit he user
// [...]
}
})
})
Notice: You might need to reattach sweeps that remain unconfirmed after a certain time frame. Check Transaction reattachments guide for detailed instructions.
Warning: Proceed to crediting only for transactions that have been confirmed.
Sweeps from addresses that belong to different users could be aggregated and sent with a single call to sendTransfer()
for improved performance, as shown before, but should be credited carefully. To credit the user fetch the bundle of the sweep transfer with getBundle()
by passing the tail transaction hash. Then examine each transaction in the bundle for negative value and credit based on value and address.
iota.api.getLatestInclusion(hashes, (err, states) => {
if (err) {
// handle error
console.log(err)
return
}
states.forEach((confirmed, i) => {
if (confirmed) {
// Get bundle by tail hash
iota.api.getBundle(hashes[i], (err, bundle) => {
if (err) {
// handle error
console.log(err)
return
}
// Check address and value for each transaction with negative value to credit the user
bundle
.filter(tx => tx.value < 0)
.forEach(tx => {
// get index of transaction address in addresses array
const j = addresses.findIndex(address => address === tx.address)
if (j !== -1) {
// Find user that deposited to the specific swept address
const user = metadata[j].user
// Get the absolute value of the sweep
const value = Math.abs(tx.value)
// credit `user` with `value`
console.log("credit " + user + " with " + value)
}
})
})
}
})
})
Notice: We only need to check the tail transaction hash with
getLatestInclusion()
. Bundles are atomic sets of transactions, as such transactions in the bundle are either all confirmed or unconfirmed.
Warning: Other approaches on crediting of aggregated sweeps may result to wrong credits; for example basing credit on
getBalances()
prior to sweep, has a time-of-check time-of-use issue, If a user has (successfully) deposited while building the sweep.