Skip to content

Commit

Permalink
fix: esm build compatibility (#1919)
Browse files Browse the repository at this point in the history
  • Loading branch information
Sam-Jeston authored Jun 19, 2024
1 parent 7b63133 commit d0ef575
Show file tree
Hide file tree
Showing 14 changed files with 364 additions and 9 deletions.
3 changes: 2 additions & 1 deletion packages/orderbook/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 1 addition & 1 deletion packages/orderbook/src/api-client/api-client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import {
anything, deepEqual, instance, mock, when,
} from 'ts-mockito';
import { ListingResult, OrdersService } from 'openapi/sdk';
import { OrderComponents } from '@opensea/seaport-js/lib/types';
import type { OrderComponents } from '@opensea/seaport-js/lib/types';
import { ItemType } from '../seaport';
import { ImmutableApiClient } from './api-client';

Expand Down
4 changes: 2 additions & 2 deletions packages/orderbook/src/seaport/components.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { OrderComponents } from '@opensea/seaport-js/lib/types';
import { getBulkOrderTree } from '@opensea/seaport-js/lib/utils/eip712/bulk-orders';
import type { OrderComponents } from '@opensea/seaport-js/lib/types';
import { BigNumber } from 'ethers';
import { getBulkOrderTree } from './lib/bulk-orders';

export function getOrderComponentsFromMessage(orderMessage: string): OrderComponents {
const data = JSON.parse(orderMessage);
Expand Down
1 change: 1 addition & 0 deletions packages/orderbook/src/seaport/lib/README.md
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.
61 changes: 61 additions & 0 deletions packages/orderbook/src/seaport/lib/bulk-orders.ts
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;
}
112 changes: 112 additions & 0 deletions packages/orderbook/src/seaport/lib/defaults.ts
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}`);
}
}
133 changes: 133 additions & 0 deletions packages/orderbook/src/seaport/lib/merkle.ts
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);
}
}
44 changes: 44 additions & 0 deletions packages/orderbook/src/seaport/lib/utils.ts
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);
4 changes: 2 additions & 2 deletions packages/orderbook/src/seaport/seaport.test.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import {
anything, deepEqual, instance, mock, when,
} from 'ts-mockito';
import { TransactionMethods } from '@opensea/seaport-js/lib/utils/usecase';
import type { TransactionMethods } from '@opensea/seaport-js/lib/utils/usecase';
import { ContractTransaction } from 'ethers-v6';
import { Seaport as SeaportLib } from '@opensea/seaport-js';
import {
import type {
ApprovalAction,
CreateOrderAction,
ExchangeAction,
Expand Down
Loading

0 comments on commit d0ef575

Please sign in to comment.