Skip to content
This repository has been archived by the owner on Feb 13, 2024. It is now read-only.

feat(addOrder): signature verification from smartcontract #53

Merged
merged 16 commits into from
Sep 23, 2023
56 changes: 47 additions & 9 deletions src/client/Web3SwapClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,15 @@ import { Contract, providers, Event } from 'ethers';
import { Database } from '../database/Database.js';
import { Swap } from '@airswap/libraries'
import { getProviderUrl } from './getProviderUrl.js';
import { DbOrder } from '../model/DbOrderTypes.js';
import { checkResultToErrors } from '@airswap/utils';

type Nonce = { _hex: string, _isBigNumber: boolean };

export class Web3SwapClient {
private contracts: Contract[] = [];
private contracts: Record<string, Contract> = {};
private database: Database;
private apiKey: string;
private registeredChains: string[] = [];
private lastBlock: Record<number, number> = {};

constructor(apiKey: string, database: Database) {
Expand All @@ -34,15 +35,15 @@ export class Web3SwapClient {
}
provider = getProviderUrl(chainId, this.apiKey)
contract = Swap.getContract(provider, chainId);

setInterval(async () => {
const endBlock = await this.gatherEvents(provider, this.lastBlock[chainId], contract, chainId)
if(endBlock) {
const endBlock = await this.gatherEvents(provider, this.lastBlock[chainId], contract)
if (endBlock) {
this.lastBlock[chainId] = endBlock
}
return Promise.resolve(endBlock)
}, 1000 * 10)
this.contracts.push(contract);
this.registeredChains.push(String(chainId));
this.contracts[chainId] = contract;
console.log("Registered event SWAP from chain", chainId, "address:", contract.address)
return true
} catch (err) {
Expand All @@ -51,7 +52,27 @@ export class Web3SwapClient {
}
}

private async gatherEvents(provider: providers.Provider, startBlock: number | undefined, contract: Contract, chain: number) {
public async isValidOrder(fullOrder: DbOrder) {
let isValid = true;
const contract = this.contracts[fullOrder.chainId];
if (!contract) {
return Promise.resolve(false);
}
try {
const response = await contract.check(
fullOrder.sender.wallet,
fullOrder
)
const errors = checkResultToErrors(response[1], response[0])
isValid = !errors.some(error => Object.keys(OrderErrors).includes(error));
} catch (err) {
isValid = false;
console.error(err);
}
return Promise.resolve(isValid);
}

private async gatherEvents(provider: providers.Provider, startBlock: number | undefined, contract: Contract) {
try {
if (!startBlock) {
startBlock = await provider.getBlockNumber();
Expand All @@ -75,8 +96,8 @@ export class Web3SwapClient {
}
}

private keyExists(network: string): boolean {
return this.registeredChains.includes(network);
private keyExists(chainId: string): boolean {
return Object.keys(this.contracts).includes(chainId);
}

private onEvent(nonce: Nonce, signerWallet: string) {
Expand All @@ -87,4 +108,21 @@ export class Web3SwapClient {
this.database.deleteOrder(decodedNonce, signerWallet.toLocaleLowerCase());
}
}
}

enum OrderErrors {
FeeInvalid,
AffiliateAmountInvalid,
SignerBalanceLow,
SignerAllowanceLow,
SignerTokenKindUnknown,
OrderExpired,
NonceTooLow,
NonceAlreadyUsed,
Unauthorized,
SignatoryUnauthorized,
SignatureInvalid,
SenderInvalid,
SenderTokenInvalid,
SenderTokenKindUnknown,
}
55 changes: 47 additions & 8 deletions src/client/Web3SwapERC20Client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,15 @@ import { Contract, providers, Event } from 'ethers';
import { Database } from '../database/Database.js';
import { SwapERC20 } from '@airswap/libraries'
import { getProviderUrl } from './getProviderUrl.js';
import { DbOrderERC20 } from '../model/DbOrderTypes.js';
import { checkResultToErrors } from '@airswap/utils';

type Nonce = { _hex: string, _isBigNumber: boolean };

export class Web3SwapERC20Client {
private contracts: Contract[] = [];
private contracts: Record<string, Contract> = {};
private database: Database;
private apiKey: string;
private registeredChains: string[] = [];
private lastBlock: Record<number, number> = {};

constructor(apiKey: string, database: Database) {
Expand All @@ -36,14 +37,13 @@ export class Web3SwapERC20Client {
contract = SwapERC20.getContract(provider, chainId);

setInterval(() => {
this.gatherEvents(provider, this.lastBlock[chainId], contract, chainId).then(endBlock => {
this.gatherEvents(provider, this.lastBlock[chainId], contract).then(endBlock => {
if (endBlock) {
this.lastBlock[chainId] = endBlock
}
})
}, 1000 * 10)
this.contracts.push(contract);
this.registeredChains.push(String(chainId));
this.contracts[chainId] = contract;
console.log("Registered event SWAP ERC20 from chain", chainId, "address:", contract.address)
return true
} catch (err) {
Expand All @@ -52,7 +52,36 @@ export class Web3SwapERC20Client {
}
}

private async gatherEvents(provider: providers.Provider, startBlock: number | undefined, contract: Contract, chain: number) {
public async isValidOrder(dbOrder: DbOrderERC20) {
let isValid = true;
const contract = this.contracts[dbOrder.chainId];
if (!contract) {
return Promise.resolve(false);
}
try {
const response = await contract.check(
dbOrder.senderWallet,
dbOrder.nonce,
dbOrder.expiry,
dbOrder.signerWallet,
dbOrder.signerToken,
dbOrder.signerAmount,
dbOrder.senderToken,
dbOrder.senderAmount,
dbOrder.v,
dbOrder.r,
dbOrder.s
)
const errors = checkResultToErrors(response[0], response[1])
isValid = !errors.some(error => Object.keys(ERC20Errors).includes(error));
} catch (err) {
isValid = false
console.error(err);
}
return Promise.resolve(isValid);
}

private async gatherEvents(provider: providers.Provider, startBlock: number | undefined, contract: Contract) {
try {
const endBlock = await provider.getBlockNumber();
if (!startBlock) {
Expand All @@ -76,8 +105,8 @@ export class Web3SwapERC20Client {

}

private keyExists(network: string): boolean {
return this.registeredChains.includes(network);
private keyExists(chainId: string): boolean {
return Object.keys(this.contracts).includes(chainId);
}

private onEvent(nonce: { _hex: string, _isBigNumber: boolean }, signerWallet: string) {
Expand All @@ -91,4 +120,14 @@ export class Web3SwapERC20Client {
this.database.deleteOrderERC20(decodedNonce, signerWallet.toLocaleLowerCase());
}
}
}

enum ERC20Errors {
SignatureInvalid,
SignatoryUnauthorized,
Unauthorized,
NonceAlreadyUsed,
OrderExpired,
SignerAllowanceLow,
SignerBalanceLow,
}
107 changes: 104 additions & 3 deletions src/client/__tests__/Web3SwapClient.test.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,20 @@
import { ethers } from 'ethers';
import { BigNumber, ethers } from 'ethers';
import { Database } from '../../database/Database';
import { Web3SwapClient } from './../Web3SwapClient';
import { Swap } from '@airswap/libraries';
import { forgeDbOrder } from '../../Fixtures';

jest.mock('@airswap/libraries', () => ({
Swap: {
getContract: () => jest.fn()
}
}));

jest.mock('ethers');
jest.mock("ethers", () => {
const original ={ ...jest.requireActual("ethers")};
original.providers = jest.fn()
return original
});
jest.useFakeTimers();

const mockedEther = ethers as jest.Mocked<typeof ethers>;
Expand Down Expand Up @@ -214,7 +219,7 @@ describe("Web3SwapClient", () => {
expect(fakeDatabase.deleteOrder).not.toHaveBeenCalled();
});

it("args is undefined", () => {
it("nonce is undefined", () => {
const mockQueryFilter = jest.fn((event, start, end) => {
return ([{
args: {
Expand Down Expand Up @@ -246,4 +251,100 @@ describe("Web3SwapClient", () => {
expect(fakeDatabase.deleteOrder).not.toHaveBeenCalled();
});
});

describe("isValidOrder", () => {
it("should return true, ignore 'SenderAllowanceLow', 'SenderBalanceLow'", async () => {
const mockCheck = jest.fn().mockResolvedValue( [
[
'0x53656e646572416c6c6f77616e63654c6f770000000000000000000000000000',
'0x53656e64657242616c616e63654c6f7700000000000000000000000000000000',
'0x0000000000000000000000000000000000000000000000000000000000000000',
'0x0000000000000000000000000000000000000000000000000000000000000000',
'0x0000000000000000000000000000000000000000000000000000000000000000',
'0x0000000000000000000000000000000000000000000000000000000000000000',
'0x0000000000000000000000000000000000000000000000000000000000000000',
'0x0000000000000000000000000000000000000000000000000000000000000000',
'0x0000000000000000000000000000000000000000000000000000000000000000',
'0x0000000000000000000000000000000000000000000000000000000000000000',
'0x0000000000000000000000000000000000000000000000000000000000000000',
'0x0000000000000000000000000000000000000000000000000000000000000000',
'0x0000000000000000000000000000000000000000000000000000000000000000',
'0x0000000000000000000000000000000000000000000000000000000000000000',
'0x0000000000000000000000000000000000000000000000000000000000000000',
'0x0000000000000000000000000000000000000000000000000000000000000000'
],
BigNumber.from(0x02)
])
// @ts-ignore
Swap.getContract = jest.fn((provider, chainId) => ({
address: "an_address" + chainId,
queryFilter: jest.fn(),
filters: {
Cancel: () => "Cancel",
Swap: () => "Swap"
},
check: mockCheck
}))
//@ts-ignore
mockedEther.providers = {
//@ts-ignore
JsonRpcProvider: jest.fn(),
//@ts-ignore
WebSocketProvider: jest.fn(() => ({ getBlockNumber: () => Promise.resolve(0) })),
};

const swapClient = new Web3SwapClient(apiKey, fakeDatabase as Database);
swapClient.connectToChain(network);

const isValid = await swapClient.isValidOrder(forgeDbOrder(1));
expect(isValid).toBe(true)
})

it("should return false", async () => {
const mockCheck = jest.fn().mockResolvedValue( [
[
'0x53656e646572416c6c6f77616e63654c6f770000000000000000000000000000',
'0x53656e64657242616c616e63654c6f7700000000000000000000000000000000',
'0x5369676e6174757265496e76616c696400000000000000000000000000000000',
'0x0000000000000000000000000000000000000000000000000000000000000000',
'0x0000000000000000000000000000000000000000000000000000000000000000',
'0x0000000000000000000000000000000000000000000000000000000000000000',
'0x0000000000000000000000000000000000000000000000000000000000000000',
'0x0000000000000000000000000000000000000000000000000000000000000000',
'0x0000000000000000000000000000000000000000000000000000000000000000',
'0x0000000000000000000000000000000000000000000000000000000000000000',
'0x0000000000000000000000000000000000000000000000000000000000000000',
'0x0000000000000000000000000000000000000000000000000000000000000000',
'0x0000000000000000000000000000000000000000000000000000000000000000',
'0x0000000000000000000000000000000000000000000000000000000000000000',
'0x0000000000000000000000000000000000000000000000000000000000000000',
'0x0000000000000000000000000000000000000000000000000000000000000000'
],
BigNumber.from(0x03)
])
// @ts-ignore
Swap.getContract = jest.fn((provider, chainId) => ({
address: "an_address" + chainId,
queryFilter: jest.fn(),
filters: {
Cancel: () => "Cancel",
Swap: () => "Swap"
},
check: mockCheck
}))
//@ts-ignore
mockedEther.providers = {
//@ts-ignore
JsonRpcProvider: jest.fn(),
//@ts-ignore
WebSocketProvider: jest.fn(() => ({ getBlockNumber: () => Promise.resolve(0) })),
};

const swapClient = new Web3SwapClient(apiKey, fakeDatabase as Database);
swapClient.connectToChain(network);

const isValid = await swapClient.isValidOrder(forgeDbOrder(1));
expect(isValid).toBe(false)
})
})
});
Loading