aboutsummaryrefslogtreecommitdiffstats
path: root/src/utils/exchange_transfer_simulator.ts
blob: 89b23c8ab7a09429ed17da639b7420220cc92df8 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
import * as _ from 'lodash';
import BigNumber from 'bignumber.js';
import {ExchangeContractErrs, TradeSide, TransferType} from '../types';
import {TokenWrapper} from '../contract_wrappers/token_wrapper';

enum FailureReason {
    Balance = 'balance',
    ProxyAllowance = 'proxyAllowance',
}

const ERR_MSG_MAPPING = {
    [FailureReason.Balance]: {
        [TradeSide.Maker]: {
            [TransferType.Trade]: ExchangeContractErrs.InsufficientMakerBalance,
            [TransferType.Fee]: ExchangeContractErrs.InsufficientMakerFeeBalance,
        },
        [TradeSide.Taker]: {
            [TransferType.Trade]: ExchangeContractErrs.InsufficientTakerBalance,
            [TransferType.Fee]: ExchangeContractErrs.InsufficientTakerFeeBalance,
        },
    },
    [FailureReason.ProxyAllowance]: {
        [TradeSide.Maker]: {
            [TransferType.Trade]: ExchangeContractErrs.InsufficientMakerAllowance,
            [TransferType.Fee]: ExchangeContractErrs.InsufficientMakerFeeAllowance,
        },
        [TradeSide.Taker]: {
            [TransferType.Trade]: ExchangeContractErrs.InsufficientTakerAllowance,
            [TransferType.Fee]: ExchangeContractErrs.InsufficientTakerFeeAllowance,
        },
    },
};

/**
 * Copy on read store for balances/proxyAllowances of tokens/accounts touched in trades
 */
export class BalanceAndProxyAllowanceLazyStore {
    protected _token: TokenWrapper;
    private _balance: {
        [tokenAddress: string]: {
            [userAddress: string]: BigNumber,
        },
    };
    private _proxyAllowance: {
        [tokenAddress: string]: {
            [userAddress: string]: BigNumber,
        },
    };
    constructor(token: TokenWrapper) {
        this._token = token;
        this._balance = {};
        this._proxyAllowance = {};
    }
    protected async getBalanceAsync(tokenAddress: string, userAddress: string): Promise<BigNumber> {
        if (_.isUndefined(this._balance[tokenAddress]) || _.isUndefined(this._balance[tokenAddress][userAddress])) {
            const balance = await this._token.getBalanceAsync(tokenAddress, userAddress);
            this.setBalance(tokenAddress, userAddress, balance);
        }
        const cachedBalance = this._balance[tokenAddress][userAddress];
        return cachedBalance;
    }
    protected setBalance(tokenAddress: string, userAddress: string, balance: BigNumber): void {
        if (_.isUndefined(this._balance[tokenAddress])) {
            this._balance[tokenAddress] = {};
        }
        this._balance[tokenAddress][userAddress] = balance;
    }
    protected async getProxyAllowanceAsync(tokenAddress: string, userAddress: string): Promise<BigNumber> {
        if (_.isUndefined(this._proxyAllowance[tokenAddress]) ||
            _.isUndefined(this._proxyAllowance[tokenAddress][userAddress])) {
            const proxyAllowance = await this._token.getProxyAllowanceAsync(tokenAddress, userAddress);
            this.setProxyAllowance(tokenAddress, userAddress, proxyAllowance);
        }
        const cachedProxyAllowance = this._proxyAllowance[tokenAddress][userAddress];
        return cachedProxyAllowance;
    }
    protected setProxyAllowance(tokenAddress: string, userAddress: string, proxyAllowance: BigNumber): void {
        if (_.isUndefined(this._proxyAllowance[tokenAddress])) {
            this._proxyAllowance[tokenAddress] = {};
        }
        this._proxyAllowance[tokenAddress][userAddress] = proxyAllowance;
    }
}

export class ExchangeTransferSimulator extends BalanceAndProxyAllowanceLazyStore {
    /**
     * Simulates transferFrom call performed by a proxy
     * @param  tokenAddress      Address of the token to be transferred
     * @param  from              Owner of the transferred tokens
     * @param  to                Recipient of the transferred tokens
     * @param  amountInBaseUnits The amount of tokens being transferred
     * @param  tradeSide         Is Maker/Taker transferring
     * @param  transferType      Is it a fee payment or a value transfer
     */
    public async transferFromAsync(tokenAddress: string, from: string, to: string,
                                   amountInBaseUnits: BigNumber, tradeSide: TradeSide,
                                   transferType: TransferType): Promise<void> {
        const balance = await this.getBalanceAsync(tokenAddress, from);
        const proxyAllowance = await this.getProxyAllowanceAsync(tokenAddress, from);
        if (proxyAllowance.lessThan(amountInBaseUnits)) {
            this.throwValidationError(FailureReason.ProxyAllowance, tradeSide, transferType);
        }
        if (balance.lessThan(amountInBaseUnits)) {
            this.throwValidationError(FailureReason.Balance, tradeSide, transferType);
        }
        await this.decreaseProxyAllowanceAsync(tokenAddress, from, amountInBaseUnits);
        await this.decreaseBalanceAsync(tokenAddress, from, amountInBaseUnits);
        await this.increaseBalanceAsync(tokenAddress, to, amountInBaseUnits);
    }
    private async decreaseProxyAllowanceAsync(tokenAddress: string, userAddress: string,
                                              amountInBaseUnits: BigNumber): Promise<void> {
        const proxyAllowance = await this.getProxyAllowanceAsync(tokenAddress, userAddress);
        if (!proxyAllowance.eq(this._token.UNLIMITED_ALLOWANCE_IN_BASE_UNITS)) {
            this.setProxyAllowance(tokenAddress, userAddress, proxyAllowance.minus(amountInBaseUnits));
        }
    }
    private async increaseBalanceAsync(tokenAddress: string, userAddress: string,
                                       amountInBaseUnits: BigNumber): Promise<void> {
        const balance = await this.getBalanceAsync(tokenAddress, userAddress);
        this.setBalance(tokenAddress, userAddress, balance.plus(amountInBaseUnits));
    }
    private async decreaseBalanceAsync(tokenAddress: string, userAddress: string,
                                       amountInBaseUnits: BigNumber): Promise<void> {
        const balance = await this.getBalanceAsync(tokenAddress, userAddress);
        this.setBalance(tokenAddress, userAddress, balance.minus(amountInBaseUnits));
    }
    private throwValidationError(failureReason: FailureReason, tradeSide: TradeSide,
                                 transferType: TransferType): Promise<never> {
        const errMsg = ERR_MSG_MAPPING[failureReason][tradeSide][transferType];
        throw new Error(errMsg);
    }
}