-
Notifications
You must be signed in to change notification settings - Fork 26
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
fix: esm build compatibility (#1919)
- Loading branch information
1 parent
7b63133
commit d0ef575
Showing
14 changed files
with
364 additions
and
9 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -8,7 +8,8 @@ | |
"@opensea/seaport-js": "4.0.2", | ||
"axios": "^1.6.5", | ||
"ethers": "^5.7.2", | ||
"ethers-v6": "npm:[email protected]" | ||
"ethers-v6": "npm:[email protected]", | ||
"merkletreejs": "^0.3.11" | ||
}, | ||
"devDependencies": { | ||
"@rollup/plugin-typescript": "^11.0.0", | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
The contents of this folder is copied directly from https://github.com/ProjectOpenSea/seaport-js/tree/main/src/utils/eip712 to work around an ESM module issue with the SDK. Once resolved this code can be removed and the import of get `getBulkOrderTree` can be reinstated. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
import type { OrderComponents } from '@opensea/seaport-js/lib/types'; | ||
import { TypedDataEncoder, keccak256, toUtf8Bytes } from 'ethers-v6'; | ||
import { EIP_712_ORDER_TYPE } from 'seaport/constants'; | ||
|
||
import { Eip712MerkleTree } from './merkle'; | ||
import { DefaultGetter } from './defaults'; | ||
import { fillArray } from './utils'; | ||
import type { EIP712TypeDefinitions } from './defaults'; | ||
|
||
function getBulkOrderTypes(height: number): EIP712TypeDefinitions { | ||
return { | ||
...EIP_712_ORDER_TYPE, | ||
// eslint-disable-next-line @typescript-eslint/naming-convention | ||
BulkOrder: [{ name: 'tree', type: `OrderComponents${'[2]'.repeat(height)}` }], | ||
}; | ||
} | ||
|
||
export function getBulkOrderTreeHeight(length: number): number { | ||
return Math.max(Math.ceil(Math.log2(length)), 1); | ||
} | ||
|
||
export function getBulkOrderTree( | ||
orderComponents: OrderComponents[], | ||
startIndex = 0, | ||
height = getBulkOrderTreeHeight(orderComponents.length + startIndex), | ||
) { | ||
const types = getBulkOrderTypes(height); | ||
const defaultNode = DefaultGetter.from(types, 'OrderComponents'); | ||
let elements = [...orderComponents]; | ||
|
||
if (startIndex > 0) { | ||
elements = [ | ||
...fillArray([] as OrderComponents[], startIndex, defaultNode), | ||
...orderComponents, | ||
]; | ||
} | ||
const tree = new Eip712MerkleTree( | ||
types, | ||
'BulkOrder', | ||
'OrderComponents', | ||
elements, | ||
height, | ||
); | ||
return tree; | ||
} | ||
|
||
export function getBulkOrderTypeHash(height: number): string { | ||
const types = getBulkOrderTypes(height); | ||
const encoder = TypedDataEncoder.from(types); | ||
const typeString = toUtf8Bytes(encoder.types.BulkOrder[0].type); | ||
return keccak256(typeString); | ||
} | ||
|
||
export function getBulkOrderTypeHashes(maxHeight: number): string[] { | ||
const typeHashes: string[] = []; | ||
// eslint-disable-next-line no-plusplus | ||
for (let i = 0; i < maxHeight; i++) { | ||
typeHashes.push(getBulkOrderTypeHash(i + 1)); | ||
} | ||
return typeHashes; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,112 @@ | ||
import { ethers, zeroPadValue, TypedDataField } from 'ethers-v6'; | ||
|
||
const baseDefaults: Record<string, any> = { | ||
integer: 0, | ||
address: ethers.zeroPadValue('0x', 20), | ||
bool: false, | ||
bytes: '0x', | ||
string: '', | ||
}; | ||
|
||
const isNullish = (value: any): boolean => { | ||
if (value === undefined) return false; | ||
|
||
return ( | ||
value !== undefined | ||
&& value !== null | ||
&& ((['string', 'number'].includes(typeof value) && BigInt(value) === 0n) | ||
|| (Array.isArray(value) && value.every(isNullish)) | ||
|| (typeof value === 'object' && Object.values(value).every(isNullish)) | ||
|| (typeof value === 'boolean' && value === false)) | ||
); | ||
}; | ||
|
||
function getDefaultForBaseType(type: string): any { | ||
// bytesXX | ||
const [, width] = type.match(/^bytes(\d+)$/) ?? []; | ||
// eslint-disable-next-line radix | ||
if (width) return zeroPadValue('0x', parseInt(width)); | ||
|
||
// eslint-disable-next-line no-param-reassign | ||
if (type.match(/^(u?)int(\d*)$/)) type = 'integer'; | ||
|
||
return baseDefaults[type]; | ||
} | ||
|
||
export type EIP712TypeDefinitions = Record<string, TypedDataField[]>; | ||
|
||
type DefaultMap<T extends EIP712TypeDefinitions> = { | ||
[K in keyof T]: any; | ||
}; | ||
|
||
export class DefaultGetter<Types extends EIP712TypeDefinitions> { | ||
defaultValues: DefaultMap<Types> = {} as DefaultMap<Types>; | ||
|
||
constructor(protected types: Types) { | ||
// eslint-disable-next-line no-restricted-syntax, guard-for-in | ||
for (const name in types) { | ||
const defaultValue = this.getDefaultValue(name); | ||
this.defaultValues[name] = defaultValue; | ||
if (!isNullish(defaultValue)) { | ||
throw new Error( | ||
`Got non-empty value for type ${name} in default generator: ${defaultValue}`, | ||
); | ||
} | ||
} | ||
} | ||
|
||
/* eslint-disable no-dupe-class-members */ | ||
static from<Types extends EIP712TypeDefinitions>( | ||
types: Types | ||
): DefaultMap<Types>; | ||
|
||
static from<Types extends EIP712TypeDefinitions>( | ||
types: Types, | ||
type: keyof Types | ||
): any; | ||
|
||
static from<Types extends EIP712TypeDefinitions>( | ||
types: Types, | ||
type?: keyof Types, | ||
): DefaultMap<Types> { | ||
const { defaultValues } = new DefaultGetter(types); | ||
if (type) return defaultValues[type]; | ||
return defaultValues; | ||
} | ||
/* eslint-enable no-dupe-class-members */ | ||
|
||
getDefaultValue(type: string): any { | ||
if (this.defaultValues[type]) return this.defaultValues[type]; | ||
// Basic type (address, bool, uint256, etc) | ||
const basic = getDefaultForBaseType(type); | ||
if (basic !== undefined) return basic; | ||
|
||
// Array | ||
const match = type.match(/^(.*)(\x5b(\d*)\x5d)$/); | ||
if (match) { | ||
const subtype = match[1]; | ||
// eslint-disable-next-line radix | ||
const length = parseInt(match[3]); | ||
if (length > 0) { | ||
const baseValue = this.getDefaultValue(subtype); | ||
return Array(length).fill(baseValue); | ||
} | ||
return []; | ||
} | ||
|
||
// Struct | ||
const fields = this.types[type]; | ||
if (fields) { | ||
return fields.reduce( | ||
// eslint-disable-next-line @typescript-eslint/no-shadow | ||
(obj, { name, type }) => ({ | ||
...obj, | ||
[name]: this.getDefaultValue(type), | ||
}), | ||
{}, | ||
); | ||
} | ||
|
||
throw new Error(`unknown type: ${type}`); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,133 @@ | ||
import { | ||
TypedDataEncoder, | ||
AbiCoder, | ||
keccak256, | ||
toUtf8Bytes, | ||
concat, | ||
} from 'ethers-v6'; | ||
import { MerkleTree } from 'merkletreejs'; | ||
|
||
import { DefaultGetter } from './defaults'; | ||
import { | ||
bufferKeccak, | ||
bufferToHex, | ||
chunk, | ||
fillArray, | ||
getRoot, | ||
hexToBuffer, | ||
} from './utils'; | ||
|
||
import type { EIP712TypeDefinitions } from './defaults'; | ||
|
||
type BulkOrderElements<T> = | ||
| [T, T] | ||
| [BulkOrderElements<T>, BulkOrderElements<T>]; | ||
|
||
// eslint-disable-next-line max-len | ||
const getTree = (leaves: string[], defaultLeafHash: string) => new MerkleTree(leaves.map(hexToBuffer), bufferKeccak, { | ||
complete: true, | ||
sort: false, | ||
hashLeaves: false, | ||
fillDefaultHash: hexToBuffer(defaultLeafHash), | ||
}); | ||
|
||
const encodeProof = ( | ||
key: number, | ||
proof: string[], | ||
signature = `0x${'ff'.repeat(64)}`, | ||
) => concat([ | ||
signature, | ||
`0x${key.toString(16).padStart(6, '0')}`, | ||
AbiCoder.defaultAbiCoder().encode([`uint256[${proof.length}]`], [proof]), | ||
]); | ||
|
||
export class Eip712MerkleTree<BaseType extends Record<string, any> = any> { | ||
tree: MerkleTree; | ||
|
||
private leafHasher: (value: any) => string; | ||
|
||
defaultNode: BaseType; | ||
|
||
defaultLeaf: string; | ||
|
||
encoder: TypedDataEncoder; | ||
|
||
get completedSize() { | ||
return 2 ** this.depth; | ||
} | ||
|
||
/** Returns the array of elements in the tree, padded to the complete size with empty items. */ | ||
getCompleteElements() { | ||
const { elements } = this; | ||
return fillArray([...elements], this.completedSize, this.defaultNode); | ||
} | ||
|
||
// eslint-disable-next-line max-len | ||
/** Returns the array of leaf nodes in the tree, padded to the complete size with default hashes. */ | ||
getCompleteLeaves() { | ||
const leaves = this.elements.map(this.leafHasher); | ||
return fillArray([...leaves], this.completedSize, this.defaultLeaf); | ||
} | ||
|
||
get root() { | ||
return this.tree.getHexRoot(); | ||
} | ||
|
||
getProof(i: number) { | ||
const leaves = this.getCompleteLeaves(); | ||
const leaf = leaves[i]; | ||
const proof = this.tree.getHexProof(leaf, i); | ||
const root = this.tree.getHexRoot(); | ||
return { leaf, proof, root }; | ||
} | ||
|
||
getEncodedProofAndSignature(i: number, signature: string) { | ||
const { proof } = this.getProof(i); | ||
return encodeProof(i, proof, signature); | ||
} | ||
|
||
getDataToSign(): BulkOrderElements<BaseType> { | ||
let layer = this.getCompleteElements() as any; | ||
while (layer.length > 2) { | ||
layer = chunk(layer, 2); | ||
} | ||
return layer; | ||
} | ||
|
||
add(element: BaseType) { | ||
this.elements.push(element); | ||
} | ||
|
||
getBulkOrderHash() { | ||
const structHash = this.encoder.hashStruct('BulkOrder', { | ||
tree: this.getDataToSign(), | ||
}); | ||
const leaves = this.getCompleteLeaves().map(hexToBuffer); | ||
const rootHash = bufferToHex(getRoot(leaves, false)); | ||
const typeHash = keccak256( | ||
toUtf8Bytes(this.encoder.types.BulkOrder[0].type), | ||
); | ||
const bulkOrderHash = keccak256(concat([typeHash, rootHash])); | ||
|
||
if (bulkOrderHash !== structHash) { | ||
throw new Error('expected derived bulk order hash to match'); | ||
} | ||
|
||
return structHash; | ||
} | ||
|
||
constructor( | ||
public types: EIP712TypeDefinitions, | ||
public rootType: string, | ||
public leafType: string, | ||
public elements: BaseType[], | ||
public depth: number, | ||
) { | ||
const encoder = TypedDataEncoder.from(types); | ||
this.encoder = encoder; | ||
this.leafHasher = (leaf: BaseType) => encoder.hashStruct(leafType, leaf); | ||
this.defaultNode = DefaultGetter.from(types, leafType); | ||
this.defaultLeaf = this.leafHasher(this.defaultNode); | ||
this.tree = getTree(this.getCompleteLeaves(), this.defaultLeaf); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
import { concat, toBeHex, keccak256 } from 'ethers-v6'; | ||
|
||
import type { BytesLike } from 'ethers-v6'; | ||
|
||
export const makeArray = <T>(len: number, getValue: (i: number) => T) => Array(len) | ||
.fill(0) | ||
.map((_, i) => getValue(i)); | ||
|
||
// eslint-disable-next-line max-len | ||
export const chunk = <T>(array: T[], size: number) => makeArray(Math.ceil(array.length / size), (i) => array.slice(i * size, (i + 1) * size)); | ||
|
||
export const bufferToHex = (buf: Buffer) => toBeHex(buf.toString('hex')); | ||
|
||
export const hexToBuffer = (value: string) => Buffer.from(value.slice(2), 'hex'); | ||
|
||
export const bufferKeccak = (value: BytesLike) => hexToBuffer(keccak256(value)); | ||
|
||
export const hashConcat = (arr: BytesLike[]) => bufferKeccak(concat(arr)); | ||
|
||
export const fillArray = <T>(arr: T[], length: number, value: T) => { | ||
if (length > arr.length) arr.push(...Array(length - arr.length).fill(value)); | ||
return arr; | ||
}; | ||
|
||
export const getRoot = (elements: (Buffer | string)[], hashLeaves = true) => { | ||
if (elements.length === 0) throw new Error('empty tree'); | ||
|
||
const leaves = elements.map((e) => { | ||
const leaf = Buffer.isBuffer(e) ? e : hexToBuffer(e); | ||
return hashLeaves ? bufferKeccak(leaf) : leaf; | ||
}); | ||
|
||
const layers: Buffer[][] = [leaves]; | ||
|
||
// Get next layer until we reach the root | ||
while (layers[layers.length - 1].length > 1) { | ||
// eslint-disable-next-line @typescript-eslint/no-use-before-define | ||
layers.push(getNextLayer(layers[layers.length - 1])); | ||
} | ||
|
||
return layers[layers.length - 1][0]; | ||
}; | ||
|
||
export const getNextLayer = (elements: Buffer[]) => chunk(elements, 2).map(hashConcat); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.