diff --git a/src/constants.ts b/src/constants.ts index eab527eb8..3bfbbcb4f 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -1,3 +1,4 @@ +import BigNumber from "bignumber.js"; import { Network } from "./types"; export const DEFAULT_GAS_INCREASE_FACTOR = 1.01; @@ -40,6 +41,7 @@ export const RPC_URL_PATH = "jsonrpc/v1/"; export const MAINNET_PROVIDER_URL = `${API_BASE_MAINNET}/${RPC_URL_PATH}`; export const ORDERBOOK_PATH = `/wyvern/v${ORDERBOOK_VERSION}`; export const API_PATH = `/api/v${ORDERBOOK_VERSION}`; +export const MAX_UINT_256 = new BigNumber(2).pow(256).minus(1); export const EIP_712_ORDER_TYPES = { EIP712Domain: [ diff --git a/src/sdk.ts b/src/sdk.ts index a06fe7fe3..f0a5fe962 100644 --- a/src/sdk.ts +++ b/src/sdk.ts @@ -8,6 +8,7 @@ import { CreateInputItem, OrderComponents, } from "@opensea/seaport-js/lib/types"; +import { generateRandomSalt } from "@opensea/seaport-js/lib/utils/order"; import { BigNumber } from "bignumber.js"; import { Web3JsProvider } from "ethereum-types"; import { isValidAddress } from "ethereumjs-util"; @@ -43,6 +44,7 @@ import { WRAPPED_NFT_LIQUIDATION_PROXY_ADDRESS_MAINNET, WRAPPED_NFT_LIQUIDATION_PROXY_ADDRESS_RINKEBY, OPENSEA_LEGACY_FEE_RECIPIENT, + MAX_UINT_256, } from "./constants"; import { CanonicalWETH, @@ -137,6 +139,7 @@ import { getAddressAfterRemappingSharedStorefrontAddressToLazyMintAdapterAddress, feesToBasisPoints, isValidProtocol, + toBaseUnitAmount, } from "./utils/utils"; export { WyvernProtocol }; @@ -186,7 +189,6 @@ export class OpenSeaSDK { // API config apiConfig.networkName = apiConfig.networkName || Network.Main; this.api = new OpenSeaAPI(apiConfig); - this._wyvernConfigOverride = apiConfig.wyvernConfig; this._networkName = apiConfig.networkName; @@ -572,7 +574,7 @@ export class OpenSeaSDK { ) as unknown as UniswapExchangeAbi; // Convert desired WNFT to wei - const amount = WyvernProtocol.toBaseUnitAmount( + const amount = toBaseUnitAmount( makeBigNumber(numTokens), Number(wrappedNFT.methods.decimals().call()) ); @@ -610,10 +612,7 @@ export class OpenSeaSDK { }) { const token = getCanonicalWrappedEther(this._networkName); - const amount = WyvernProtocol.toBaseUnitAmount( - makeBigNumber(amountInEth), - token.decimals - ); + const amount = toBaseUnitAmount(makeBigNumber(amountInEth), token.decimals); this._dispatch(EventType.WrapEth, { accountAddress, amount }); @@ -649,10 +648,7 @@ export class OpenSeaSDK { }) { const token = getCanonicalWrappedEther(this._networkName); - const amount = WyvernProtocol.toBaseUnitAmount( - makeBigNumber(amountInEth), - token.decimals - ); + const amount = toBaseUnitAmount(makeBigNumber(amountInEth), token.decimals); this._dispatch(EventType.UnwrapWeth, { accountAddress, amount }); @@ -1264,90 +1260,6 @@ export class OpenSeaSDK { ); } - /** - * Cancel an order on-chain, preventing it from ever being fulfilled. - * @param param0 __namedParameters Object - * @param order The order to cancel - * @param accountAddress The order maker's wallet address - */ - public async cancelOrderLegacyWyvern({ - order, - accountAddress, - }: { - order: Order; - accountAddress: string; - }) { - this._dispatch(EventType.CancelOrder, { order, accountAddress }); - - const transactionHash = await this._wyvernProtocol.wyvernExchange - .cancelOrder_( - [ - order.exchange, - order.maker, - order.taker, - order.feeRecipient, - order.target, - order.staticTarget, - order.paymentToken, - ], - [ - order.makerRelayerFee, - order.takerRelayerFee, - order.makerProtocolFee, - order.takerProtocolFee, - order.basePrice, - order.extra, - order.listingTime, - order.expirationTime, - order.salt, - ], - order.feeMethod, - order.side, - order.saleKind, - order.howToCall, - order.calldata, - order.replacementPattern, - order.staticExtradata, - order.v || 0, - order.r || NULL_BLOCK_HASH, - order.s || NULL_BLOCK_HASH - ) - .sendTransactionAsync({ from: accountAddress }); - - await this._confirmTransaction( - transactionHash, - EventType.CancelOrder, - "Cancelling order", - async () => { - const isOpen = await this._validateOrder(order); - return !isOpen; - } - ); - } - - /** - * Cancel all existing orders with a lower nonce on-chain, preventing them from ever being fulfilled. - * @param param0 __namedParameters Object - * @param accountAddress The order maker's wallet address - */ - public async bulkCancelExistingOrdersLegacyWyvern({ - accountAddress, - }: { - accountAddress: string; - }) { - this._dispatch(EventType.BulkCancelExistingOrders, { accountAddress }); - - const transactionHash = await this._wyvernProtocol.wyvernExchange - .incrementNonce() - .sendTransactionAsync({ from: accountAddress }); - - await this._confirmTransaction( - transactionHash.toString(), - EventType.BulkCancelExistingOrders, - "Bulk cancelling existing orders" - ); - } - /** * Approve a non-fungible token for use in trades. * Requires an account to be initialized first. @@ -1572,7 +1484,7 @@ export class OpenSeaSDK { accountAddress, tokenAddress, proxyAddress, - minimumAmount = WyvernProtocol.MAX_UINT_256, + minimumAmount = MAX_UINT_256, }: { accountAddress: string; tokenAddress: string; @@ -1627,7 +1539,7 @@ export class OpenSeaSDK { getMethod(ERC20, "approve"), // Always approve maximum amount, to prevent the need for followup // transactions (and because old ERC20s like MANA/ENJ are non-compliant) - [proxyAddress, WyvernProtocol.MAX_UINT_256.toString()] + [proxyAddress, MAX_UINT_256.toString()] ), }, (error) => { @@ -1911,10 +1823,7 @@ export class OpenSeaSDK { ): Promise { const schema = this._getSchema(this._getSchemaName(asset)); const quantityBN = quantity - ? WyvernProtocol.toBaseUnitAmount( - makeBigNumber(quantity), - asset.decimals || 0 - ) + ? toBaseUnitAmount(makeBigNumber(quantity), asset.decimals || 0) : makeBigNumber(1); const wyAsset = getWyvernAsset(schema, asset, quantityBN); const abi = schema.functions.transfer(wyAsset); @@ -1975,7 +1884,7 @@ export class OpenSeaSDK { quantity?: number | BigNumber; }): Promise { const schema = this._getSchema(this._getSchemaName(asset)); - const quantityBN = WyvernProtocol.toBaseUnitAmount( + const quantityBN = toBaseUnitAmount( makeBigNumber(quantity), asset.decimals || 0 ); @@ -2643,7 +2552,7 @@ export class OpenSeaSDK { extra: makeBigNumber(0), listingTime: times.listingTime, expirationTime: times.expirationTime, - salt: WyvernProtocol.generatePseudoRandomSalt(), + salt: new BigNumber(generateRandomSalt()), metadata: order.metadata, }; @@ -3394,21 +3303,21 @@ export class OpenSeaSDK { ? makeBigNumber( this.web3.utils.toWei(startAmount.toString(), "ether") ).integerValue() - : WyvernProtocol.toBaseUnitAmount(startAmount, token.decimals); + : toBaseUnitAmount(startAmount, token.decimals); const endPrice = endAmount ? isEther ? makeBigNumber( this.web3.utils.toWei(endAmount.toString(), "ether") ).integerValue() - : WyvernProtocol.toBaseUnitAmount(endAmount, token.decimals) + : toBaseUnitAmount(endAmount, token.decimals) : undefined; const extra = isEther ? makeBigNumber( this.web3.utils.toWei(priceDiff.toString(), "ether") ).integerValue() - : WyvernProtocol.toBaseUnitAmount(priceDiff, token.decimals); + : toBaseUnitAmount(priceDiff, token.decimals); const reservePrice = englishAuctionReservePrice ? isEther @@ -3418,10 +3327,7 @@ export class OpenSeaSDK { "ether" ) ).integerValue() - : WyvernProtocol.toBaseUnitAmount( - englishAuctionReservePrice, - token.decimals - ) + : toBaseUnitAmount(englishAuctionReservePrice, token.decimals) : undefined; return { basePrice, extra, paymentToken, reservePrice, endPrice }; diff --git a/src/utils/assert.ts b/src/utils/assert.ts new file mode 100644 index 000000000..c81c84dba --- /dev/null +++ b/src/utils/assert.ts @@ -0,0 +1,103 @@ +/* Sourced from 0x.js */ + +import { assert as sharedAssert } from "@0x/assert"; +// We need those two unused imports because they're actually used by sharedAssert which gets injected here +// eslint-disable-next-line @typescript-eslint/no-unused-vars +import { schemas } from "@0x/json-schemas"; +// eslint-disable-next-line @typescript-eslint/no-unused-vars +import { BigNumber } from "@0x/utils"; +import { Web3Wrapper } from "@0x/web3-wrapper"; +import * as ethUtil from "ethereumjs-util"; +import * as _ from "lodash"; +import { ECSignature } from "../types"; + +/* Sourced from: https://github.com/ProjectOpenSea/wyvern-js/blob/master/src/utils/assert.ts */ +export const assert = { + ...sharedAssert, + isValidSignature( + orderHash: string, + ecSignature: ECSignature, + signerAddress: string + ) { + const isValidSignature = signatureUtils.isValidSignature( + orderHash, + ecSignature, + signerAddress + ); + this.assert( + isValidSignature, + `Expected order with hash '${orderHash}' to have a valid signature` + ); + }, + async isSenderAddressAsync( + variableName: string, + senderAddressHex: string, + web3Wrapper: Web3Wrapper + ): Promise { + sharedAssert.isETHAddressHex(variableName, senderAddressHex); + const isSenderAddressAvailable = + await web3Wrapper.isSenderAddressAvailableAsync(senderAddressHex); + sharedAssert.assert( + isSenderAddressAvailable, + `Specified ${variableName} ${senderAddressHex} isn't available through the supplied web3 provider` + ); + }, + async isUserAddressAvailableAsync(web3Wrapper: Web3Wrapper): Promise { + const availableAddresses = await web3Wrapper.getAvailableAddressesAsync(); + this.assert( + !_.isEmpty(availableAddresses), + "No addresses were available on the provided web3 provider" + ); + }, +}; + +/* Sourced from https://github.com/ProjectOpenSea/wyvern-js/blob/master/src/utils/signature_utils.ts */ +const signatureUtils = { + isValidSignature( + data: string, + signature: ECSignature, + signerAddress: string + ): boolean { + const dataBuff = ethUtil.toBuffer(data); + // const msgHashBuff = ethUtil.hashPersonalMessage(dataBuff); + const msgHashBuff = dataBuff; + try { + const pubKey = ethUtil.ecrecover( + msgHashBuff, + signature.v, + ethUtil.toBuffer(signature.r), + ethUtil.toBuffer(signature.s) + ); + const retrievedAddress = ethUtil.bufferToHex( + ethUtil.pubToAddress(pubKey) + ); + return retrievedAddress === signerAddress; + } catch (err) { + return false; + } + }, + parseSignatureHexAsVRS(signatureHex: string): ECSignature { + const signatureBuffer = ethUtil.toBuffer(signatureHex); + let v = +ethUtil.bufferToHex(signatureBuffer.slice(0, 1)); + if (v < 27) { + v += 27; + } + const r = signatureBuffer.slice(1, 33); + const s = signatureBuffer.slice(33, 65); + const ecSignature: ECSignature = { + v, + r: ethUtil.bufferToHex(r), + s: ethUtil.bufferToHex(s), + }; + return ecSignature; + }, + parseSignatureHexAsRSV(signatureHex: string): ECSignature { + const { v, r, s } = ethUtil.fromRpcSig(signatureHex); + const ecSignature: ECSignature = { + v, + r: ethUtil.bufferToHex(r), + s: ethUtil.bufferToHex(s), + }; + return ecSignature; + }, +}; diff --git a/src/utils/utils.ts b/src/utils/utils.ts index f513f9893..ee7f29f40 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -14,6 +14,7 @@ import Web3 from "web3"; import { AbstractProvider } from "web3-core/types"; import { JsonRpcResponse } from "web3-core-helpers/types"; import { Contract } from "web3-eth-contract"; +import { assert } from "./assert"; import { Schema } from "./schemas/schema"; import { ENJIN_ADDRESS, @@ -965,7 +966,7 @@ export function getOrderHash(order: UnhashedOrder) { return getOrderHashHex(orderWithStringTypes as any); } -// Copied from: https://github.com/ProjectOpenSea/wyvern-js/blob/master/src/utils/utils.ts#L39 +// sourced from: https://github.com/ProjectOpenSea/wyvern-js/blob/master/src/utils/utils.ts#L39 function getOrderHashHex(order: UnhashedOrder): string { const orderParts = [ { value: order.exchange, type: SolidityTypes.Address }, @@ -1023,6 +1024,24 @@ function bigNumberToBN(value: BigNumber) { return new BN(value.toString(), 10); } +// Sourced from: https://github.com/ProjectOpenSea/wyvern-js/blob/master/src/wyvernProtocol.ts#L170 +export function toBaseUnitAmount( + amount: BigNumber, + decimals: number +): BigNumber { + assert.isBigNumber("amount", amount); + assert.isNumber("decimals", decimals); + const unit = new BigNumber(10).pow(decimals); + const baseUnitAmount = amount.times(unit); + const hasDecimals = baseUnitAmount.decimalPlaces() !== 0; + if (hasDecimals) { + throw new Error( + `Invalid unit amount: ${amount.toString()} - Too many decimal places` + ); + } + return baseUnitAmount; +} + /** * Assign an order and a new matching order to their buy/sell sides * @param order Original order