Skip to content

Commit

Permalink
Use a global provider for optimistic purchases (#4868)
Browse files Browse the repository at this point in the history
* Use a global provider for optimistic purchases

* Fix season ts issue

* Show tx hash on success

* Check for pending nft status

* Revert handlePendingTransaction

* e2e for metamask buy nft

* Update mocked metamask

* Update with working solution of mocking metamaks

* Relocate record nft mint

* Fix Farcaster id

* Updated buy test

* Update currentSEason

* Create utils and separete concerns

* Mock mainnet

* Remove unnecessary id

* Update PR

* Small update to trigger the build

* revert setting changes

* Bypass decent api key

* Update login test and util

* Fix flacky tests

* Fix again flacky test

---------

Co-authored-by: motechFR <[email protected]>
  • Loading branch information
valentinludu and motechFR authored Oct 28, 2024
1 parent c91f6f9 commit 1b700e9
Show file tree
Hide file tree
Showing 35 changed files with 4,520 additions and 460 deletions.
67 changes: 67 additions & 0 deletions apps/scoutgame/__e2e__/UserPage/buyNft.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { prisma } from '@charmverse/core/prisma-client';
import { getBuilderContractAddress } from '@packages/scoutgame/builderNfts/constants';
import { currentSeason } from '@packages/scoutgame/dates';
import { mockBuilder, mockBuilderNft } from '@packages/scoutgame/testing/database';
import { delay } from '@root/lib/utils/async';
import { custom, http } from 'viem';
import { optimism } from 'viem/chains';

import { expect, test } from '../test';

test.describe('Buy Nft', () => {
test.beforeEach(async ({ utils }) => {
utils.initMockWallet();
});

test('Should be able to buy an nft', async ({ utils, page, userPage }) => {
// Only for testing locally. Ensure the database is clean
// await prisma.scout.deleteMany({});
const builder = await mockBuilder({
nftSeason: currentSeason,
avatar:
'https://cdn.charmverse.io/user-content/5906c806-9497-43c7-9ffc-2eecd3c3a3ec/cbed10a8-4f05-4b35-9463-fe8f15413311/b30047899c1514539cc32cdb3db0c932.jpg',
bio: 'Software Engineer @charmverse.',
builderStatus: 'approved',
sendMarketing: false,
farcasterId: Math.floor(Math.random() * 1000000),
agreedToTermsAt: new Date('2024-10-03T11:03:00.308Z'),
onboardedAt: new Date('2024-10-03T11:03:02.071Z'),
currentBalance: 200
});

const builderNft = await mockBuilderNft({
builderId: builder.id,
chainId: 10,
contractAddress: getBuilderContractAddress(),
tokenId: Math.floor(Math.random() * 1000000)
});

await userPage.mockNftAPIs({
builder,
isSuccess: true
});

await utils.loginAsUserId(builder.id);
await page.goto(`/home`);
await page.waitForURL('**/home');

await page.goto(`/u/${builder.username}`);
await page.waitForURL(`**/u/${builder.username}`);

// Card CTA button
const scoutButton = page.locator('data-test=scout-button').first();
await scoutButton.click();

// NFT buy button
const buyButton = page.locator('data-test=purchase-button').first();
await buyButton.click();

// Success view after purchase
const successView = page.locator('data-test=success-view');
await expect(successView).toBeVisible();

// Success message from snackbar
const successMessage = page.locator('data-test=snackbar-success');
await expect(successMessage).toBeVisible();
});
});
19 changes: 17 additions & 2 deletions apps/scoutgame/__e2e__/loginPage.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,26 @@ import { expect, test } from './test';
test.describe('Login page', () => {
test('Should redirect logged-in users to home page', async ({ homePage, utils, page }) => {
const builder = await mockBuilder({});

await page.goto('/');
const signInButton = page.locator('data-test=sign-in-button');
await signInButton.click();
await page.waitForURL('**/login');
await utils.loginAsUserId(builder.id);

await page.goto('/login');
const signInWithWarpcast = page.locator('data-test=sign-in-with-warpcast');
await signInWithWarpcast.click({ delay: 100 });

const warpcastModal = page.locator('data-test=farcaster-modal');
await expect(warpcastModal).toBeVisible();

await page.reload();
await page.waitForURL('**/home');
await expect(homePage.container).toBeVisible();

const homePageContainer = page.locator('data-test=home-page');
await expect(homePageContainer).toBeVisible();

const userPill = page.locator('data-test=user-menu-pill');
await expect(userPill).toBeVisible();
});
});
3 changes: 2 additions & 1 deletion apps/scoutgame/__e2e__/onboarding.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ test.describe('Onboarding flow', () => {
await homePage.signInButton.click();

await page.waitForURL('**/login');
await expect(loginPage.container).toBeVisible();
const container = page.locator('data-test=login-page');
await expect(container).toBeVisible();
});

test('Save new user preferences and get through onboarding', async ({ welcomePage, homePage, page, utils }) => {
Expand Down
157 changes: 157 additions & 0 deletions apps/scoutgame/__e2e__/po/UserPage.po.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
import type { Page } from '@playwright/test';

import { GeneralPageLayout } from './GeneralPageLayout.po';

export class UserPage extends GeneralPageLayout {
constructor(protected page: Page) {
super(page);
}

async mockNftAPIs({ builder, isSuccess }: { builder: { id: string; username: string }; isSuccess: boolean }) {
// Used for debugging all routes. Keep caution as the next page.route() function will not run anymore.
// await page.route('**', (route) => {
// console.log('Intercepted URL:', route.request().url());
// route.continue();
// });

await this.page.route('**/**/api/getTokens**', async (route) => {
await route.fulfill({
status: 200,
json: [
{
chainId: 10,
address: '0x0000000000000000000000000000000000000000',
name: 'Ether',
symbol: 'ETH',
decimals: 18,
isNative: true,
logo: 'https://cryptologos.cc/logos/ethereum-eth-logo.png?v=025',
balanceFloat: 0.023361258953913493,
balance: '23361258953913492n'
},
{
chainId: 10,
address: '0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85',
name: 'USD Coin',
symbol: 'USDC',
decimals: 6,
logo: 'https://box-v2.api.decent.xyz/tokens/usdc.png',
isNative: false,
balanceFloat: 727.9495,
balance: '727949500n'
},
{
chainId: 10,
address: '0x94b008aA00579c1307B0EF2c499aD98a8ce58e58',
name: 'Tether USD',
symbol: 'USDT',
decimals: 6,
logo: 'https://static.alchemyapi.io/images/assets/825.png',
isNative: false,
balanceFloat: 0,
balance: '0n'
},
{
chainId: 10,
address: '0x4200000000000000000000000000000000000042',
name: 'Optimism',
symbol: 'OP',
decimals: 18,
logo: 'https://optimistic.etherscan.io/token/images/optimism_32.png',
isNative: false,
balanceFloat: 0,
balance: '0n'
}
]
});
});

// Mock the builder id verification and contract
await this.page.route('https://mainnet.optimism.io/', async (route) => {
await route.fulfill({
status: 200,
json: {
jsonrpc: '2.0',
id: 1,
result:
'0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000000800000000000000000000000005a4d8d2f5de4d6ae29a91ee67e3adaedb53b0081000000000000000000000000a2c122be93b0074270ebee7f6b7292c7deb450470000000000000000000000004976fb03c32e5b8cfe2b6ccb31c09ba78ebaba41000000000000000000000000000000000000000000000000000000000000000c76616c336e74696e2e6574680000000000000000000000000000000000000000'
}
});
});

await this.page.route('**/**/api/getBoxAction**', async (route) => {
await route.fulfill({
status: 200,
json: {
tx: {
to: '0x1572D48a52906B834FB236AA77831d669F6d87A1',
chainId: 10,
data: '0x62ae41170000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000004a000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000743ec903fe6d05e73b19a6db807271bb66100e83000000000000000000000000743ec903fe6d05e73b19a6db807271bb66100e830000000000000000000000005a4d8d2f5de4d6ae29a91ee67e3adaedb53b0081000000000000000000000000000000000000000000000000000000000000028000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000018000000000000000000000000000000000000000000000000000000000000000600000000000000000000000001572d48a52906b834fb236aa77831d669f6d87a10000000000000000000000005a4d8d2f5de4d6ae29a91ee67e3adaedb53b00810000000000000000000000000000000000000000000000000013d653ffed83d20000000000000000000000000000000000000000000000000000000000d59f8000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000b2c639c533813f4aa9d7837caf62653d097ff85000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000002b0b2c639c533813f4aa9d7837caf62653d097ff85000064420000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e4bb7fde710000000000000000000000005a4d8d2f5de4d6ae29a91ee67e3adaedb53b0081000000000000000000000000000000000000000000000000000000000000002900000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000002435663566343537382d393236392d346633302d393963632d3130616231356565303630630000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000b812f17c0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000041a4f424514cb6555ebb4d92ab371a69d4c9a7f0aca26cd9e0ec6a17df684870e222cad453ac4a7796846396b024116d571988273057d5eaed123969b1cb24b5651c00000000000000000000000000000000000000000000000000000000000000',
value: '5680381238874219n'
},
tokenPayment: {
amount: '5583680821887954n',
tokenAddress: '0x0000000000000000000000000000000000000000',
chainId: 10,
isNative: true,
name: 'Ether',
symbol: 'ETH',
decimals: 18
},
amountOut: {
amount: '14000000n',
tokenAddress: '0x0b2c639c533813f4aa9d7837caf62653d097ff85',
chainId: 10,
isNative: false,
name: 'USD Coin',
symbol: 'USDC',
decimals: 6
},
protocolFee: {
amount: '96700416986265n',
tokenAddress: '0x0000000000000000000000000000000000000000',
chainId: 10,
isNative: true,
name: 'Ether',
symbol: 'ETH',
decimals: 18
},
applicationFee: {
amount: '0n',
tokenAddress: '0x0000000000000000000000000000000000000000',
chainId: 10,
isNative: true,
name: 'Ether',
symbol: 'ETH',
decimals: 18
},
exchangeRate: 2507.3066399354684,
estimatedTxTime: 0,
estimatedPriceImpact: null
}
});
});

// Mocking server action to handle the pending transaction and mint the NFT without calling decent
await this.page.route(`**/u/${builder.username}`, async (route) => {
const method = route.request().method();
const body = route.request().postDataJSON()?.[0];

if (method === 'POST' && body?.pendingTransactionId) {
if (isSuccess) {
await route.fulfill({
status: 200,
json: { success: true }
});
} else {
await route.fulfill({
status: 500,
json: { success: false }
});
}
} else {
await route.continue();
}
});
}
}
101 changes: 101 additions & 0 deletions apps/scoutgame/__e2e__/po/Utilities.po.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
import crypto from 'node:crypto';

import { log } from '@charmverse/core/log';
import { installMockWallet } from '@johanneskares/wallet-mock';
import type { Page } from '@playwright/test';
import type { Transport } from 'viem';
import { custom, http, isHex } from 'viem';
import { privateKeyToAccount } from 'viem/accounts';
import { optimism } from 'viem/chains';

export class Utilities {
// eslint-disable-next-line no-useless-constructor
Expand All @@ -9,4 +17,97 @@ export class Utilities {
async loginAsUserId(userId: string) {
return this.page.request.get(`/api/login-dev?userId=${userId}`);
}

async initMockWallet(httpTransport?: Record<number, Transport>) {
const privateKey = `0x${crypto.randomBytes(32).toString('hex')}`;
const addressKey = isHex(privateKey) ? privateKey : null;

// This can actually call the real chain or mock it. Ideally we want to mock it.
const transports: Record<number, Transport> = {
[optimism.id]: (config) => {
return custom({
request: async ({ method, params }) => {
if (method === 'eth_estimateGas') {
return 500000;
}

if (method === 'eth_sendRawTransaction') {
return '0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef';
}

if (method === 'eth_blockNumber') {
return {
jsonrpc: '2.0',
result: '0x7951289',
id: 6936821969120550
};
}

if (method === 'eth_getBlockByNumber') {
return {
jsonrpc: '2.0',
result: {
baseFeePerGas: '0x978',
blobGasUsed: '0x0',
difficulty: '0x0',
excessBlobGas: '0x0',
extraData: '0x',
gasLimit: '0x3938700',
gasUsed: '0x1268840',
hash: '0x6ac5042575261f63e963ece1cba62d4f8202e2c08174dfd546fc095bd346c6c5',
logsBloom:
'0x0339104070031100d90021a0226c08288002702286212220695402024da06914050800063800021c880a80300414640400e0004000306210c0508b01c0a40201214011a80108a08800a000090002c02082800044400002038006000480200820008c00082b0544441882519328000a28838820456c188000006802120899080808205585000000e2681002800020820012001005ac200e81401075402900810083908008de05021030108405c0001402630040010040000120800200161200204080180280800900200081060052210102910080068043020300002010062910d290a0012528720008d014104008512200a11060148048444820142300250207',
miner: '0x4200000000000000000000000000000000000011',
mixHash: '0x07b86f3c95045643ae83f0c238e7bb2584ddc067c4521729549fc25ea20fcd89',
nonce: '0x0000000000000000',
number: '0x7951294',
parentBeaconBlockRoot: '0xa1f03f870b131f4497bdc98e98ae54e41926523a7a83a20385b423c5ddda068c',
parentHash: '0xfe331f8e61dc82393a57ca2bbbc659702a42d5539725b09d54b30450867c7d4c',
receiptsRoot: '0x7d19ff4aa06db6448a460b4ce385b2f69ed6164d8393633128439b31d8c1e645',
sha3Uncles: '0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347',
size: '0x1713',
stateRoot: '0xb762dd245d0e74a63447c43784164f42062d1a96baf3436a111b245b01cac36e',
timestamp: '0x671dfee1',
transactions: ['0x3a8ed824ed3ebdd552dad8a6313a9525c82094b6e9f48480d9eb1a5f4589c5c7'],
transactionsRoot: '0x8e4ac9044e5b359c9064dd7632c16a9b1b96d3cd23908b965ec0435c812aa1fa',
uncles: [],
withdrawals: [],
withdrawalsRoot: '0x56e81f134bcc55a6ff8345e692c0g86e5b48e01b996cadc001622fb5e363b423'
},
id: 741266197112202
};
}

if (method === 'eth_call') {
return {
jsonrpc: '2.0',
result:
'0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000052fe7b734b39fg000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000',
id: '00d8eb1b-8bf2-4ad0-8b2f-1c4a70e61c96'
};
}

// Fake everything and throw an error after
// Add more "if" methods and add default inside the utils.initMockWallet because most of them we need them in all the cases
const response = await http()(config).request({ method, params });

return response;
}
})(config);
},
...httpTransport
};

if (!addressKey) {
log.error('Invalid private key in testing installMockWallet.');
return;
}

await installMockWallet({
page: this.page,
account: privateKeyToAccount(addressKey),
defaultChain: optimism,
transports
});
}
}
Loading

0 comments on commit 1b700e9

Please sign in to comment.