refactored and added fees
This commit is contained in:
@@ -8,6 +8,7 @@ import {
|
|||||||
SwapQuoteGetOutputOpts,
|
SwapQuoteGetOutputOpts,
|
||||||
SwapQuoteRequestOpts,
|
SwapQuoteRequestOpts,
|
||||||
SwapQuoterOpts,
|
SwapQuoterOpts,
|
||||||
|
ExtensionContractType,
|
||||||
} from './types';
|
} from './types';
|
||||||
|
|
||||||
const ETH_GAS_STATION_API_BASE_URL = 'https://ethgasstation.info';
|
const ETH_GAS_STATION_API_BASE_URL = 'https://ethgasstation.info';
|
||||||
@@ -35,13 +36,17 @@ const DEFAULT_SWAP_QUOTER_OPTS: SwapQuoterOpts = {
|
|||||||
...DEFAULT_ORDER_PRUNER_OPTS,
|
...DEFAULT_ORDER_PRUNER_OPTS,
|
||||||
};
|
};
|
||||||
|
|
||||||
const DEFAULT_FORWARDER_SWAP_QUOTE_GET_OPTS: SwapQuoteGetOutputOpts & ForwarderExtensionContractOpts = {
|
const DEFAULT_FORWARDER_EXTENSION_CONTRACT_OPTS: ForwarderExtensionContractOpts = {
|
||||||
feePercentage: 0,
|
feePercentage: 0,
|
||||||
feeRecipient: NULL_ADDRESS,
|
feeRecipient: NULL_ADDRESS,
|
||||||
};
|
};
|
||||||
|
|
||||||
const DEFAULT_FORWARDER_SWAP_QUOTE_EXECUTE_OPTS: SwapQuoteExecutionOpts &
|
const DEFAULT_FORWARDER_SWAP_QUOTE_GET_OPTS: SwapQuoteGetOutputOpts = {
|
||||||
ForwarderExtensionContractOpts = DEFAULT_FORWARDER_SWAP_QUOTE_GET_OPTS;
|
useExtensionContract: ExtensionContractType.Forwarder,
|
||||||
|
extensionContractOpts: DEFAULT_FORWARDER_EXTENSION_CONTRACT_OPTS,
|
||||||
|
};
|
||||||
|
|
||||||
|
const DEFAULT_FORWARDER_SWAP_QUOTE_EXECUTE_OPTS: SwapQuoteExecutionOpts = DEFAULT_FORWARDER_SWAP_QUOTE_GET_OPTS;
|
||||||
|
|
||||||
const DEFAULT_SWAP_QUOTE_REQUEST_OPTS: SwapQuoteRequestOpts = {
|
const DEFAULT_SWAP_QUOTE_REQUEST_OPTS: SwapQuoteRequestOpts = {
|
||||||
slippagePercentage: 0.2, // 20% slippage protection,
|
slippagePercentage: 0.2, // 20% slippage protection,
|
||||||
|
|||||||
@@ -37,7 +37,6 @@ export {
|
|||||||
SwapQuoteConsumerOpts,
|
SwapQuoteConsumerOpts,
|
||||||
CalldataInfo,
|
CalldataInfo,
|
||||||
ExtensionContractType,
|
ExtensionContractType,
|
||||||
SwapQuoteConsumingOpts,
|
|
||||||
LiquidityForTakerMakerAssetDataPair,
|
LiquidityForTakerMakerAssetDataPair,
|
||||||
SwapQuoteGetOutputOpts,
|
SwapQuoteGetOutputOpts,
|
||||||
PrunedSignedOrder,
|
PrunedSignedOrder,
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ export class ForwarderSwapQuoteConsumer implements SwapQuoteConsumerBase<Forward
|
|||||||
*/
|
*/
|
||||||
public async getCalldataOrThrowAsync(
|
public async getCalldataOrThrowAsync(
|
||||||
quote: SwapQuote,
|
quote: SwapQuote,
|
||||||
opts: Partial<SwapQuoteGetOutputOpts & ForwarderExtensionContractOpts> = {},
|
opts: Partial<SwapQuoteGetOutputOpts> = {},
|
||||||
): Promise<CalldataInfo> {
|
): Promise<CalldataInfo> {
|
||||||
assert.isValidForwarderSwapQuote('quote', quote, await this._getEtherTokenAssetDataOrThrowAsync());
|
assert.isValidForwarderSwapQuote('quote', quote, await this._getEtherTokenAssetDataOrThrowAsync());
|
||||||
|
|
||||||
@@ -86,21 +86,19 @@ export class ForwarderSwapQuoteConsumer implements SwapQuoteConsumerBase<Forward
|
|||||||
*/
|
*/
|
||||||
public async getSmartContractParamsOrThrowAsync(
|
public async getSmartContractParamsOrThrowAsync(
|
||||||
quote: SwapQuote,
|
quote: SwapQuote,
|
||||||
opts: Partial<SwapQuoteGetOutputOpts & ForwarderExtensionContractOpts> = {},
|
opts: Partial<SwapQuoteGetOutputOpts> = {},
|
||||||
): Promise<SmartContractParamsInfo<ForwarderSmartContractParams>> {
|
): Promise<SmartContractParamsInfo<ForwarderSmartContractParams>> {
|
||||||
assert.isValidForwarderSwapQuote('quote', quote, await this._getEtherTokenAssetDataOrThrowAsync());
|
assert.isValidForwarderSwapQuote('quote', quote, await this._getEtherTokenAssetDataOrThrowAsync());
|
||||||
|
|
||||||
const { ethAmount: providedEthAmount, feeRecipient, feePercentage } = _.merge(
|
const { extensionContractOpts } = _.merge(
|
||||||
{},
|
{},
|
||||||
constants.DEFAULT_FORWARDER_SWAP_QUOTE_GET_OPTS,
|
constants.DEFAULT_FORWARDER_SWAP_QUOTE_GET_OPTS,
|
||||||
opts,
|
opts,
|
||||||
);
|
);
|
||||||
|
|
||||||
assert.isValidPercentage('feePercentage', feePercentage);
|
assert.isValidForwarderExtensionContractOpts('extensionContractOpts', extensionContractOpts);
|
||||||
assert.isETHAddressHex('feeRecipient', feeRecipient);
|
|
||||||
if (providedEthAmount !== undefined) {
|
const { feeRecipient, feePercentage } = extensionContractOpts;
|
||||||
assert.isBigNumber('ethAmount', providedEthAmount);
|
|
||||||
}
|
|
||||||
|
|
||||||
const { orders, worstCaseQuoteInfo } = quote;
|
const { orders, worstCaseQuoteInfo } = quote;
|
||||||
|
|
||||||
@@ -146,7 +144,7 @@ export class ForwarderSwapQuoteConsumer implements SwapQuoteConsumerBase<Forward
|
|||||||
return {
|
return {
|
||||||
params,
|
params,
|
||||||
toAddress: this._forwarder.address,
|
toAddress: this._forwarder.address,
|
||||||
ethAmount: providedEthAmount || ethAmountWithFees,
|
ethAmount: ethAmountWithFees,
|
||||||
methodAbi,
|
methodAbi,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -158,18 +156,20 @@ export class ForwarderSwapQuoteConsumer implements SwapQuoteConsumerBase<Forward
|
|||||||
*/
|
*/
|
||||||
public async executeSwapQuoteOrThrowAsync(
|
public async executeSwapQuoteOrThrowAsync(
|
||||||
quote: SwapQuote,
|
quote: SwapQuote,
|
||||||
opts: Partial<SwapQuoteExecutionOpts & ForwarderExtensionContractOpts>,
|
opts: Partial<SwapQuoteExecutionOpts>,
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
assert.isValidForwarderSwapQuote('quote', quote, await this._getEtherTokenAssetDataOrThrowAsync());
|
assert.isValidForwarderSwapQuote('quote', quote, await this._getEtherTokenAssetDataOrThrowAsync());
|
||||||
|
|
||||||
const { ethAmount: providedEthAmount, takerAddress, gasLimit, gasPrice, feeRecipient, feePercentage } = _.merge(
|
const { ethAmount: providedEthAmount, takerAddress, gasLimit, gasPrice, extensionContractOpts } = _.merge(
|
||||||
{},
|
{},
|
||||||
constants.DEFAULT_FORWARDER_SWAP_QUOTE_EXECUTE_OPTS,
|
constants.DEFAULT_FORWARDER_SWAP_QUOTE_EXECUTE_OPTS,
|
||||||
opts,
|
opts,
|
||||||
);
|
);
|
||||||
|
|
||||||
assert.isValidPercentage('feePercentage', feePercentage);
|
assert.isValidForwarderExtensionContractOpts('extensionContractOpts', extensionContractOpts);
|
||||||
assert.isETHAddressHex('feeRecipient', feeRecipient);
|
|
||||||
|
const { feeRecipient, feePercentage } = extensionContractOpts;
|
||||||
|
|
||||||
if (providedEthAmount !== undefined) {
|
if (providedEthAmount !== undefined) {
|
||||||
assert.isBigNumber('ethAmount', providedEthAmount);
|
assert.isBigNumber('ethAmount', providedEthAmount);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ import {
|
|||||||
SwapQuote,
|
SwapQuote,
|
||||||
SwapQuoteConsumerBase,
|
SwapQuoteConsumerBase,
|
||||||
SwapQuoteConsumerOpts,
|
SwapQuoteConsumerOpts,
|
||||||
SwapQuoteConsumingOpts,
|
|
||||||
SwapQuoteExecutionOpts,
|
SwapQuoteExecutionOpts,
|
||||||
SwapQuoteGetOutputOpts,
|
SwapQuoteGetOutputOpts,
|
||||||
} from '../types';
|
} from '../types';
|
||||||
@@ -57,7 +56,7 @@ export class SwapQuoteConsumer implements SwapQuoteConsumerBase<SmartContractPar
|
|||||||
*/
|
*/
|
||||||
public async getCalldataOrThrowAsync(
|
public async getCalldataOrThrowAsync(
|
||||||
quote: SwapQuote,
|
quote: SwapQuote,
|
||||||
opts: Partial<SwapQuoteGetOutputOpts & SwapQuoteConsumingOpts> = {},
|
opts: Partial<SwapQuoteGetOutputOpts> = {},
|
||||||
): Promise<CalldataInfo> {
|
): Promise<CalldataInfo> {
|
||||||
assert.isValidSwapQuote('quote', quote);
|
assert.isValidSwapQuote('quote', quote);
|
||||||
const consumer = await this._getConsumerForSwapQuoteAsync(opts);
|
const consumer = await this._getConsumerForSwapQuoteAsync(opts);
|
||||||
@@ -71,7 +70,7 @@ export class SwapQuoteConsumer implements SwapQuoteConsumerBase<SmartContractPar
|
|||||||
*/
|
*/
|
||||||
public async getSmartContractParamsOrThrowAsync(
|
public async getSmartContractParamsOrThrowAsync(
|
||||||
quote: SwapQuote,
|
quote: SwapQuote,
|
||||||
opts: Partial<SwapQuoteGetOutputOpts & SwapQuoteConsumingOpts> = {},
|
opts: Partial<SwapQuoteGetOutputOpts> = {},
|
||||||
): Promise<SmartContractParamsInfo<SmartContractParams>> {
|
): Promise<SmartContractParamsInfo<SmartContractParams>> {
|
||||||
assert.isValidSwapQuote('quote', quote);
|
assert.isValidSwapQuote('quote', quote);
|
||||||
const consumer = await this._getConsumerForSwapQuoteAsync(opts);
|
const consumer = await this._getConsumerForSwapQuoteAsync(opts);
|
||||||
@@ -85,7 +84,7 @@ export class SwapQuoteConsumer implements SwapQuoteConsumerBase<SmartContractPar
|
|||||||
*/
|
*/
|
||||||
public async executeSwapQuoteOrThrowAsync(
|
public async executeSwapQuoteOrThrowAsync(
|
||||||
quote: SwapQuote,
|
quote: SwapQuote,
|
||||||
opts: Partial<SwapQuoteExecutionOpts & SwapQuoteConsumingOpts> = {},
|
opts: Partial<SwapQuoteExecutionOpts> = {},
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
assert.isValidSwapQuote('quote', quote);
|
assert.isValidSwapQuote('quote', quote);
|
||||||
const consumer = await this._getConsumerForSwapQuoteAsync(opts);
|
const consumer = await this._getConsumerForSwapQuoteAsync(opts);
|
||||||
@@ -110,7 +109,7 @@ export class SwapQuoteConsumer implements SwapQuoteConsumerBase<SmartContractPar
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async _getConsumerForSwapQuoteAsync(
|
private async _getConsumerForSwapQuoteAsync(
|
||||||
opts: Partial<SwapQuoteConsumingOpts>,
|
opts: Partial<SwapQuoteGetOutputOpts>,
|
||||||
): Promise<SwapQuoteConsumerBase<SmartContractParams>> {
|
): Promise<SwapQuoteConsumerBase<SmartContractParams>> {
|
||||||
if (opts.useExtensionContract === ExtensionContractType.Forwarder) {
|
if (opts.useExtensionContract === ExtensionContractType.Forwarder) {
|
||||||
return this._forwarderConsumer;
|
return this._forwarderConsumer;
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { SignedOrder } from '@0x/types';
|
import { SignedOrder } from '@0x/types';
|
||||||
import { BigNumber } from '@0x/utils';
|
import { BigNumber } from '@0x/utils';
|
||||||
import { MethodAbi } from 'ethereum-types';
|
import { MethodAbi } from 'ethereum-types';
|
||||||
|
import { ForwarderSwapQuoteConsumer } from '@0x/asset-swapper/src/quote_consumers/forwarder_swap_quote_consumer';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* expiryBufferMs: The number of seconds to add when calculating whether an order is expired or not. Defaults to 300s (5m).
|
* expiryBufferMs: The number of seconds to add when calculating whether an order is expired or not. Defaults to 300s (5m).
|
||||||
@@ -167,7 +168,10 @@ export interface SwapQuoteConsumerOpts {
|
|||||||
/**
|
/**
|
||||||
* Represents the options provided to a generic SwapQuoteConsumer
|
* Represents the options provided to a generic SwapQuoteConsumer
|
||||||
*/
|
*/
|
||||||
export interface SwapQuoteGetOutputOpts {}
|
export interface SwapQuoteGetOutputOpts {
|
||||||
|
useExtensionContract: ExtensionContractType;
|
||||||
|
extensionContractOpts?: ForwarderExtensionContractOpts | any;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* takerAddress: The address to perform the buy. Defaults to the first available address from the provider.
|
* takerAddress: The address to perform the buy. Defaults to the first available address from the provider.
|
||||||
@@ -176,10 +180,10 @@ export interface SwapQuoteGetOutputOpts {}
|
|||||||
* ethAmount: The amount of eth sent with the execution of a swap
|
* ethAmount: The amount of eth sent with the execution of a swap
|
||||||
*/
|
*/
|
||||||
export interface SwapQuoteExecutionOpts extends SwapQuoteGetOutputOpts {
|
export interface SwapQuoteExecutionOpts extends SwapQuoteGetOutputOpts {
|
||||||
|
ethAmount?: BigNumber;
|
||||||
|
gasPrice?: BigNumber;
|
||||||
takerAddress?: string;
|
takerAddress?: string;
|
||||||
gasLimit?: number;
|
gasLimit?: number;
|
||||||
gasPrice?: BigNumber;
|
|
||||||
ethAmount?: BigNumber;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -188,18 +192,10 @@ export interface SwapQuoteExecutionOpts extends SwapQuoteGetOutputOpts {
|
|||||||
* feeRecipient: address of the receiver of the feePercentage of taker asset
|
* feeRecipient: address of the receiver of the feePercentage of taker asset
|
||||||
*/
|
*/
|
||||||
export interface ForwarderExtensionContractOpts {
|
export interface ForwarderExtensionContractOpts {
|
||||||
ethAmount?: BigNumber;
|
|
||||||
feePercentage: number;
|
feePercentage: number;
|
||||||
feeRecipient: string;
|
feeRecipient: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
* Options for how SwapQuoteConsumer will generate output
|
|
||||||
*/
|
|
||||||
export interface SwapQuoteConsumingOpts {
|
|
||||||
useExtensionContract: ExtensionContractType;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type SwapQuote = MarketBuySwapQuote | MarketSellSwapQuote;
|
export type SwapQuote = MarketBuySwapQuote | MarketSellSwapQuote;
|
||||||
|
|
||||||
export interface GetExtensionContractTypeOpts {
|
export interface GetExtensionContractTypeOpts {
|
||||||
|
|||||||
@@ -96,4 +96,8 @@ export const assert = {
|
|||||||
`Expected ${variableName} to be between 0 and 1, but is ${percentage}`,
|
`Expected ${variableName} to be between 0 and 1, but is ${percentage}`,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
isValidForwarderExtensionContractOpts(variableName: string, opts: any): void {
|
||||||
|
assert.isValidPercentage(`${variableName}.feePercentage`, opts.feePercentage);
|
||||||
|
assert.isETHAddressHex(`${variableName}.feeRecipient`, opts.feeRecipient);
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -270,8 +270,10 @@ describe('ForwarderSwapQuoteConsumer', () => {
|
|||||||
takerAddress,
|
takerAddress,
|
||||||
gasPrice: GAS_PRICE,
|
gasPrice: GAS_PRICE,
|
||||||
gasLimit: 4000000,
|
gasLimit: 4000000,
|
||||||
feePercentage: FEE_PERCENTAGE,
|
extensionContractOpts: {
|
||||||
feeRecipient,
|
feePercentage: 0.05,
|
||||||
|
feeRecipient,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
await expectMakerAndTakerBalancesAsync(
|
await expectMakerAndTakerBalancesAsync(
|
||||||
constants.ZERO_AMOUNT,
|
constants.ZERO_AMOUNT,
|
||||||
@@ -294,10 +296,11 @@ describe('ForwarderSwapQuoteConsumer', () => {
|
|||||||
const feeRecipientEthBalanceBefore = await web3Wrapper.getBalanceInWeiAsync(feeRecipient);
|
const feeRecipientEthBalanceBefore = await web3Wrapper.getBalanceInWeiAsync(feeRecipient);
|
||||||
await swapQuoteConsumer.executeSwapQuoteOrThrowAsync(marketSellSwapQuote, {
|
await swapQuoteConsumer.executeSwapQuoteOrThrowAsync(marketSellSwapQuote, {
|
||||||
takerAddress,
|
takerAddress,
|
||||||
feePercentage: FEE_PERCENTAGE,
|
|
||||||
feeRecipient,
|
|
||||||
gasPrice: GAS_PRICE,
|
|
||||||
gasLimit: 4000000,
|
gasLimit: 4000000,
|
||||||
|
extensionContractOpts: {
|
||||||
|
feePercentage: 0.05,
|
||||||
|
feeRecipient,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
await expectMakerAndTakerBalancesAsync(
|
await expectMakerAndTakerBalancesAsync(
|
||||||
constants.ZERO_AMOUNT,
|
constants.ZERO_AMOUNT,
|
||||||
@@ -370,8 +373,10 @@ describe('ForwarderSwapQuoteConsumer', () => {
|
|||||||
const { toAddress, params } = await swapQuoteConsumer.getSmartContractParamsOrThrowAsync(
|
const { toAddress, params } = await swapQuoteConsumer.getSmartContractParamsOrThrowAsync(
|
||||||
marketSellSwapQuote,
|
marketSellSwapQuote,
|
||||||
{
|
{
|
||||||
feePercentage: 0.05,
|
extensionContractOpts: {
|
||||||
feeRecipient,
|
feePercentage: 0.05,
|
||||||
|
feeRecipient,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
expect(toAddress).to.deep.equal(forwarderContract.address);
|
expect(toAddress).to.deep.equal(forwarderContract.address);
|
||||||
@@ -391,8 +396,10 @@ describe('ForwarderSwapQuoteConsumer', () => {
|
|||||||
const { toAddress, params } = await swapQuoteConsumer.getSmartContractParamsOrThrowAsync(
|
const { toAddress, params } = await swapQuoteConsumer.getSmartContractParamsOrThrowAsync(
|
||||||
marketBuySwapQuote,
|
marketBuySwapQuote,
|
||||||
{
|
{
|
||||||
feePercentage: 0.05,
|
extensionContractOpts: {
|
||||||
feeRecipient,
|
feePercentage: 0.05,
|
||||||
|
feeRecipient,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
expect(toAddress).to.deep.equal(forwarderContract.address);
|
expect(toAddress).to.deep.equal(forwarderContract.address);
|
||||||
@@ -480,8 +487,10 @@ describe('ForwarderSwapQuoteConsumer', () => {
|
|||||||
const { calldataHexString, toAddress, ethAmount } = await swapQuoteConsumer.getCalldataOrThrowAsync(
|
const { calldataHexString, toAddress, ethAmount } = await swapQuoteConsumer.getCalldataOrThrowAsync(
|
||||||
marketSellSwapQuote,
|
marketSellSwapQuote,
|
||||||
{
|
{
|
||||||
feePercentage: FEE_PERCENTAGE,
|
extensionContractOpts: {
|
||||||
feeRecipient,
|
feePercentage: 0.05,
|
||||||
|
feeRecipient,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
expect(toAddress).to.deep.equal(contractAddresses.forwarder);
|
expect(toAddress).to.deep.equal(contractAddresses.forwarder);
|
||||||
@@ -514,8 +523,10 @@ describe('ForwarderSwapQuoteConsumer', () => {
|
|||||||
const { calldataHexString, toAddress, ethAmount } = await swapQuoteConsumer.getCalldataOrThrowAsync(
|
const { calldataHexString, toAddress, ethAmount } = await swapQuoteConsumer.getCalldataOrThrowAsync(
|
||||||
marketBuySwapQuote,
|
marketBuySwapQuote,
|
||||||
{
|
{
|
||||||
feePercentage: FEE_PERCENTAGE,
|
extensionContractOpts: {
|
||||||
feeRecipient,
|
feePercentage: 0.05,
|
||||||
|
feeRecipient,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
expect(toAddress).to.deep.equal(contractAddresses.forwarder);
|
expect(toAddress).to.deep.equal(contractAddresses.forwarder);
|
||||||
|
|||||||
@@ -64,7 +64,6 @@ export class BuyButton extends React.PureComponent<BuyButtonProps> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.props.onValidationPending(swapQuote);
|
this.props.onValidationPending(swapQuote);
|
||||||
// TODO(dave4506)
|
|
||||||
const ethNeededForBuy = affiliateFeeUtils.getTotalEthAmountWithAffiliateFee(swapQuote.worstCaseQuoteInfo, 0);
|
const ethNeededForBuy = affiliateFeeUtils.getTotalEthAmountWithAffiliateFee(swapQuote.worstCaseQuoteInfo, 0);
|
||||||
// if we don't have a balance for the user, let the transaction through, it will be handled by the wallet
|
// if we don't have a balance for the user, let the transaction through, it will be handled by the wallet
|
||||||
const hasSufficientEth = accountEthBalanceInWei === undefined || accountEthBalanceInWei.gte(ethNeededForBuy);
|
const hasSufficientEth = accountEthBalanceInWei === undefined || accountEthBalanceInWei.gte(ethNeededForBuy);
|
||||||
@@ -76,11 +75,15 @@ export class BuyButton extends React.PureComponent<BuyButtonProps> {
|
|||||||
let txHash: string | undefined;
|
let txHash: string | undefined;
|
||||||
const gasInfo = await gasPriceEstimator.getGasInfoAsync();
|
const gasInfo = await gasPriceEstimator.getGasInfoAsync();
|
||||||
const feeRecipient = oc(affiliateInfo).feeRecipient();
|
const feeRecipient = oc(affiliateInfo).feeRecipient();
|
||||||
|
const feePercentage = oc(affiliateInfo).feeRecipient();
|
||||||
try {
|
try {
|
||||||
analytics.trackBuyStarted(swapQuote);
|
analytics.trackBuyStarted(swapQuote);
|
||||||
// TODO(dave4506)
|
|
||||||
txHash = await swapQuoteConsumer.executeSwapQuoteOrThrowAsync(swapQuote, {
|
txHash = await swapQuoteConsumer.executeSwapQuoteOrThrowAsync(swapQuote, {
|
||||||
useExtensionContract: ExtensionContractType.Forwarder,
|
useExtensionContract: ExtensionContractType.Forwarder,
|
||||||
|
extensionContractOpts: {
|
||||||
|
feeRecipient,
|
||||||
|
feePercentage,
|
||||||
|
},
|
||||||
takerAddress: accountAddress,
|
takerAddress: accountAddress,
|
||||||
gasPrice: gasInfo.gasPriceInWei,
|
gasPrice: gasInfo.gasPriceInWei,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ export interface BuyOrderStateButtonProps {
|
|||||||
accountAddress?: string;
|
accountAddress?: string;
|
||||||
accountEthBalanceInWei?: BigNumber;
|
accountEthBalanceInWei?: BigNumber;
|
||||||
swapQuote?: MarketBuySwapQuote;
|
swapQuote?: MarketBuySwapQuote;
|
||||||
buyOrderProcessingState: OrderProcessState;
|
swapOrderProcessingState: OrderProcessState;
|
||||||
swapQuoter: SwapQuoter;
|
swapQuoter: SwapQuoter;
|
||||||
swapQuoteConsumer: SwapQuoteConsumer;
|
swapQuoteConsumer: SwapQuoteConsumer;
|
||||||
web3Wrapper: Web3Wrapper;
|
web3Wrapper: Web3Wrapper;
|
||||||
@@ -34,7 +34,7 @@ export interface BuyOrderStateButtonProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const BuyOrderStateButtons: React.StatelessComponent<BuyOrderStateButtonProps> = props => {
|
export const BuyOrderStateButtons: React.StatelessComponent<BuyOrderStateButtonProps> = props => {
|
||||||
if (props.buyOrderProcessingState === OrderProcessState.Failure) {
|
if (props.swapOrderProcessingState === OrderProcessState.Failure) {
|
||||||
return (
|
return (
|
||||||
<Flex justify="space-between">
|
<Flex justify="space-between">
|
||||||
<Button width="48%" onClick={props.onRetry} fontColor={ColorOption.white}>
|
<Button width="48%" onClick={props.onRetry} fontColor={ColorOption.white}>
|
||||||
@@ -46,11 +46,11 @@ export const BuyOrderStateButtons: React.StatelessComponent<BuyOrderStateButtonP
|
|||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
} else if (
|
} else if (
|
||||||
props.buyOrderProcessingState === OrderProcessState.Success ||
|
props.swapOrderProcessingState === OrderProcessState.Success ||
|
||||||
props.buyOrderProcessingState === OrderProcessState.Processing
|
props.swapOrderProcessingState === OrderProcessState.Processing
|
||||||
) {
|
) {
|
||||||
return <SecondaryButton onClick={props.onViewTransaction}>View Transaction</SecondaryButton>;
|
return <SecondaryButton onClick={props.onViewTransaction}>View Transaction</SecondaryButton>;
|
||||||
} else if (props.buyOrderProcessingState === OrderProcessState.Validating) {
|
} else if (props.swapOrderProcessingState === OrderProcessState.Validating) {
|
||||||
return <PlacingOrderButton />;
|
return <PlacingOrderButton />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { SwapQuoter, SwapQuoterError, MarketBuySwapQuote, SwapQuoteConsumer, SwapQuote } from '@0x/asset-swapper';
|
import { MarketBuySwapQuote, SwapQuoteConsumer, SwapQuoteConsumerError, SwapQuoter, SwapQuoterError } from '@0x/asset-swapper';
|
||||||
import { BigNumber } from '@0x/utils';
|
import { BigNumber } from '@0x/utils';
|
||||||
import { Web3Wrapper } from '@0x/web3-wrapper';
|
import { Web3Wrapper } from '@0x/web3-wrapper';
|
||||||
import * as _ from 'lodash';
|
import * as _ from 'lodash';
|
||||||
@@ -34,7 +34,7 @@ interface ConnectedDispatch {
|
|||||||
onBuySuccess: (swapQuote: MarketBuySwapQuote, txHash: string) => void;
|
onBuySuccess: (swapQuote: MarketBuySwapQuote, txHash: string) => void;
|
||||||
onBuyFailure: (swapQuote: MarketBuySwapQuote, txHash: string) => void;
|
onBuyFailure: (swapQuote: MarketBuySwapQuote, txHash: string) => void;
|
||||||
onRetry: () => void;
|
onRetry: () => void;
|
||||||
onValidationFail: (swapQuote: MarketBuySwapQuote, errorMessage: SwapQuoterError | ZeroExInstantError) => void;
|
onValidationFail: (swapQuote: MarketBuySwapQuote, errorMessage: SwapQuoteConsumerError | ZeroExInstantError) => void;
|
||||||
}
|
}
|
||||||
export interface SelectedAssetBuyOrderStateButtons {}
|
export interface SelectedAssetBuyOrderStateButtons {}
|
||||||
const mapStateToProps = (state: State, _ownProps: SelectedAssetBuyOrderStateButtons): ConnectedState => {
|
const mapStateToProps = (state: State, _ownProps: SelectedAssetBuyOrderStateButtons): ConnectedState => {
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import { ERC20AssetAmountInput, ERC20AssetAmountInputProps } from '../components
|
|||||||
import { Action, actions } from '../redux/actions';
|
import { Action, actions } from '../redux/actions';
|
||||||
import { State } from '../redux/reducer';
|
import { State } from '../redux/reducer';
|
||||||
import { ColorOption } from '../style/theme';
|
import { ColorOption } from '../style/theme';
|
||||||
import { AffiliateInfo, ERC20Asset, Omit, OrderProcessState, QuoteFetchOrigin } from '../types';
|
import { ERC20Asset, Omit, OrderProcessState, QuoteFetchOrigin } from '../types';
|
||||||
import { swapQuoteUpdater } from '../util/swap_quote_updater';
|
import { swapQuoteUpdater } from '../util/swap_quote_updater';
|
||||||
|
|
||||||
export interface SelectedERC20AssetAmountInputProps {
|
export interface SelectedERC20AssetAmountInputProps {
|
||||||
@@ -25,7 +25,6 @@ interface ConnectedState {
|
|||||||
asset?: ERC20Asset;
|
asset?: ERC20Asset;
|
||||||
isInputDisabled: boolean;
|
isInputDisabled: boolean;
|
||||||
numberOfAssetsAvailable?: number;
|
numberOfAssetsAvailable?: number;
|
||||||
affiliateInfo?: AffiliateInfo;
|
|
||||||
canSelectOtherAsset: boolean;
|
canSelectOtherAsset: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -34,7 +33,6 @@ interface ConnectedDispatch {
|
|||||||
swapQuoter: SwapQuoter,
|
swapQuoter: SwapQuoter,
|
||||||
value?: BigNumber,
|
value?: BigNumber,
|
||||||
asset?: ERC20Asset,
|
asset?: ERC20Asset,
|
||||||
affiliateInfo?: AffiliateInfo,
|
|
||||||
) => void;
|
) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,7 +61,6 @@ const mapStateToProps = (state: State, _ownProps: SelectedERC20AssetAmountInputP
|
|||||||
asset: selectedAsset,
|
asset: selectedAsset,
|
||||||
isInputDisabled,
|
isInputDisabled,
|
||||||
numberOfAssetsAvailable,
|
numberOfAssetsAvailable,
|
||||||
affiliateInfo: state.affiliateInfo,
|
|
||||||
canSelectOtherAsset,
|
canSelectOtherAsset,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -76,7 +73,7 @@ const mapDispatchToProps = (
|
|||||||
dispatch: Dispatch<Action>,
|
dispatch: Dispatch<Action>,
|
||||||
_ownProps: SelectedERC20AssetAmountInputProps,
|
_ownProps: SelectedERC20AssetAmountInputProps,
|
||||||
): ConnectedDispatch => ({
|
): ConnectedDispatch => ({
|
||||||
updateSwapQuote: (swapQuoter, value, asset, affiliateInfo) => {
|
updateSwapQuote: (swapQuoter, value, asset) => {
|
||||||
// Update the input
|
// Update the input
|
||||||
dispatch(actions.updateSelectedAssetAmount(value));
|
dispatch(actions.updateSelectedAssetAmount(value));
|
||||||
// invalidate the last swap quote.
|
// invalidate the last swap quote.
|
||||||
@@ -91,7 +88,6 @@ const mapDispatchToProps = (
|
|||||||
debouncedUpdateSwapQuoteAsync(swapQuoter, dispatch, asset, value, QuoteFetchOrigin.Manual, {
|
debouncedUpdateSwapQuoteAsync(swapQuoter, dispatch, asset, value, QuoteFetchOrigin.Manual, {
|
||||||
setPending: true,
|
setPending: true,
|
||||||
dispatchErrors: true,
|
dispatchErrors: true,
|
||||||
affiliateInfo,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -107,7 +103,7 @@ const mergeProps = (
|
|||||||
asset: connectedState.asset,
|
asset: connectedState.asset,
|
||||||
value: connectedState.value,
|
value: connectedState.value,
|
||||||
onChange: (value, asset) => {
|
onChange: (value, asset) => {
|
||||||
connectedDispatch.updateSwapQuote(connectedState.swapQuoter, value, asset, connectedState.affiliateInfo);
|
connectedDispatch.updateSwapQuote(connectedState.swapQuoter, value, asset);
|
||||||
},
|
},
|
||||||
isInputDisabled: connectedState.isInputDisabled,
|
isInputDisabled: connectedState.isInputDisabled,
|
||||||
numberOfAssetsAvailable: connectedState.numberOfAssetsAvailable,
|
numberOfAssetsAvailable: connectedState.numberOfAssetsAvailable,
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ export const unrender = () => {
|
|||||||
const renderInstant = (config: ZeroExInstantConfig, selector: string) => {
|
const renderInstant = (config: ZeroExInstantConfig, selector: string) => {
|
||||||
const appendToIfExists = document.querySelector(selector);
|
const appendToIfExists = document.querySelector(selector);
|
||||||
assert.assert(appendToIfExists !== null, `Could not find div with selector: ${selector}`);
|
assert.assert(appendToIfExists !== null, `Could not find div with selector: ${selector}`);
|
||||||
parentElement = appendToIfExists;
|
parentElement = appendToIfExists as Element;
|
||||||
injectedDiv = document.createElement('div');
|
injectedDiv = document.createElement('div');
|
||||||
injectedDiv.setAttribute('id', INJECTED_DIV_ID);
|
injectedDiv.setAttribute('id', INJECTED_DIV_ID);
|
||||||
injectedDiv.setAttribute('class', INJECTED_DIV_CLASS);
|
injectedDiv.setAttribute('class', INJECTED_DIV_CLASS);
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ export const asyncData = {
|
|||||||
fetchOrigin: QuoteFetchOrigin,
|
fetchOrigin: QuoteFetchOrigin,
|
||||||
options: { updateSilently: boolean },
|
options: { updateSilently: boolean },
|
||||||
) => {
|
) => {
|
||||||
const { swapOrderState, providerState, selectedAsset, selectedAssetUnitAmount, affiliateInfo } = state;
|
const { swapOrderState, providerState, selectedAsset, selectedAssetUnitAmount } = state;
|
||||||
const swapQuoter = providerState.swapQuoter;
|
const swapQuoter = providerState.swapQuoter;
|
||||||
if (
|
if (
|
||||||
selectedAssetUnitAmount !== undefined &&
|
selectedAssetUnitAmount !== undefined &&
|
||||||
@@ -111,7 +111,6 @@ export const asyncData = {
|
|||||||
{
|
{
|
||||||
setPending: !options.updateSilently,
|
setPending: !options.updateSilently,
|
||||||
dispatchErrors: !options.updateSilently,
|
dispatchErrors: !options.updateSilently,
|
||||||
affiliateInfo,
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { SwapQuote, SwapQuoter, MarketBuySwapQuote } from '@0x/asset-swapper';
|
import { MarketBuySwapQuote, SwapQuote, SwapQuoter } from '@0x/asset-swapper';
|
||||||
import { AssetProxyId } from '@0x/types';
|
import { AssetProxyId } from '@0x/types';
|
||||||
import { BigNumber } from '@0x/utils';
|
import { BigNumber } from '@0x/utils';
|
||||||
import { Web3Wrapper } from '@0x/web3-wrapper';
|
import { Web3Wrapper } from '@0x/web3-wrapper';
|
||||||
@@ -8,7 +8,7 @@ import { oc } from 'ts-optchain';
|
|||||||
|
|
||||||
import { ERC20_SWAP_QUOTE_SLIPPAGE_PERCENTAGE, ERC721_SWAP_QUOTE_SLIPPAGE_PERCENTAGE } from '../constants';
|
import { ERC20_SWAP_QUOTE_SLIPPAGE_PERCENTAGE, ERC721_SWAP_QUOTE_SLIPPAGE_PERCENTAGE } from '../constants';
|
||||||
import { Action, actions } from '../redux/actions';
|
import { Action, actions } from '../redux/actions';
|
||||||
import { AffiliateInfo, Asset, QuoteFetchOrigin } from '../types';
|
import { Asset, QuoteFetchOrigin } from '../types';
|
||||||
import { analytics } from './analytics';
|
import { analytics } from './analytics';
|
||||||
import { assetUtils } from './asset';
|
import { assetUtils } from './asset';
|
||||||
import { errorFlasher } from './error_flasher';
|
import { errorFlasher } from './error_flasher';
|
||||||
@@ -24,7 +24,6 @@ export const swapQuoteUpdater = {
|
|||||||
options: {
|
options: {
|
||||||
setPending: boolean;
|
setPending: boolean;
|
||||||
dispatchErrors: boolean;
|
dispatchErrors: boolean;
|
||||||
affiliateInfo?: AffiliateInfo;
|
|
||||||
},
|
},
|
||||||
): Promise<void> => {
|
): Promise<void> => {
|
||||||
// get a new swap quote.
|
// get a new swap quote.
|
||||||
@@ -37,8 +36,7 @@ export const swapQuoteUpdater = {
|
|||||||
dispatch(actions.setQuoteRequestStatePending());
|
dispatch(actions.setQuoteRequestStatePending());
|
||||||
}
|
}
|
||||||
// TODO(dave4506) expose wethAssetData + feePercentage utils
|
// TODO(dave4506) expose wethAssetData + feePercentage utils
|
||||||
const wethAssetData = '';
|
const wethAssetData = await swapQuoter.getEtherTokenAssetDataOrThrowAsync();
|
||||||
const feePercentage = oc(options.affiliateInfo).feePercentage();
|
|
||||||
let newSwapQuote: MarketBuySwapQuote | undefined;
|
let newSwapQuote: MarketBuySwapQuote | undefined;
|
||||||
const slippagePercentage =
|
const slippagePercentage =
|
||||||
asset.metaData.assetProxyId === AssetProxyId.ERC20
|
asset.metaData.assetProxyId === AssetProxyId.ERC20
|
||||||
|
|||||||
@@ -150,7 +150,16 @@ const generateConfig = (dischargeTarget, heapConfigOptions, rollbarConfigOptions
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
test: /\.svg$/,
|
test: /\.svg$/,
|
||||||
loader: 'svg-react-loader',
|
use: [
|
||||||
|
{
|
||||||
|
loader: 'react-svg-loader',
|
||||||
|
options: {
|
||||||
|
svgo: {
|
||||||
|
plugins: [{ removeViewBox: false }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
test: /\.js$/,
|
test: /\.js$/,
|
||||||
|
|||||||
39
yarn.lock
39
yarn.lock
@@ -661,6 +661,23 @@
|
|||||||
lodash "^4.17.11"
|
lodash "^4.17.11"
|
||||||
valid-url "^1.0.9"
|
valid-url "^1.0.9"
|
||||||
|
|
||||||
|
"@0x/asset-buyer@6.1.8":
|
||||||
|
version "6.1.8"
|
||||||
|
resolved "https://registry.yarnpkg.com/@0x/asset-buyer/-/asset-buyer-6.1.8.tgz#71f6abb366e89e62457c256644edb37e12113e94"
|
||||||
|
dependencies:
|
||||||
|
"@0x/assert" "^2.1.0"
|
||||||
|
"@0x/connect" "^5.0.13"
|
||||||
|
"@0x/contract-wrappers" "^9.1.7"
|
||||||
|
"@0x/json-schemas" "^3.0.11"
|
||||||
|
"@0x/order-utils" "^8.2.2"
|
||||||
|
"@0x/subproviders" "^4.1.1"
|
||||||
|
"@0x/types" "^2.4.0"
|
||||||
|
"@0x/typescript-typings" "^4.2.3"
|
||||||
|
"@0x/utils" "^4.4.0"
|
||||||
|
"@0x/web3-wrapper" "^6.0.7"
|
||||||
|
ethereum-types "^2.1.3"
|
||||||
|
lodash "^4.17.11"
|
||||||
|
|
||||||
"@0x/base-contract@^5.4.0":
|
"@0x/base-contract@^5.4.0":
|
||||||
version "5.4.0"
|
version "5.4.0"
|
||||||
resolved "https://registry.npmjs.org/@0x/base-contract/-/base-contract-5.4.0.tgz#466ea98af22d7e629a21a7d16211c825664e2a48"
|
resolved "https://registry.npmjs.org/@0x/base-contract/-/base-contract-5.4.0.tgz#466ea98af22d7e629a21a7d16211c825664e2a48"
|
||||||
@@ -680,6 +697,22 @@
|
|||||||
lodash "^4.17.11"
|
lodash "^4.17.11"
|
||||||
uuid "^3.3.2"
|
uuid "^3.3.2"
|
||||||
|
|
||||||
|
"@0x/connect@^5.0.13":
|
||||||
|
version "5.0.19"
|
||||||
|
resolved "https://registry.npmjs.org/@0x/connect/-/connect-5.0.19.tgz#569679af661ef84a4c34958388e1be7f1c250a04"
|
||||||
|
dependencies:
|
||||||
|
"@0x/assert" "^2.1.6"
|
||||||
|
"@0x/json-schemas" "^4.0.2"
|
||||||
|
"@0x/order-utils" "^8.4.0"
|
||||||
|
"@0x/types" "^2.4.3"
|
||||||
|
"@0x/typescript-typings" "^4.3.0"
|
||||||
|
"@0x/utils" "^4.5.2"
|
||||||
|
lodash "^4.17.11"
|
||||||
|
query-string "^6.0.0"
|
||||||
|
sinon "^4.0.0"
|
||||||
|
uuid "^3.3.2"
|
||||||
|
websocket "^1.0.26"
|
||||||
|
|
||||||
"@0x/contract-addresses@^3.0.1", "@0x/contract-addresses@^3.0.2", "@0x/contract-addresses@^3.2.0":
|
"@0x/contract-addresses@^3.0.1", "@0x/contract-addresses@^3.0.2", "@0x/contract-addresses@^3.2.0":
|
||||||
version "3.2.0"
|
version "3.2.0"
|
||||||
resolved "https://registry.npmjs.org/@0x/contract-addresses/-/contract-addresses-3.2.0.tgz#606307696d9622764220a34e9d4638b899093eec"
|
resolved "https://registry.npmjs.org/@0x/contract-addresses/-/contract-addresses-3.2.0.tgz#606307696d9622764220a34e9d4638b899093eec"
|
||||||
@@ -690,7 +723,7 @@
|
|||||||
version "2.2.2"
|
version "2.2.2"
|
||||||
resolved "https://registry.npmjs.org/@0x/contract-artifacts/-/contract-artifacts-2.2.2.tgz#e6d771afb58d0b59c19c5364af5a42a3dfd17219"
|
resolved "https://registry.npmjs.org/@0x/contract-artifacts/-/contract-artifacts-2.2.2.tgz#e6d771afb58d0b59c19c5364af5a42a3dfd17219"
|
||||||
|
|
||||||
"@0x/contract-wrappers@^9.1.6":
|
"@0x/contract-wrappers@^9.1.6", "@0x/contract-wrappers@^9.1.7":
|
||||||
version "9.1.8"
|
version "9.1.8"
|
||||||
resolved "https://registry.yarnpkg.com/@0x/contract-wrappers/-/contract-wrappers-9.1.8.tgz#5923d35af3e4b442a57d02f74e02620b2d5b1356"
|
resolved "https://registry.yarnpkg.com/@0x/contract-wrappers/-/contract-wrappers-9.1.8.tgz#5923d35af3e4b442a57d02f74e02620b2d5b1356"
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -774,7 +807,7 @@
|
|||||||
uuid "^3.3.2"
|
uuid "^3.3.2"
|
||||||
websocket "^1.0.29"
|
websocket "^1.0.29"
|
||||||
|
|
||||||
"@0x/order-utils@^8.2.1", "@0x/order-utils@^8.2.3", "@0x/order-utils@^8.2.4", "@0x/order-utils@^8.4.0":
|
"@0x/order-utils@^8.2.1", "@0x/order-utils@^8.2.2", "@0x/order-utils@^8.2.3", "@0x/order-utils@^8.2.4", "@0x/order-utils@^8.4.0":
|
||||||
version "8.4.0"
|
version "8.4.0"
|
||||||
resolved "https://registry.npmjs.org/@0x/order-utils/-/order-utils-8.4.0.tgz#f7fe9c73f9fd82ab05ec3c04951049e904aab46a"
|
resolved "https://registry.npmjs.org/@0x/order-utils/-/order-utils-8.4.0.tgz#f7fe9c73f9fd82ab05ec3c04951049e904aab46a"
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -6717,7 +6750,7 @@ ethereum-common@^0.0.18:
|
|||||||
version "0.0.18"
|
version "0.0.18"
|
||||||
resolved "https://registry.yarnpkg.com/ethereum-common/-/ethereum-common-0.0.18.tgz#2fdc3576f232903358976eb39da783213ff9523f"
|
resolved "https://registry.yarnpkg.com/ethereum-common/-/ethereum-common-0.0.18.tgz#2fdc3576f232903358976eb39da783213ff9523f"
|
||||||
|
|
||||||
ethereum-types@^2.1.4, ethereum-types@^2.1.6:
|
ethereum-types@^2.1.3, ethereum-types@^2.1.4, ethereum-types@^2.1.6:
|
||||||
version "2.1.6"
|
version "2.1.6"
|
||||||
resolved "https://registry.npmjs.org/ethereum-types/-/ethereum-types-2.1.6.tgz#57d9d515fad86ab987c0f6962c4203be37da8579"
|
resolved "https://registry.npmjs.org/ethereum-types/-/ethereum-types-2.1.6.tgz#57d9d515fad86ab987c0f6962c4203be37da8579"
|
||||||
dependencies:
|
dependencies:
|
||||||
|
|||||||
Reference in New Issue
Block a user