Skip to content

Commit 2d06f3b

Browse files
arilotterjohnrjj
andauthored
add eip 1271 support to v4 (#98)
* add eip 1271 support to v4 * fix: comment out ropsten tests until migrated to goerli. adds contract wallet classifier tests Co-authored-by: John Johnson <johnrjj@gmail.com>
1 parent 304ad76 commit 2d06f3b

File tree

10 files changed

+291
-58
lines changed

10 files changed

+291
-58
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ or npm:
3030

3131
`npm install @traderxyz/nft-swap-sdk`
3232

33-
You can check out an example project [here](https://nft-swapping-demo.vercel.app/)
33+
You can check out an example project [here](https://nft-swapping-demo.vercel.app/)
3434
or check out the example repo [here](https://github.com/HaidarEzio/NFTswap/tree/example-project)
3535

3636
## Configuration

src/sdk/v4/NftSwapV4.ts

Lines changed: 137 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import type {
2323
import { UnexpectedAssetTypeError } from '../error';
2424
import {
2525
approveAsset,
26+
checkIfContractWallet,
2627
DEFAULT_APP_ID,
2728
generateErc1155Order,
2829
generateErc721Order,
@@ -34,6 +35,7 @@ import {
3435
import type {
3536
AddressesForChainV4,
3637
ApprovalOverrides,
38+
AvailableSignatureTypesV4,
3739
ERC721OrderStruct,
3840
FillOrderOverrides,
3941
NftOrderV4,
@@ -97,8 +99,6 @@ export const SupportedChainsForV4OrderbookStatusMonitoring = [
9799
export interface INftSwapV4 extends BaseNftSwap {
98100
signOrder: (
99101
order: NftOrderV4,
100-
signerAddress: string,
101-
signer: Signer,
102102
signingOptions?: Partial<SigningOptionsV4>
103103
) => Promise<SignedNftOrderV4>;
104104
buildNftAndErc20Order: (
@@ -601,32 +601,85 @@ class NftSwapV4 implements INftSwapV4 {
601601
* Once signed, the order becomes fillable (as long as the order is valid)
602602
* 0x orders require a signature to fill.
603603
* @param order A 0x v4 order
604+
* @param signingOptions Options for signing
604605
* @returns A signed 0x v4 order
605606
*/
606-
signOrder = async (order: NftOrderV4): Promise<SignedNftOrderV4> => {
607+
signOrder = async (
608+
order: NftOrderV4,
609+
signingOptions?: Partial<SigningOptionsV4>
610+
): Promise<SignedNftOrderV4> => {
607611
if (!this.signer) {
608-
throw new Error('Signed not defined');
612+
throw new Error('Signer not defined');
609613
}
610614

611-
const rawSignature = await signOrderWithEoaWallet(
612-
order,
613-
this.signer as unknown as TypedDataSigner,
614-
this.chainId,
615-
this.exchangeProxy.address
616-
);
615+
let method: AvailableSignatureTypesV4 = 'eoa';
616+
// If we have any specific signature type overrides, prefer those
617+
if (signingOptions?.signatureType === 'eip1271') {
618+
method = 'eip1271';
619+
} else if (signingOptions?.signatureType === 'eoa') {
620+
method = 'eoa';
621+
} else {
622+
// Try to detect...
623+
if (signingOptions?.autodetectSignatureType === false) {
624+
method = 'eoa';
625+
} else {
626+
// If we made it here, consumer has no preferred signing method,
627+
// let's try feature detection to automagically pick a signature type
628+
// By default we fallback to EOA signing if we can't figure it out.
617629

618-
const ecSignature = parseRawSignature(rawSignature);
630+
// Let's try to determine if the signer is a contract wallet or not.
631+
// If it is, we'll try EIP-1271, otherwise we'll do a normal sign
632+
const signerAddress = await this.signer.getAddress();
619633

620-
const signedOrder = {
621-
...order,
622-
signature: {
623-
signatureType: 2,
624-
r: ecSignature.r,
625-
s: ecSignature.s,
626-
v: ecSignature.v,
627-
},
628-
};
629-
return signedOrder;
634+
const isContractWallet = await checkIfContractWallet(
635+
this.provider,
636+
signerAddress
637+
);
638+
if (isContractWallet) {
639+
method = 'eip1271';
640+
} else {
641+
method = 'eoa';
642+
}
643+
}
644+
}
645+
switch (method) {
646+
case 'eoa': {
647+
const rawSignature = await signOrderWithEoaWallet(
648+
order,
649+
this.signer as unknown as TypedDataSigner,
650+
this.chainId,
651+
this.exchangeProxy.address
652+
);
653+
654+
const ecSignature = parseRawSignature(rawSignature);
655+
656+
const signedOrder = {
657+
...order,
658+
signature: {
659+
signatureType: 2,
660+
r: ecSignature.r,
661+
s: ecSignature.s,
662+
v: ecSignature.v,
663+
},
664+
};
665+
return signedOrder;
666+
}
667+
case 'eip1271': {
668+
await this.preSignOrder(order);
669+
const preSignedOrder = {
670+
...order,
671+
signature: {
672+
signatureType: 4,
673+
r: '0',
674+
s: '0',
675+
v: 0,
676+
},
677+
};
678+
return preSignedOrder;
679+
}
680+
default:
681+
throw new Error(`Unknown signature method chosen: ${method}`);
682+
}
630683
};
631684

632685
/**
@@ -862,6 +915,69 @@ class NftSwapV4 implements INftSwapV4 {
862915
console.log('unsupported order', signedOrder);
863916
throw new Error('unsupport signedOrder type');
864917
};
918+
/**
919+
* Pre-signs and submits an order
920+
* @param signedOrder An unsigned 0x v4 order
921+
* @param fillOrderOverrides Optional configuration on possible ways to fill the order
922+
* @param transactionOverrides Ethers transaction overrides (e.g. gas price)
923+
* @returns
924+
*/
925+
preSignOrder = async (
926+
order: NftOrderV4,
927+
fillOrderOverrides?: Partial<FillOrderOverrides>,
928+
transactionOverrides?: Partial<PayableOverrides>
929+
) => {
930+
// Only Sell orders can be filled with ETH
931+
const canOrderTypeBeFilledWithNativeToken =
932+
order.direction === TradeDirection.SellNFT;
933+
// Is ERC20 being traded the native token
934+
const isNativeToken = this.isErc20NativeToken(order);
935+
const needsEthAttached =
936+
isNativeToken && canOrderTypeBeFilledWithNativeToken;
937+
if (needsEthAttached) {
938+
console.log(
939+
"can't pre-sign orders that need to be filled with ETH",
940+
order
941+
);
942+
throw new Error("can't pre-sign orders that need to be filled with ETH");
943+
}
944+
// do fill
945+
if ('erc1155Token' in order) {
946+
// If maker is selling an NFT, taker wants to 'buy' nft
947+
if (
948+
order.direction === TradeDirection.BuyNFT &&
949+
order.erc1155TokenProperties.length > 0 &&
950+
fillOrderOverrides?.tokenIdToSellForCollectionOrder === undefined
951+
) {
952+
// property based order, let's make sure they've specifically provided a tokenIdToSellForCollectionOrder
953+
throw new Error(
954+
'Collection order missing NFT tokenId to fill with. Specify in fillOrderOverrides.tokenIdToSellForCollectionOrder'
955+
);
956+
}
957+
958+
return this.exchangeProxy.preSignERC1155Order(order, {
959+
...transactionOverrides,
960+
});
961+
} else if ('erc721Token' in order) {
962+
// If maker is selling an NFT, taker wants to 'buy' nft
963+
if (
964+
order.direction === TradeDirection.BuyNFT &&
965+
order.erc721TokenProperties.length > 0 &&
966+
fillOrderOverrides?.tokenIdToSellForCollectionOrder === undefined
967+
) {
968+
// property based order, let's make sure they've specifically provided a tokenIdToSellForCollectionOrder
969+
throw new Error(
970+
'Collection order missing NFT tokenId to fill with. Specify in fillOrderOverrides.tokenIdToSellForCollectionOrder'
971+
);
972+
}
973+
// Otherwise, taker is selling the nft (and buying an ERC20)
974+
return this.exchangeProxy.preSignERC721Order(order, {
975+
...transactionOverrides,
976+
});
977+
}
978+
console.log('unsupported order', order);
979+
throw new Error('unsupport signedOrder type');
980+
};
865981

866982
/**
867983
* Posts a 0x order to the Trader.xyz NFT open orderbook

src/sdk/v4/pure.ts

Lines changed: 88 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { Signer, TypedDataSigner } from '@ethersproject/abstract-signer';
22
import { BigNumber } from '@ethersproject/bignumber';
33
import { hexDataLength, hexDataSlice } from '@ethersproject/bytes';
4-
import type { BaseProvider } from '@ethersproject/providers';
4+
import type { BaseProvider, Provider } from '@ethersproject/providers';
55
import type { ContractTransaction } from '@ethersproject/contracts';
66
import getUnixTime from 'date-fns/getUnixTime';
77
import { v4 } from 'uuid';
@@ -100,6 +100,93 @@ export const signOrderWithEoaWallet = async (
100100
throw new Error(`Unknown order type`);
101101
};
102102

103+
export const preSignOrder = async (
104+
order: NftOrderV4,
105+
signer: TypedDataSigner,
106+
chainId: number,
107+
exchangeContractAddress: string
108+
) => {
109+
if ((order as ERC1155OrderStruct).erc1155Token) {
110+
const domain = {
111+
chainId: chainId,
112+
verifyingContract: exchangeContractAddress,
113+
name: 'ZeroEx',
114+
version: '1.0.0',
115+
};
116+
const types = {
117+
[ERC1155ORDER_STRUCT_NAME]: ERC1155ORDER_STRUCT_ABI,
118+
Fee: FEE_ABI,
119+
Property: PROPERTY_ABI,
120+
};
121+
const value = order;
122+
123+
const rawSignatureFromEoaWallet = await signer._signTypedData(
124+
domain,
125+
types,
126+
value
127+
);
128+
129+
return rawSignatureFromEoaWallet;
130+
}
131+
132+
if ((order as ERC721OrderStruct).erc721Token) {
133+
const domain = {
134+
chainId: chainId,
135+
verifyingContract: exchangeContractAddress,
136+
name: 'ZeroEx',
137+
version: '1.0.0',
138+
};
139+
const types = {
140+
[ERC721ORDER_STRUCT_NAME]: ERC721ORDER_STRUCT_ABI,
141+
Fee: FEE_ABI,
142+
Property: PROPERTY_ABI,
143+
};
144+
const value = order;
145+
146+
const rawSignatureFromEoaWallet = await signer._signTypedData(
147+
domain,
148+
types,
149+
value
150+
);
151+
152+
return rawSignatureFromEoaWallet;
153+
}
154+
155+
warning(!order, 'Unknown order type');
156+
throw new Error(`Unknown order type`);
157+
};
158+
159+
export const checkIfContractWallet = async (
160+
provider: Provider,
161+
walletAddress: string
162+
): Promise<boolean> => {
163+
let isContractWallet: boolean = false;
164+
if (provider.getCode) {
165+
let walletCode = await provider.getCode(walletAddress);
166+
// Wallet Code returns '0x' if no contract address is associated with
167+
// Note: Lazy loaded contract wallets will show 0x initially, so we fall back to feature detection
168+
if (walletCode && walletCode !== '0x') {
169+
isContractWallet = true;
170+
}
171+
}
172+
let isSequence = !!(provider as any)._isSequenceProvider;
173+
if (isSequence) {
174+
isContractWallet = true;
175+
}
176+
// Walletconnect hides the real provider in the provider (yo dawg)
177+
let providerToUse = (provider as any).provider;
178+
if (providerToUse?.isWalletConnect) {
179+
const isSequenceViaWalletConnect = !!(
180+
(providerToUse as any).connector?._peerMeta?.description === 'Sequence'
181+
);
182+
if (isSequenceViaWalletConnect) {
183+
isContractWallet = true;
184+
}
185+
}
186+
187+
return isContractWallet;
188+
};
189+
103190
/**
104191
*
105192
* @param walletAddress Owner of the asset

src/sdk/v4/types.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -167,14 +167,14 @@ export type ECSignature = {
167167
};
168168

169169
export type SignatureStruct = {
170-
signatureType: number; // 2 for EIP-712
170+
signatureType: number; // 2 for EIP-712, 4 for PRESIGNED
171171
v: number;
172172
r: string;
173173
s: string;
174174
};
175175

176176
export type SignatureStructSerialized = {
177-
signatureType: number; // 2 for EIP-712
177+
signatureType: number; // 2 for EIP-712, 4 for PRESIGNED
178178
v: number;
179179
r: string;
180180
s: string;
@@ -207,7 +207,7 @@ export interface BuildOrderAdditionalConfig {
207207
nonce: BigNumberish;
208208
}
209209

210-
export type AvailableSignatureTypesV4 = 'eoa'; // No EIP-1271 / preSign yet (soon though)
210+
export type AvailableSignatureTypesV4 = 'eoa' | 'eip1271';
211211

212212
export interface SigningOptionsV4 {
213213
signatureType: AvailableSignatureTypesV4; // | 'autodetect' ? and remove autodetectSignatureType maybe?

test/v3/swap.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ const MAKER_ASSET: SwappableAsset = {
4747
};
4848

4949
describe('NFTSwap', () => {
50-
it('swaps 0.1 DAI and 0.1 USDC correctly', async () => {
50+
xit('swaps 0.1 DAI and 0.1 USDC correctly', async () => {
5151
// NOTE(johnrjj) - Assumes USDC and DAI are already approved w/ the ExchangeProxy
5252

5353
const gasPrice = (await PROVIDER.getGasPrice()).mul(2);

test/v4/erc1155-test-swap.test.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,14 @@ const RPC_TESTNET =
2020

2121
const MAKER_WALLET = new ethers.Wallet(MAKER_PRIVATE_KEY);
2222

23-
const PROVIDER = new ethers.providers.StaticJsonRpcProvider(RPC_TESTNET);
23+
const ROPSTEN_CHAIN_ID = 3;
24+
const PROVIDER = new ethers.providers.StaticJsonRpcProvider(
25+
RPC_TESTNET,
26+
ROPSTEN_CHAIN_ID
27+
);
2428

2529
const MAKER_SIGNER = MAKER_WALLET.connect(PROVIDER);
2630

27-
const ROPSTEN_CHAIN_ID = 3;
28-
2931
const nftSwapperMaker = new NftSwapV4(
3032
MAKER_SIGNER as any,
3133
MAKER_SIGNER,
@@ -88,7 +90,7 @@ describe('NFTSwapV4', () => {
8890
expect(signedOrderErc1155.erc20TokenAmount).toBe(ERC20_ASSET.amount);
8991
expect(signedOrderErc1155.direction.toString()).toBe('0');
9092

91-
await nftSwapperMaker.postOrder(signedOrder, '3');
93+
// await nftSwapperMaker.postOrder(signedOrder, '3');
9294
// Uncomment to fill
9395
// const fillTx = await nftSwapperMaker.fillSignedOrder(signedOrder);
9496
// const txReceipt = await fillTx.wait();

test/v4/erc721-test-swap-mumbai.test.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -101,10 +101,11 @@ describe('NFTSwapV4', () => {
101101
// })
102102

103103
// const maybeOrder = maybeOrders.orders[0];
104+
// const maybeOrder = { order: signedOrderErc1155 }
104105

105-
// expect(maybeOrder.order.nonce).toEqual(signedOrder.nonce.toString(10))
106+
// // expect(maybeOrder.order.nonce).toEqual(signedOrder.nonce.toString(10))
106107

107-
// expect(maybeOrder.order.signature.signatureType.toString()).toEqual('2');
108+
// // expect(maybeOrder.order.signature.signatureType.toString()).toEqual('2');
108109

109110
// const fillTx = await nftSwapperMaker.fillSignedOrder(maybeOrder.order);
110111

@@ -115,6 +116,6 @@ describe('NFTSwapV4', () => {
115116

116117
// expect(txReceipt.transactionHash).toBeTruthy();
117118

118-
// console.log(`Swapped on Polygon Mumbai (txHAsh: ${txReceipt.transactionIndex})`);
119+
// console.log(`Swapped on Polygon Mumbai (txHash: ${txReceipt.transactionHash})`);
119120
});
120121
});

0 commit comments

Comments
 (0)