Skip to content

Commit

Permalink
Merge branch 'development' of https://github.com/ubiquity/pay.ubq.fi
Browse files Browse the repository at this point in the history
…into preview-deployer-bug
  • Loading branch information
zugdev committed Oct 15, 2024
2 parents 83d43ce + 84c49d8 commit b0b3cf5
Show file tree
Hide file tree
Showing 66 changed files with 14,858 additions and 8,477 deletions.
7 changes: 7 additions & 0 deletions .cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"useGitignore": true,
"language": "en",
"words": [
"adblocker",
"binkey",
"binsec",
"blockscan",
Expand All @@ -15,6 +16,8 @@
"devpool",
"ethersproject",
"fract",
"giftcards",
"gnosischain",
"funder",
"Funder",
"gnosisscan",
Expand All @@ -29,9 +32,13 @@
"Numberish",
"outdir",
"outfile",
"pageable",
"ress",
"Reloadly",
"Rpcs",
"scalarmult",
"servedir",
"skus",
"solmate",
"sonarjs",
"SUPABASE",
Expand Down
4 changes: 2 additions & 2 deletions .github/knip.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import type { KnipConfig } from "knip";

const config: KnipConfig = {
entry: ["build/esbuild-build.ts", "static/scripts/rewards/init.ts"],
project: ["src/**/*.ts", "static/scripts/**/*.ts"],
entry: ["build/esbuild-build.ts", "static/scripts/rewards/init.ts", "static/scripts/ubiquity-dollar/init.ts", "static/scripts/shared/api.ts"],
project: ["src/**/*.ts", "static/scripts/**/*.ts", "shared/**/api-types.ts"],
ignore: ["src/types/config.ts", "**/__mocks__/**", "**/__fixtures__/**", "lib/**/*"],
ignoreExportsUsedInFile: true,
// eslint can also be safely ignored as per the docs: https://knip.dev/guides/handling-issues#eslint--jest
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,5 +37,7 @@ jobs:
name: full-stack-app
path: |
static
functions
shared
package.json
yarn.lock
9 changes: 6 additions & 3 deletions .github/workflows/cypress-testing.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,14 +38,17 @@ jobs:
sleep 1
done || exit 1
- name: Fund test accounts
run: yarn test:fund
- name: Start Cloudflare Wrangler
run: npx wrangler pages dev static --port 8080 --binding USE_RELOADLY_SANDBOX=true RELOADLY_API_CLIENT_ID="$RELOADLY_SANDBOX_API_CLIENT_ID" RELOADLY_API_CLIENT_SECRET="$RELOADLY_SANDBOX_API_CLIENT_SECRET" &
env:
RELOADLY_SANDBOX_API_CLIENT_ID: ${{ secrets.RELOADLY_SANDBOX_API_CLIENT_ID }}
RELOADLY_SANDBOX_API_CLIENT_SECRET: ${{ secrets.RELOADLY_SANDBOX_API_CLIENT_SECRET }}

- name: Cypress run
uses: cypress-io/github-action@v6
with:
build: yarn run build
start: yarn start
start: yarn test:fund

env:
SUPABASE_URL: "https://wfzpewmlyiozupulbuur.supabase.co"
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,6 @@ static/bundles

cypress/screenshots
cypress/videos
.wrangler
coverage
junit.xml
37 changes: 33 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,23 +23,39 @@ A vanilla Typescript dApp for claiming Ubiquity Rewards. It also includes tools
SUPABASE_ANON_KEY="...." # used for storing permit tx data
# Variables depending on spender (bounty hunter)
AMOUNT_IN_ETH="1"
AMOUNT_IN_ETH="50"
BENEFICIARY_ADDRESS="0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"
# Legacy env vars (only used when invalidating **REAL** permits via /scripts/solidity/getInvalidateNonceParams.ts)
NONCE="0"
NONCE_SIGNER_ADDRESS="0x"
```

3. Update values for wrangler variables to use Reloadly sandbox or production API in the `wrangler.toml` file.

```
[vars]
USE_RELOADLY_SANDBOX = "true"
RELOADLY_API_CLIENT_ID = "xxxxxxxxxxxxxxxxxx"
RELOADLY_API_CLIENT_SECRET = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
```

## Local Testing

1. Set `.env` variables.
2. Run `yarn test:anvil` in terminal A and `yarn test:fund` in terminal B.
3. In terminal B, run `yarn start`.
2. Run `yarn`
3. Run `yarn test:anvil` in terminal A and `yarn test:fund` in terminal B.
4. In terminal B, run

```
yarn build
yarn start
```

4. A permit URL for both ERC20 and ERC721 will be generated.
5. Open the generated permit URL from the console.
6. Connect your wallet (import anvil accounts [0] & [1] into your wallet).
7. Depending on your connected account, either the claim or invalidate button will be visible.
7. Depending on your connected account, either the claim or invalidate button will be visible. The virtual card section will also display an available virtual card.
8. To test ERC721 permits, deploy the `nft-rewards` contract from the [repository](https://github.com/ubiquity/nft-rewards).

### Importing Anvil Accounts
Expand Down Expand Up @@ -80,6 +96,19 @@ A vanilla Typescript dApp for claiming Ubiquity Rewards. It also includes tools
- Ensure `.env` is correctly configured and wallet provider network is correct if `Allowance` or `Balance` is `0.00`.
- Always start the Anvil instance before using `yarn start` as permit generation requires an on-chain call to `token.decimals()`.

### Troubleshooting virtual cards

Virtual cards are subject to regulations and are not available for all countries. Moreover, each virtual card is available for specific amounts. If you are unable to see an available virtual card it is either because of your location or the amount of your permit.

If you are not getting an available card, you can perform a few extra steps to get a virtual card for testing purposes. You can set the permit amount `AMOUNT_IN_ETH` to be 50 WXDAI in the `.env` file and mock your location as United States. To set your location to United States, you can follow one of the steps given below:

- Use a USA VPN
- Set your timezone to `Eastern Time (ET) New York` and block the ajax request to `https://ipinfo.io/json` so that your timezone is used to detect your location.

One of these steps should get you a virtual card to try both on Reloadly sandbox and production. Please note that if you are minting a virtual card with a mock location on Reloadly production, you will get a redeem code but you may not able to use the card due to restrictions on the card, and there is no refund or replacement. Use your real location if you want to use the virtual card.

If you are using mainnet with your local environments, you may want to change the `giftCardTreasuryAddress` to a wallet that you own in the file `shared/constants.ts`. It is the wallet where payments for the virtual cards are sent.

## How to generate a permit2 URL using the script

1. Admin sets `env.AMOUNT_IN_ETH` and `env.BENEFICIARY_ADDRESS` depending on a bounty hunter's reward and address
Expand Down
10 changes: 6 additions & 4 deletions build/esbuild-build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,16 @@ const cssFiles: string[] = [
"static/styles/rewards/background.css",
"static/styles/toast.css",
"static/styles/rewards/claim-table.css",
"static/styles/rewards/gift-cards.css",
"static/styles/rewards/ubiquity-dollar.css",
"static/styles/rewards/media-queries.css",
"static/styles/rewards/light-mode.css",
];

// Output bundles file
const outputFilePath = "static/bundles/bundles.css";
const outputFilePath = "static/out/bundles.css";

const typescriptEntries = ["static/scripts/rewards/init.ts"];
const typescriptEntries = ["static/scripts/rewards/init.ts", "static/scripts/ubiquity-dollar/init.ts"];
export const entries = [...typescriptEntries];

export const esBuildContext: esbuild.BuildOptions = {
Expand All @@ -32,8 +34,8 @@ export const esBuildContext: esbuild.BuildOptions = {
".ttf": "dataurl",
".svg": "dataurl",
},
outfile: "static/bundles/bundles.js",
entryNames: "bundles", // Ensure the CSS is named bundles.css
outdir: "static/out",
entryNames: "[dir]", // Ensure the CSS is named bundles.css
define: createEnvDefines(["SUPABASE_URL", "SUPABASE_ANON_KEY"], {
commitHash: execSync(`git rev-parse --short HEAD`).toString().trim(),
}),
Expand Down
3 changes: 3 additions & 0 deletions cypress.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ export default defineConfig({
},
baseUrl: "http://localhost:8080",
experimentalStudio: true,
env: {
permitConfig: { ...process.env },
},
},
viewportHeight: 900,
viewportWidth: 1440,
Expand Down
200 changes: 200 additions & 0 deletions cypress/e2e/claim-gift-card.cy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
/* eslint-disable sonarjs/no-duplicate-string */
import { JsonRpcProvider, JsonRpcSigner } from "@ethersproject/providers";
import { Wallet } from "ethers";
import { PermitConfig, generateErc20Permit } from "../../scripts/typescript/generate-erc20-permit-url";

const beneficiary = "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"; // anvil
const SENDER_PRIVATE_KEY = "0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d"; // anvil

describe("Gift Cards", () => {
beforeEach(() => {
cy.clearAllCookies();
cy.clearAllLocalStorage();
cy.clearAllSessionStorage();
setupStubs();

setupIntercepts();
});

it.only("should show redeem info", () => {
const permitConfig = Cypress.env("permitConfig");
void cy.getPermitUrl(permitConfig).then((permitUrl) => {
cy.visit(`${permitUrl as string}`);

cy.wait("@getBestCard");
cy.wait(2000);

cy.get("#gift-cards").should("exist").and("include.text", "Or mint a virtual visa/mastercard");
cy.get(".card-section").should("have.length.above", 0);
cy.get(".redeem-info").should("exist");
cy.get(".redeem-info").eq(0).should("include.text", "How to use redeem code?");
});
});

it("should claim a gift card", () => {
const permitConfig = Cypress.env("permitConfig");

const customPermitConfig = { ...permitConfig, AMOUNT_IN_ETH: "30.0" };

void cy.getPermitUrl(customPermitConfig).then((permitUrl) => {
cy.visit(permitUrl);
cy.wait(2000);

cy.wait("@getBestCard");
cy.get(".card-section").should("have.length.above", 0);
cy.get("#offered-card").should("exist");
cy.get("#offered-card .details h3").then(($name) => {
const giftCardName = $name;
cy.wrap(giftCardName).as("giftCardName");
});

cy.intercept({ method: "POST", url: "/post-order?country=US" }).as("postOrder");

cy.get("#offered-card .details #mint").should("exist");
cy.intercept({ method: "GET", url: "/get-order**" }).as("getOrder");

cy.get("#offered-card .details #mint").invoke("click");

cy.get(".notifications", { timeout: 10000 }).should("contain.text", "Processing... Please wait. Do not close this page.");
cy.get(".notifications", { timeout: 10000 }).should("contain.text", "Transaction confirmed. Minting your card now.");
cy.wait("@getOrder", { timeout: 10000 });

cy.get("#gift-cards").should("exist").and("include.text", "Your virtual visa/mastercard");

cy.get("#redeem-code").should("exist");
cy.get("@giftCardName").then((name) => {
cy.get("#offered-card .details h3")
.eq(0)
.should("have.text", name.text() as string);
});
});
});

it("should reveal a redeem code after claim", () => {
cy.visit(
"http://localhost:8080/?claim=W3sidHlwZSI6ImVyYzIwLXBlcm1pdCIsInBlcm1pdCI6eyJwZXJtaXR0ZWQiOnsidG9rZW4iOiIweGU5MUQxNTNFMGI0MTUxOEEyQ2U4RGQzRDc5NDRGYTg2MzQ2M2E5N2QiLCJhbW91bnQiOiIzMDAwMDAwMDAwMDAwMDAwMDAwMCJ9LCJub25jZSI6IjczMDU2NzU0MjU1ODU4ODMxMzQ0NTMzNDgxMDc0Njg5NTE1ODEyNzIzNDE5NTkwNjMwOTY2MTUwOTIxNzk3ODEzMzExMDE4NjgyMDMzIiwiZGVhZGxpbmUiOiIxMTU3OTIwODkyMzczMTYxOTU0MjM1NzA5ODUwMDg2ODc5MDc4NTMyNjk5ODQ2NjU2NDA1NjQwMzk0NTc1ODQwMDc5MTMxMjk2Mzk5MzUifSwidHJhbnNmZXJEZXRhaWxzIjp7InRvIjoiMHhmMzlGZDZlNTFhYWQ4OEY2RjRjZTZhQjg4MjcyNzljZmZGYjkyMjY2IiwicmVxdWVzdGVkQW1vdW50IjoiMzAwMDAwMDAwMDAwMDAwMDAwMDAifSwib3duZXIiOiIweDcwOTk3OTcwQzUxODEyZGMzQTAxMEM3ZDAxYjUwZTBkMTdkYzc5QzgiLCJzaWduYXR1cmUiOiIweDdkYWYxMTNhNTA0ZjYxYzk5MDg0ZGM2ZGFlZTZkZDFkZjhhM2I4YjM5ZTU0N2VkYWIxMjNhNzQxNjBhNWVhNDYwZDgyODdmYWM1MDlhYTc5M2ZhNjc5M2RlOTg5YmVhOTg4Y2M3NDAyNGE5ZmQyNjAyMjY2YTQzZjg1MDlhYTJkMWIiLCJuZXR3b3JrSWQiOjMxMzM3fSx7InR5cGUiOiJlcmMyMC1wZXJtaXQiLCJwZXJtaXQiOnsicGVybWl0dGVkIjp7InRva2VuIjoiMHhlOTFEMTUzRTBiNDE1MThBMkNlOERkM0Q3OTQ0RmE4NjM0NjNhOTdkIiwiYW1vdW50IjoiOTAwMDAwMDAwMDAwMDAwMDAwMCJ9LCJub25jZSI6IjYyOTc2MjY4MDU3NjQ1MTA0ODc3MTI4NDU3MTU1NDgwNTU5NzU1OTQwMjA4MzExMDQ3Mjc1Njc2NjAyNDI3NzQwODY1NzE0MDkxMzAwIiwiZGVhZGxpbmUiOiIxMTU3OTIwODkyMzczMTYxOTU0MjM1NzA5ODUwMDg2ODc5MDc4NTMyNjk5ODQ2NjU2NDA1NjQwMzk0NTc1ODQwMDc5MTMxMjk2Mzk5MzUifSwidHJhbnNmZXJEZXRhaWxzIjp7InRvIjoiMHhmMzlGZDZlNTFhYWQ4OEY2RjRjZTZhQjg4MjcyNzljZmZGYjkyMjY2IiwicmVxdWVzdGVkQW1vdW50IjoiOTAwMDAwMDAwMDAwMDAwMDAwMCJ9LCJvd25lciI6IjB4NzA5OTc5NzBDNTE4MTJkYzNBMDEwQzdkMDFiNTBlMGQxN2RjNzlDOCIsInNpZ25hdHVyZSI6IjB4N2RhZjExM2E1MDRmNjFjOTkwODRkYzZkYWVlNmRkMWRmOGEzYjhiMzllNTQ3ZWRhYjEyM2E3NDE2MGE1ZWE0NjBkODI4N2ZhYzUwOWFhNzkzZmE2NzkzZGU5ODliZWE5ODhjYzc0MDI0YTlmZDI2MDIyNjZhNDNmODUwOWFhMmQxYiIsIm5ldHdvcmtJZCI6MzEzMzd9XQ=="
);
cy.wait(2000);

cy.wait("@getBestCard");

cy.get("#gift-cards").should("exist").and("include.text", "Your virtual visa/mastercard");
cy.get("#redeem-code > h3").eq(0).should("have.text", "Redeem code");
cy.get("#redeem-code > p").eq(0).should("have.text", "xxxxxxxxxxxx");
cy.get("#redeem-code > p").eq(1).should("have.text", "xxxxxxxxxxxx");
cy.get("#redeem-code > p").eq(2).should("have.text", "xxxxxxxxxxxx");
cy.get("#redeem-code > #reveal").invoke("click");

cy.get("#redeem-code > h3").eq(0).should("have.text", "Redeem code");
cy.get("#redeem-code > p").should("exist");
cy.get("#redeem-code > p").eq(0).should("not.have.text", "xxxxxxxxxxxx");
});
});

function setupStubs() {
const provider = new JsonRpcProvider("http://localhost:8545");
const signer = provider.getSigner(beneficiary);
const wallet = new Wallet(SENDER_PRIVATE_KEY, provider);

signer.signMessage = cy.stub().callsFake(async () => {
return "0x4d9f92f69898fd112748ff04c98e294cced4dbde80ac3cba42fb546538bf54ca0e3fbc3f94416813f8da58a4b26957b62bae66c48bf01ca1068af0f222bf18df1c";
});
stubEthereum(signer);

return { provider, signer, wallet };
}

function setupIntercepts() {
cy.intercept("POST", "*", (req) => {
// capturing the RPC optimization calls
if (req.body.method === "eth_getBlockByNumber") {
req.reply({
statusCode: 200,
body: cy.fixture("eth_getBlockByNumber.json"),
});
}
});

cy.intercept("POST", "https://wfzpewmlyiozupulbuur.supabase.co/rest/v1/*", {
statusCode: 200,
body: {},
});
cy.intercept("PATCH", "https://wfzpewmlyiozupulbuur.supabase.co/rest/v1/*", {
statusCode: 200,
body: {},
});
cy.intercept("GET", "https://wfzpewmlyiozupulbuur.supabase.co/rest/v1/*", {
statusCode: 200,
body: {},
});

cy.intercept({ method: "GET", url: "/get-best-card?country=US**" }).as("getBestCard");
cy.intercept("GET", "https://ipinfo.io/json", {
statusCode: 200,
body: {
ip: "192.158.1.38",
hostname: "example.com",
city: "Los Angeles",
region: "California",
country: "US",
loc: "34.0522,-118.2437",
org: "Example org",
postal: "90009",
timezone: "America/Los_Angeles",
readme: "https://ipinfo.io/missingauth",
},
});
}

function stubEthereum(signer: JsonRpcSigner) {
// Stubbing the ethereum object
cy.on("window:before:load", (win) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
((win as any).ethereum = {
isMetaMask: true,
enable: cy.stub().resolves(["0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"]),
request: cy.stub().callsFake(async (method) => providerFunctions(method)),
on: cy.stub().callsFake((event, cb) => {
if (event === "accountsChanged") {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(win as any).ethereum.onAccountsChanged = cb;
}
}),
autoRefreshOnNetworkChange: false,
chainId: "0x7a69",
selectedAddress: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
requestAccounts: cy.stub().resolves(["0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"]),
send: cy.stub().callsFake(async (method) => providerFunctions(method)),
getSigner: () => signer,
}),
// eslint-disable-next-line @typescript-eslint/no-explicit-any
((win as any).signer = signer);
});
}

function providerFunctions(method: string) {
switch (method) {
case "eth_requestAccounts":
return ["0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"];
case "wallet_sendDomainMetadata":
return true;
case "wallet_addEthereumChain":
return true;
case "wallet_switchEthereumChain":
return true;
case "wallet_watchAsset":
return true;
case "eth_chainId":
return "0x7a69";
case "eth_accounts":
return ["0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"];
case "eth_signTypedData_v4":
return "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266";
case "eth_estimateGas":
return "0x7a69";
}
}

Cypress.Commands.add("getPermitUrl", (customPermitConfig: PermitConfig) => {
return generateErc20Permit(customPermitConfig);
});
2 changes: 1 addition & 1 deletion cypress/e2e/claim-portal-success.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ describe("Claims Portal Success", () => {
it("should successfully claim a permit", () => {
cy.get("#additionalDetails", { timeout: 15000 }).should("be.visible").invoke("click");

cy.get('table[data-make-claim="ok"]').should("exist").and("include.text", "337888.4 WXDAI");
cy.get('table[data-make-claim="ok"]').should("exist");

cy.get("button[id='make-claim']").invoke("click");

Expand Down
7 changes: 7 additions & 0 deletions cypress/e2e/index.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/// <reference types="cypress" />

declare namespace Cypress {
interface Chainable {
getPermitUrl(permitConfig: PermitConfig): Promise<string>;
}
}
Loading

0 comments on commit b0b3cf5

Please sign in to comment.