Skip to content

Commit 5576039

Browse files
committed
ADDS support for integrator attribution
1 parent 491c578 commit 5576039

File tree

5 files changed

+202
-29
lines changed

5 files changed

+202
-29
lines changed

src/sdk/v4/NftSwapV4.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import type {
2323
import { UnexpectedAssetTypeError } from '../error';
2424
import {
2525
approveAsset,
26+
DEFAULT_APP_ID,
2627
generateErc1155Order,
2728
generateErc721Order,
2829
getApprovalStatus,
@@ -154,7 +155,11 @@ export interface INftSwapV4 extends BaseNftSwap {
154155
}
155156

156157
export interface AdditionalSdkConfig {
158+
// Identify your app fills with distinct integer
159+
appId: string;
160+
// Custom zeroex proxy contract address (defaults to the canonical contracts deployed by 0x Labs core team)
157161
zeroExExchangeProxyContractAddress: string;
162+
// Custom orderbook url. Defaults to using Trader.xyz's multi-chain open orderbook
158163
orderbookRootUrl: string;
159164
}
160165

@@ -165,6 +170,9 @@ class NftSwapV4 implements INftSwapV4 {
165170
public exchangeProxy: IZeroEx;
166171
public exchangeProxyContractAddress: string;
167172

173+
// Unique identifier for app. Must be a positive integer between 1 and 2**128
174+
public appId: string;
175+
168176
public orderbookRootUrl: string;
169177

170178
constructor(
@@ -195,6 +203,8 @@ class NftSwapV4 implements INftSwapV4 {
195203
this.orderbookRootUrl =
196204
additionalConfig?.orderbookRootUrl ?? ORDERBOOK_API_ROOT_URL_PRODUCTION;
197205

206+
this.appId = additionalConfig?.appId ?? DEFAULT_APP_ID;
207+
198208
this.exchangeProxy = IZeroEx__factory.connect(
199209
zeroExExchangeContractAddress,
200210
signer ?? provider
@@ -432,7 +442,11 @@ class NftSwapV4 implements INftSwapV4 {
432442
makerAddress: string,
433443
userConfig?: Partial<OrderStructOptionsCommonStrict>
434444
): NftOrderV4Serialized => {
435-
const defaultConfig = { chainId: this.chainId, makerAddress: makerAddress };
445+
const defaultConfig = {
446+
chainId: this.chainId,
447+
makerAddress: makerAddress,
448+
appId: this.appId,
449+
};
436450
const config = { ...defaultConfig, ...userConfig };
437451

438452
const direction =

src/sdk/v4/pure.ts

Lines changed: 70 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ import type { ContractTransaction } from '@ethersproject/contracts';
66
import getUnixTime from 'date-fns/getUnixTime';
77
import { v4 } from 'uuid';
88
import warning from 'tiny-warning';
9+
import invariant from 'tiny-invariant';
10+
import padEnd from 'lodash/padEnd';
11+
import padStart from 'lodash/padStart';
912
import {
1013
ERC1155__factory,
1114
ERC20__factory,
@@ -322,7 +325,9 @@ export const generateErc721Order = (
322325
};
323326
}) ?? [],
324327
expiry: expiry,
325-
nonce: orderData.nonce?.toString() ?? generateRandomV4OrderNonce(),
328+
nonce:
329+
orderData.nonce?.toString() ??
330+
generateRandomV4OrderNonce(orderData.appId),
326331
taker: orderData.taker?.toLowerCase() ?? NULL_ADDRESS,
327332
};
328333

@@ -367,21 +372,81 @@ export const generateErc1155Order = (
367372
};
368373
}) ?? [],
369374
expiry: expiry,
370-
nonce: orderData.nonce?.toString() ?? generateRandomV4OrderNonce(),
375+
nonce:
376+
orderData.nonce?.toString() ??
377+
generateRandomV4OrderNonce(orderData.appId),
371378
taker: orderData.taker?.toLowerCase() ?? NULL_ADDRESS,
372379
};
373380

374381
return erc1155Order;
375382
};
376383

384+
// Number of digits in base 10 128bit nonce
385+
// floor(log_10(2^128 - 1)) + 1
386+
export const ONE_TWENTY_EIGHT_BIT_LENGTH = 39;
387+
388+
// Max nonce digit length in base 10
389+
// floor(log_10(2^256 - 1)) + 1
390+
export const TWO_FIFTY_SIX_BIT_LENGTH = 78;
391+
392+
const checkIfStringContainsOnlyNumbers = (val: string) => {
393+
const onlyNumbers = /^\d+$/.test(val);
394+
return onlyNumbers;
395+
};
396+
397+
export const RESERVED_APP_ID_PREFIX = '1001';
398+
const RESERVED_APP_ID_PREFIX_DIGITS = RESERVED_APP_ID_PREFIX.length;
399+
400+
export const DEFAULT_APP_ID = '314159';
401+
402+
const verifyAppIdOrThrow = (appId: string) => {
403+
const isCorrectLength =
404+
appId.length <= ONE_TWENTY_EIGHT_BIT_LENGTH - RESERVED_APP_ID_PREFIX_DIGITS;
405+
const hasOnlyNumbers = checkIfStringContainsOnlyNumbers(appId);
406+
invariant(isCorrectLength, 'appId must be 39 digits or less');
407+
invariant(
408+
hasOnlyNumbers,
409+
'appId must be numeric only (no alpha or special characters, only numbers)'
410+
);
411+
};
412+
377413
/**
414+
* Generates a 256bit nonce.
415+
* The format:
416+
* First 128bits: ${SDK_PREFIX}${APP_ID}000000 (right padded zeroes to fill)
417+
* Second 128bits: ${RANDOM_GENERATED_128BIT_ORDER_HASH}
378418
* @returns 128bit nonce as string (0x orders can handle up to 256 bit nonce)
379419
*/
380-
export const generateRandomV4OrderNonce = (): string => {
420+
export const generateRandomV4OrderNonce = (
421+
appId: string = DEFAULT_APP_ID
422+
): string => {
423+
if (appId) {
424+
verifyAppIdOrThrow(appId);
425+
}
426+
const order128 = padStart(
427+
generateRandom128BitNumber(),
428+
ONE_TWENTY_EIGHT_BIT_LENGTH,
429+
'0'
430+
);
431+
const appId128 = padEnd(
432+
`${RESERVED_APP_ID_PREFIX}${appId}`,
433+
ONE_TWENTY_EIGHT_BIT_LENGTH,
434+
'0'
435+
);
436+
const final256BitNonce = `${appId128}${order128}`;
437+
invariant(
438+
final256BitNonce.length <= TWO_FIFTY_SIX_BIT_LENGTH,
439+
'Invalid nonce size'
440+
);
441+
return final256BitNonce;
442+
};
443+
444+
// uuids are 128bits
445+
export const generateRandom128BitNumber = (base = 10): string => {
381446
const hex = '0x' + v4().replace(/-/g, '');
382447
const value = BigInt(hex);
383-
const decimal = value.toString(); // don't convert this to a number, will lose precision
384-
return decimal;
448+
const valueBase10String = value.toString(base); // don't convert this to a number, will lose precision
449+
return valueBase10String;
385450
};
386451

387452
export const serializeNftOrder = (

src/sdk/v4/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ export interface OrderStructOptionsCommon {
9595
direction: BigNumberish;
9696
maker: string;
9797
taker: string;
98+
appId: string;
9899
expiry: Date | number;
99100
nonce: BigNumberish;
100101
// erc20Token: string;
@@ -108,6 +109,7 @@ export interface OrderStructOptionsCommonStrict {
108109
// erc20Token: string;
109110
// erc20TokenAmount: BigNumberish;
110111
maker: string;
112+
appId?: string;
111113
taker?: string;
112114
expiry?: Date | number;
113115
nonce?: BigNumberish;

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

Lines changed: 2 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -96,36 +96,15 @@ describe('NFTSwapV4', () => {
9696
expect(signedOrderErc1155.direction.toString()).toBe('0');
9797

9898
await nftSwapperMaker.postOrder(signedOrder, '3');
99-
// console.log('erc721 signatuee', signedOrder.signature);
100-
// expect(signedOrder.signature.signatureType.toString()).toEqual('2');
99+
100+
expect(signedOrder.signature.signatureType.toString()).toEqual('2');
101101

102102
// const fillTx = await nftSwapperMaker.fillSignedOrder(signedOrder);
103103
// const txReceipt = await fillTx.wait();
104104
// console.log('erc721 fill tx', txReceipt.transactionHash);
105105

106106
// expect(txReceipt.transactionHash).toBeTruthy();
107107

108-
// const normalizedOrder = normalizeOrder(order);
109-
// const signedOrder = await nftSwapperMaker.signOrder(
110-
// normalizedOrder,
111-
// );
112-
113-
// const normalizedSignedOrder = normalizeOrder(signedOrder);
114-
115-
// expect(normalizedSignedOrder.makerAddress.toLowerCase()).toBe(
116-
// MAKER_WALLET_ADDRESS.toLowerCase()
117-
// );
118-
119-
// Uncomment to actually fill order
120-
// const tx = await nftSwapperMaker.fillSignedOrder(signedOrder, undefined, {
121-
// gasPrice,
122-
// gasLimit: '500000',
123-
// // HACK(johnnrjj) - Rinkeby still has protocol fees, so we give it a little bit of ETH so its happy.
124-
// value: parseEther('0.01'),
125-
// });
126-
127-
// const txReceipt = await tx.wait();
128-
// expect(txReceipt.transactionHash).toBeTruthy();
129108
// console.log(`Swapped on Ropsten (txHAsh: ${txReceipt.transactionIndex})`);
130109
});
131110
});

test/v4/nonce.test.ts

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import { ethers } from 'ethers';
2+
import { ETH_ADDRESS_AS_ERC20 } from '../../src/sdk';
3+
import { NftSwapV4 } from '../../src/sdk/v4/NftSwapV4';
4+
import {
5+
generateRandomV4OrderNonce,
6+
TWO_FIFTY_SIX_BIT_LENGTH,
7+
} from '../../src/sdk/v4/pure';
8+
9+
import { SwappableAssetV4 } from '../../src/sdk/v4/types';
10+
11+
jest.setTimeout(90 * 1000);
12+
13+
const MAKER_WALLET_ADDRESS = '0xabc23F70Df4F45dD3Df4EC6DA6827CB05853eC9b';
14+
const MAKER_PRIVATE_KEY =
15+
'fc5db508b0a52da8fbcac3ab698088715595f8de9cccf2467d51952eec564ec9';
16+
17+
const TEST_NFT_CONTRACT_ADDRESS = '0xf5de760f2e916647fd766b4ad9e85ff943ce3a2b'; // https://ropsten.etherscan.io/token/0xf5de760f2e916647fd766b4ad9e85ff943ce3a2b?a=0xabc23F70Df4F45dD3Df4EC6DA6827CB05853eC9b
18+
19+
const RPC_TESTNET =
20+
'https://eth-ropsten.alchemyapi.io/v2/is1WqyAFM1nNFFx2aCozhTep7IxHVNGo';
21+
22+
const MAKER_WALLET = new ethers.Wallet(MAKER_PRIVATE_KEY);
23+
24+
const PROVIDER = new ethers.providers.StaticJsonRpcProvider(RPC_TESTNET);
25+
26+
const MAKER_SIGNER = MAKER_WALLET.connect(PROVIDER);
27+
28+
const ROPSTEN_CHAIN_ID = 3;
29+
30+
const ETH_ASSET: SwappableAssetV4 = {
31+
type: 'ERC20',
32+
tokenAddress: ETH_ADDRESS_AS_ERC20,
33+
amount: '420000000000000', // 1 USDC
34+
};
35+
36+
const NFT_ASSET: SwappableAssetV4 = {
37+
type: 'ERC721',
38+
tokenAddress: TEST_NFT_CONTRACT_ADDRESS,
39+
tokenId: '11045',
40+
};
41+
42+
const maxNonce = BigInt(2 ** 256 - 1) + BigInt(1);
43+
44+
const sdkReservedNoncePrefix = '1001';
45+
46+
describe('NFTSwapV4', () => {
47+
it('custom nonce testing', async () => {
48+
// const half = BigInt(2^128 - 1);
49+
const TWO_FIFTY_SIX_BIT_LENGTH = 78;
50+
51+
const appId = '1337';
52+
const v4Nonce = generateRandomV4OrderNonce(appId);
53+
54+
// 256 bit number
55+
const nonceBigInt = BigInt(v4Nonce);
56+
57+
expect(v4Nonce.startsWith(sdkReservedNoncePrefix)).toBe(true);
58+
expect(v4Nonce.length).toEqual(TWO_FIFTY_SIX_BIT_LENGTH);
59+
expect(v4Nonce.substring(4).startsWith(appId)).toBe(true);
60+
expect(nonceBigInt <= maxNonce).toBe(true);
61+
});
62+
63+
it('order with default nonce', async () => {
64+
const defaultAppId = '314159';
65+
const nftSwapperMaker = new NftSwapV4(
66+
MAKER_SIGNER as any,
67+
MAKER_SIGNER,
68+
ROPSTEN_CHAIN_ID
69+
);
70+
71+
const order = nftSwapperMaker.buildOrder(
72+
NFT_ASSET,
73+
ETH_ASSET,
74+
MAKER_WALLET_ADDRESS
75+
);
76+
77+
// 256 bit number
78+
const v4Nonce = order.nonce;
79+
const nonceBigInt = BigInt(order.nonce);
80+
81+
expect(v4Nonce.startsWith(sdkReservedNoncePrefix)).toBe(true);
82+
expect(v4Nonce.length).toEqual(TWO_FIFTY_SIX_BIT_LENGTH);
83+
expect(v4Nonce.substring(4).startsWith(defaultAppId)).toBe(true);
84+
const isWithinMaxBits = nonceBigInt <= maxNonce;
85+
expect(isWithinMaxBits).toBe(true);
86+
});
87+
88+
it('order with custom nonce', async () => {
89+
const customAppId = '696969';
90+
const nftSwapperMaker = new NftSwapV4(
91+
MAKER_SIGNER as any,
92+
MAKER_SIGNER,
93+
ROPSTEN_CHAIN_ID,
94+
{
95+
appId: customAppId,
96+
}
97+
);
98+
99+
const order = nftSwapperMaker.buildOrder(
100+
NFT_ASSET,
101+
ETH_ASSET,
102+
MAKER_WALLET_ADDRESS
103+
);
104+
105+
const v4Nonce = order.nonce;
106+
const nonceBigInt = BigInt(order.nonce);
107+
108+
expect(v4Nonce.startsWith(sdkReservedNoncePrefix)).toBe(true);
109+
expect(v4Nonce.length).toEqual(TWO_FIFTY_SIX_BIT_LENGTH);
110+
expect(v4Nonce.substring(4).startsWith(customAppId)).toBe(true);
111+
expect(nonceBigInt < maxNonce).toBe(true);
112+
});
113+
});

0 commit comments

Comments
 (0)