diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..72399e6 --- /dev/null +++ b/.env.example @@ -0,0 +1,3 @@ +RPC_URL=http://127.0.0.1:5050 +ADDRESS=0x000000000000000000000000000000000000000000000000000000000000000 +PRIVATE_KEY=0x000000000000000000000000000000000000000000000000000000000000000 \ No newline at end of file diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..bd6285a --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,12 @@ +{ + "parser": "@typescript-eslint/parser", + "parserOptions": { + "ecmaVersion": "latest", + "sourceType": "module" + }, + "extends": ["plugin:@typescript-eslint/recommended"], + "env": { + "node": true + }, + "ignorePatterns": ["dist", "cairo"] +} diff --git a/.github/workflows/cairo-ci.yml b/.github/workflows/cairo-ci.yml new file mode 100644 index 0000000..0a283f4 --- /dev/null +++ b/.github/workflows/cairo-ci.yml @@ -0,0 +1,26 @@ +name: Cairo CI + +on: push + +jobs: + tests: + runs-on: ubuntu-latest + steps: + - name: Step 1 - Check out main branch + uses: actions/checkout@v3 + - name: Step 2 - Getting scarb + uses: software-mansion/setup-scarb@v1.3.2 + - name: Step 3 - Setting up snfoundry + uses: foundry-rs/setup-snfoundry@v3 + - name: Step 4 - Running tests + run: scarb test + + format: + runs-on: ubuntu-latest + steps: + - name: Step 1 - Check out main branch + uses: actions/checkout@v3 + - name: Step 2 - Getting scarb + uses: software-mansion/setup-scarb@v1.3.2 + - name: Step 3 - Checking format + run: scarb fmt --check diff --git a/.github/workflows/integration-ci.yml b/.github/workflows/integration-ci.yml new file mode 100644 index 0000000..c92d17f --- /dev/null +++ b/.github/workflows/integration-ci.yml @@ -0,0 +1,64 @@ +name: Integration CI + +on: push + +jobs: + integration-tests: + runs-on: ubuntu-latest + steps: + - name: Check out main branch + uses: actions/checkout@v3 + + - name: Setup Scarb + uses: software-mansion/setup-scarb@v1.3.2 + + - name: Install project + run: yarn install --frozen-lockfile + + - name: Start devnet in background + run: scarb run start-devnet + + - name: Run integration tests + run: scarb --release build && tsc && yarn mocha tests-integration/*.test.ts --forbid-only --forbid-pending + + format: + runs-on: ubuntu-latest + steps: + - name: Step 1 - Check out main branch + uses: actions/checkout@v3 + + - name: Step 2 - Install project + run: yarn install --frozen-lockfile + + - name: Step 3 - Check correct formatting + run: yarn prettier --check . + + lint: + runs-on: ubuntu-latest + steps: + - name: Step 1 - Check out main branch + uses: actions/checkout@v3 + + - name: Step 2 - Install project + run: yarn install --frozen-lockfile + + - name: Step 3 - Check correct linting + run: yarn eslint . + + gas-report: + runs-on: ubuntu-latest + steps: + - name: Check out main branch + uses: actions/checkout@v3 + + - name: Setup Scarb + uses: software-mansion/setup-scarb@v1.3.2 + + - name: Install project + run: yarn install --frozen-lockfile + + - name: Start devnet in background + run: scarb run start-devnet + + - name: Gas report + run: scarb run profile --check \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b45c9b4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,46 @@ +# Logs +logs +*.log +npm-debug.log* + +# Runtime data +pids +*.pid +*.seed + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (http://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules +jspm_packages + +# Optional npm cache directory +.npm + +# Optional REPL history +.node_repl_history +.next + +# Cairo +target +.snfoundry_cache/ + +.env +dist +.DS_STORE diff --git a/.mocharc.json b/.mocharc.json new file mode 100644 index 0000000..f8e85f3 --- /dev/null +++ b/.mocharc.json @@ -0,0 +1,7 @@ +{ + "extensions": ["ts"], + "test": ["tests/**.ts"], + "node-option": ["loader=ts-node/esm"], + "slow": 5000, + "timeout": 300000 +} diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000..25bf17f --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +18 \ No newline at end of file diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..04548a6 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,8 @@ +cairo +venv +target +deployments/artifacts +dist +.github +tests-integration/fixtures +starknet-devnet-rs diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..187dc33 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,9 @@ +{ + "arrowParens": "always", + "useTabs": false, + "trailingComma": "all", + "singleQuote": false, + "semi": true, + "printWidth": 120, + "plugins": ["prettier-plugin-organize-imports"] +} diff --git a/.tool-versions b/.tool-versions new file mode 100644 index 0000000..823941b --- /dev/null +++ b/.tool-versions @@ -0,0 +1,2 @@ +scarb 2.6.3 +starknet-foundry 0.24.0 diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..d9a9554 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,8 @@ +# Use the base image +FROM shardlabs/starknet-devnet-rs:bab781a018318df51adb20fc60716c8429ee89b0 + +# Expose port 5050 +EXPOSE 5050 + +# Set default command to run the container +CMD ["--gas-price", "36000000000", "--data-gas-price", "1", "--timeout", "320", "--seed", "0", "--lite-mode", "--gas-price-strk", "36000000000", "--data-gas-price-strk", "1"] \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..0d9b7b0 --- /dev/null +++ b/README.md @@ -0,0 +1,118 @@ +# Starknet Gifting + +The goal of this protocol is to allow sending tokens to a recipient without knowing their address. This is done using a non-custodial escrow contract. Since the escrow contract functions as an account, it can pay for its own transactions, meaning the recipient doesn't need funds to initiate the claim. This is ideal for onboarding new users who can claim a gift to a newly created and even undeployed account. + +## High level Flow + +1. The sender creates a key pair locally called **gift_key**. +2. The sender deposits the tokens to be transferred, along with a small amount of fee token (ETH or STK) to cover the claim transaction, to the factory. The sender also specifies the **public key** as an identifier. +3. The factory deploys an escrow account to which the gift amount is transferred along with the fee amount. +4. The sender shares the **private key** with the recipient over an external channel such as text or email. +5. The recipient can claim the tokens by transferring them from the escrow account to their account using the private key to sign the transaction. + +As the fee should be larger than the claiming transaction cost, there might be a small amount of fee token left. We will refer to this leftover amount as "dust". + +## Deposits + +Deposits follow the flow described in the first 3 steps above. + +![Sessions diagram](/docs/deposit_diagram.png) + +For more details please see the `deposit` function at [Deposit example](./lib/deposit.ts). + +## Claiming + +Claiming can be done in two ways: + +### Internal claim + +The recipient uses the private key to craft a transaction to claim the gift. The `fee_amount` will be used to cover the transaction fees, so the recipient only gets the `gift_amount`. The recipient doesn’t need to have any funds in their wallet or even a deployed wallet to claim the gift using this method. + +![Sessions diagram](/docs/internal_claim.png) + +Edge cases: + +- Insufficient `fee_amount`: Alternative options are "external claiming", waiting for transaction price to go down, or canceling the gift (see below). +- Dust: `fee_amount` will usually be higher than the actual fee and there will be some amount left in the contract. The protocol owner can collect the dust later. +- If the internal claim transaction fails for any reason, the account won't allow to submit another transaction. But the gift can be cancelled or claimed using the external method. + +For more details about how to trigger it please see the `claimInternal` function at [Claim Internal Example](./lib/claim.ts). + +### External claim + +It is also possible for someone else to pay for the claim fees. This can be useful if the funds deposited to pay for the claim transaction are not enough, or if someone wants to subsidize the claim. + +The receiver can use the private key sign a message containing the address receiving the address (and optionally some address that will receive the dust). Using this signature, anybody can execute a transaction to perform the claim. To do so, they should call `claim_external` on the escrow account (through the `execute_action` entrypoint). + +![Sessions diagram](/docs/external_claim.png) + +For more details please see the `claimExternal` function at [Claim External Example](./lib/claim.ts). + +## Cancelling Gifts + +Gifts can be cancelled by the sender provided that they have not been claimed yet. The sender will retrieve both the `gift_amount` and the `fee_amount` they deposited for that gift. + +For more details please see the `cancelGift` function at [Cancel example](./lib/claim.ts). + +## Operator + +This section outlines all the operations that the factory owner is allowed to perform. + +### Claim Dust + +The operator can claim the dust left in an escrow account. This action can only be done after a claim has been performed. + +### Pause deposits + +The owner has the capability to pause all deposits. However, it cannot prevent any claims from happening, nor can it prevent any cancellations. + +### Upgrade + +The protocol can be upgraded to add new functionality or fix issues however, it can only be upgraded after a 7 day timelock. This prevents the owner from upgrading to a malicious implementation, as users will have enough time to leave the protocol by either claiming or cancelling their gifts. + +Through an upgrade, the owner can make the protocol non upgradeable in the future. + +## Escrow account address calculation + +To compute the address of the escrow account, you can either call `get_escrow_address()` with the relevant arguments. Or you can do it off-chain using, for example, starknetJS. +The parameters are as follow: + +- Salt: 0 +- Class hash: the class hash of the escrow account +- Constructor calldata: The constructor argument used to deploy the escrow account +- Deployer address: The address of the factory + +# Development + +## Local development + +We recommend you to install scarb through ASDF. Please refer to [these instructions](https://docs.swmansion.com/scarb/download.html#install-via-asdf). +Thanks to the [.tool-versions file](./.tool-versions), you don't need to install a specific scarb or starknet foundry version. The correct one will be automatically downloaded and installed. + +##@ Test the contracts (Cairo) + +``` +scarb test +``` + +### Install the devnet (run in project root folder) + +You should have docker installed in your machine then you can start the devnet by running the following command: + +```shell +scarb run start-devnet +``` + +### Install JS dependencies + +Install all packages: + +```shell +yarn +``` + +Run all integration tests: + +```shell +scarb run test-ts +``` diff --git a/Scarb.lock b/Scarb.lock new file mode 100644 index 0000000..1b19aea --- /dev/null +++ b/Scarb.lock @@ -0,0 +1,54 @@ +# Code generated by scarb DO NOT EDIT. +version = 1 + +[[package]] +name = "alexandria_data_structures" +version = "0.2.0" +source = "git+https://github.com/keep-starknet-strange/alexandria.git?rev=cairo-v2.6.0#946e6e2f9d390ad9f345882a352c0dd6f02ef3ad" +dependencies = [ + "alexandria_encoding", +] + +[[package]] +name = "alexandria_encoding" +version = "0.1.0" +source = "git+https://github.com/keep-starknet-strange/alexandria.git?rev=cairo-v2.6.0#946e6e2f9d390ad9f345882a352c0dd6f02ef3ad" +dependencies = [ + "alexandria_math", + "alexandria_numeric", +] + +[[package]] +name = "alexandria_math" +version = "0.2.0" +source = "git+https://github.com/keep-starknet-strange/alexandria.git?rev=cairo-v2.6.0#946e6e2f9d390ad9f345882a352c0dd6f02ef3ad" +dependencies = [ + "alexandria_data_structures", +] + +[[package]] +name = "alexandria_numeric" +version = "0.1.0" +source = "git+https://github.com/keep-starknet-strange/alexandria.git?rev=cairo-v2.6.0#946e6e2f9d390ad9f345882a352c0dd6f02ef3ad" +dependencies = [ + "alexandria_math", +] + +[[package]] +name = "argent_gifting" +version = "0.1.0" +dependencies = [ + "alexandria_math", + "openzeppelin", + "snforge_std", +] + +[[package]] +name = "openzeppelin" +version = "0.13.0" +source = "git+https://github.com/OpenZeppelin/cairo-contracts.git?tag=v0.13.0#978b4e75209da355667d8954d2450e32bd71fe49" + +[[package]] +name = "snforge_std" +version = "0.24.0" +source = "git+https://github.com/foundry-rs/starknet-foundry?tag=v0.24.0#95e9fb09cb91b3c05295915179ee1b55bf923653" diff --git a/Scarb.toml b/Scarb.toml new file mode 100644 index 0000000..f957cbd --- /dev/null +++ b/Scarb.toml @@ -0,0 +1,28 @@ +[package] +name = "argent_gifting" +version = "0.1.0" +edition = "2023_11" + + +[dependencies] +starknet = "2.6.3" +snforge_std = { git = "https://github.com/foundry-rs/starknet-foundry", tag = "v0.24.0" } +openzeppelin = { git = "https://github.com/OpenZeppelin/cairo-contracts.git", tag = "v0.13.0" } +alexandria_math = { git = "https://github.com/keep-starknet-strange/alexandria.git", rev = "cairo-v2.6.0" } + +[[target.starknet-contract]] +sierra = true +casm = true + +[tool.fmt] +max-line-length = 120 +sort-module-level-items = true + +[scripts] +test = "snforge test" +start-devnet = "docker build -t devnet . && docker run -d -p 127.0.0.1:5050:5050 devnet" +kill-devnet = "docker ps -q --filter 'ancestor=devnet' | xargs docker stop" +test-ts = "scarb --profile release build && yarn tsc && yarn mocha tests-integration/*.test.ts" +profile = "scarb --profile release build && node --loader ts-node/esm scripts/profile.ts" +format = "scarb fmt && yarn prettier --write ." +deploy = "scarb --profile release build && node --loader ts-node/esm scripts/deploy.ts" diff --git a/audits/Argent_Gifting_Audit_Report.pdf b/audits/Argent_Gifting_Audit_Report.pdf new file mode 100644 index 0000000..45aade6 Binary files /dev/null and b/audits/Argent_Gifting_Audit_Report.pdf differ diff --git a/deployments.txt b/deployments.txt new file mode 100644 index 0000000..2c0ba29 --- /dev/null +++ b/deployments.txt @@ -0,0 +1,14 @@ + +# Mainnet && Sepolia Deployment + +GiftFactory class hash: 0x7fc7e4c4ec573895f7ea4a332c3bc4a3ddcc91604d53956dcc060b7eb9d0813 +EscrowAccount class hash: 0x661aad3c9812f0dc0a78f320a58bdd8fed18ef601245c20e4bf43667bfd0289 +EscrowLibrary class hash: 0x7a8cca68e7a1ba75b420623224563f3900662f8b5ea6dc660c84ee665076e8d + +# Mainnet +GiftFactory address: 0x03667b42afbb0c8539aa411a6b181ab30b9da64725bdf61e997820dd630f39fa +GiftFactory owner: 0x64d28d1d1d53a0b5de12e3678699bc9ba32c1cb19ce1c048578581ebb7f8396 + +# Sepolia +GiftFactory address: 0x42a18d85a621332f749947a96342ba682f08e499b9f1364325903a37c5def60 +GiftFactory owner: 0x6b054e8dbc5756e3f43b70cf1bfa4639c560898a3c70b2f753ba53bef549a1c \ No newline at end of file diff --git a/docs/deposit_diagram.png b/docs/deposit_diagram.png new file mode 100644 index 0000000..a727809 Binary files /dev/null and b/docs/deposit_diagram.png differ diff --git a/docs/diagrams.drawio b/docs/diagrams.drawio new file mode 100644 index 0000000..c8bb787 --- /dev/null +++ b/docs/diagrams.drawio @@ -0,0 +1,231 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/external_claim.png b/docs/external_claim.png new file mode 100644 index 0000000..353b8f7 Binary files /dev/null and b/docs/external_claim.png differ diff --git a/docs/internal_claim.png b/docs/internal_claim.png new file mode 100644 index 0000000..48bdb43 Binary files /dev/null and b/docs/internal_claim.png differ diff --git a/empty_file b/empty_file deleted file mode 100644 index 8b13789..0000000 --- a/empty_file +++ /dev/null @@ -1 +0,0 @@ - diff --git a/gas-report.txt b/gas-report.txt new file mode 100644 index 0000000..2dad893 --- /dev/null +++ b/gas-report.txt @@ -0,0 +1,54 @@ +Summary: +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ (index) β”‚ Actual fee β”‚ Fee usd β”‚ Fee without DA β”‚ Gas without DA β”‚ Computation gas β”‚ Event gas β”‚ Calldata gas β”‚ Max computation per Category β”‚ Storage diffs β”‚ DA fee β”‚ DA mode β”‚ +β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ +β”‚ Transfer ETH (FeeToken: WEI) β”‚ '828.000.000.192' β”‚ 0.0033 β”‚ 828000000000 β”‚ 23 β”‚ 21 β”‚ 2 β”‚ 1 β”‚ 'steps' β”‚ 3 β”‚ 192 β”‚ 'BLOB' β”‚ +β”‚ Transfer STRK (FeeToken: WEI) β”‚ '828.000.000.320' β”‚ 0.0033 β”‚ 828000000000 β”‚ 23 β”‚ 21 β”‚ 2 β”‚ 1 β”‚ 'steps' β”‚ 4 β”‚ 320 β”‚ 'BLOB' β”‚ +β”‚ Gifting WEI (FeeToken: WEI) β”‚ '1.548.000.000.288' β”‚ 0.0061 β”‚ 1548000000000 β”‚ 43 β”‚ 37 β”‚ 5 β”‚ 2 β”‚ 'steps' β”‚ 3 β”‚ 288 β”‚ 'BLOB' β”‚ +β”‚ Claiming WEI (FeeToken: WEI) β”‚ '1.188.000.000.192' β”‚ 0.0047 β”‚ 1188000000000 β”‚ 33 β”‚ 30 β”‚ 2 β”‚ 2 β”‚ 'steps' β”‚ 3 β”‚ 192 β”‚ 'BLOB' β”‚ +β”‚ Claiming external WEI (FeeToken: WEI) β”‚ '1.620.000.000.256' β”‚ 0.0064 β”‚ 1620000000000 β”‚ 45 β”‚ 42 β”‚ 2 β”‚ 2 β”‚ 'steps' β”‚ 4 β”‚ 256 β”‚ 'BLOB' β”‚ +β”‚ Get dust WEI (FeeToken: WEI) β”‚ '1.620.000.000.192' β”‚ 0.0064 β”‚ 1620000000000 β”‚ 45 β”‚ 42 β”‚ 2 β”‚ 2 β”‚ 'steps' β”‚ 3 β”‚ 192 β”‚ 'BLOB' β”‚ +β”‚ Gifting WEI (FeeToken: FRI) β”‚ '2.196.000.000.480' β”‚ 0.0087 β”‚ 2196000000000 β”‚ 61 β”‚ 52 β”‚ 7 β”‚ 3 β”‚ 'steps' β”‚ 5 β”‚ 480 β”‚ 'BLOB' β”‚ +β”‚ Claiming WEI (FeeToken: FRI) β”‚ '1.188.000.000.320' β”‚ 0 β”‚ 1188000000000 β”‚ 33 β”‚ 30 β”‚ 2 β”‚ 2 β”‚ 'steps' β”‚ 4 β”‚ 320 β”‚ 'BLOB' β”‚ +β”‚ Claiming external WEI (FeeToken: FRI) β”‚ '1.620.000.000.320' β”‚ 0 β”‚ 1620000000000 β”‚ 45 β”‚ 42 β”‚ 2 β”‚ 2 β”‚ 'steps' β”‚ 4 β”‚ 320 β”‚ 'BLOB' β”‚ +β”‚ Get dust WEI (FeeToken: FRI) β”‚ '1.728.000.000.320' β”‚ 0.0069 β”‚ 1728000000000 β”‚ 48 β”‚ 45 β”‚ 2 β”‚ 2 β”‚ 'steps' β”‚ 4 β”‚ 320 β”‚ 'BLOB' β”‚ +β”‚ Gifting FRI (FeeToken: WEI) β”‚ '2.196.000.000.480' β”‚ 0.0087 β”‚ 2196000000000 β”‚ 61 β”‚ 52 β”‚ 7 β”‚ 3 β”‚ 'steps' β”‚ 5 β”‚ 480 β”‚ 'BLOB' β”‚ +β”‚ Claiming FRI (FeeToken: WEI) β”‚ '1.188.000.000.320' β”‚ 0.0047 β”‚ 1188000000000 β”‚ 33 β”‚ 30 β”‚ 2 β”‚ 2 β”‚ 'steps' β”‚ 4 β”‚ 320 β”‚ 'BLOB' β”‚ +β”‚ Claiming external FRI (FeeToken: WEI) β”‚ '1.620.000.000.320' β”‚ 0.0064 β”‚ 1620000000000 β”‚ 45 β”‚ 42 β”‚ 2 β”‚ 2 β”‚ 'steps' β”‚ 4 β”‚ 320 β”‚ 'BLOB' β”‚ +β”‚ Get dust FRI (FeeToken: WEI) β”‚ '1.728.000.000.192' β”‚ 0.0069 β”‚ 1728000000000 β”‚ 48 β”‚ 45 β”‚ 2 β”‚ 2 β”‚ 'steps' β”‚ 3 β”‚ 192 β”‚ 'BLOB' β”‚ +β”‚ Gifting FRI (FeeToken: FRI) β”‚ '1.548.000.000.416' β”‚ 0.0061 β”‚ 1548000000000 β”‚ 43 β”‚ 37 β”‚ 5 β”‚ 2 β”‚ 'steps' β”‚ 4 β”‚ 416 β”‚ 'BLOB' β”‚ +β”‚ Claiming FRI (FeeToken: FRI) β”‚ '1.188.000.000.192' β”‚ 0 β”‚ 1188000000000 β”‚ 33 β”‚ 30 β”‚ 2 β”‚ 2 β”‚ 'steps' β”‚ 3 β”‚ 192 β”‚ 'BLOB' β”‚ +β”‚ Claiming external FRI (FeeToken: FRI) β”‚ '1.620.000.000.256' β”‚ 0 β”‚ 1620000000000 β”‚ 45 β”‚ 42 β”‚ 2 β”‚ 2 β”‚ 'steps' β”‚ 4 β”‚ 256 β”‚ 'BLOB' β”‚ +β”‚ Get dust FRI (FeeToken: FRI) β”‚ '1.620.000.000.320' β”‚ 0.0064 β”‚ 1620000000000 β”‚ 45 β”‚ 42 β”‚ 2 β”‚ 2 β”‚ 'steps' β”‚ 4 β”‚ 320 β”‚ 'BLOB' β”‚ +β”‚ Get dust 2 (FeeToken: WEI) β”‚ '2.772.000.000.320' β”‚ 0.011 β”‚ 2772000000000 β”‚ 77 β”‚ 71 β”‚ 3 β”‚ 4 β”‚ 'steps' β”‚ 5 β”‚ 320 β”‚ 'BLOB' β”‚ +β”‚ Get dust 3 (FeeToken: WEI) β”‚ '3.960.000.000.384' β”‚ 0.0158 β”‚ 3960000000000 β”‚ 110 β”‚ 101 β”‚ 4 β”‚ 6 β”‚ 'steps' β”‚ 6 β”‚ 384 β”‚ 'BLOB' β”‚ +β”‚ Get dust 4 (FeeToken: WEI) β”‚ '5.112.000.000.448' β”‚ 0.0204 β”‚ 5112000000000 β”‚ 142 β”‚ 130 β”‚ 5 β”‚ 8 β”‚ 'steps' β”‚ 7 β”‚ 448 β”‚ 'BLOB' β”‚ +β”‚ Get dust 5 (FeeToken: WEI) β”‚ '6.264.000.000.512' β”‚ 0.025 β”‚ 6264000000000 β”‚ 174 β”‚ 160 β”‚ 6 β”‚ 9 β”‚ 'steps' β”‚ 8 β”‚ 512 β”‚ 'BLOB' β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +Resources: +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β” +β”‚ (index) β”‚ bitwise β”‚ ec_op β”‚ ecdsa β”‚ keccak β”‚ pedersen β”‚ poseidon β”‚ range_check β”‚ steps β”‚ +β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€ +β”‚ Transfer ETH (FeeToken: WEI) β”‚ 0 β”‚ 3 β”‚ 0 β”‚ 0 β”‚ 25 β”‚ 0 β”‚ 181 β”‚ 8184 β”‚ +β”‚ Transfer STRK (FeeToken: WEI) β”‚ 0 β”‚ 3 β”‚ 0 β”‚ 0 β”‚ 25 β”‚ 0 β”‚ 181 β”‚ 8184 β”‚ +β”‚ Gifting WEI (FeeToken: WEI) β”‚ 0 β”‚ 3 β”‚ 0 β”‚ 0 β”‚ 48 β”‚ 0 β”‚ 339 β”‚ 14624 β”‚ +β”‚ Claiming WEI (FeeToken: WEI) β”‚ 0 β”‚ 3 β”‚ 0 β”‚ 0 β”‚ 47 β”‚ 0 β”‚ 373 β”‚ 11725 β”‚ +β”‚ Claiming external WEI (FeeToken: WEI) β”‚ 0 β”‚ 6 β”‚ 0 β”‚ 0 β”‚ 52 β”‚ 4 β”‚ 477 β”‚ 16713 β”‚ +β”‚ Get dust WEI (FeeToken: WEI) β”‚ 0 β”‚ 3 β”‚ 0 β”‚ 0 β”‚ 49 β”‚ 0 β”‚ 480 β”‚ 16585 β”‚ +β”‚ Gifting WEI (FeeToken: FRI) β”‚ 0 β”‚ 3 β”‚ 0 β”‚ 0 β”‚ 64 β”‚ 0 β”‚ 465 β”‚ 20607 β”‚ +β”‚ Claiming WEI (FeeToken: FRI) β”‚ 0 β”‚ 3 β”‚ 0 β”‚ 0 β”‚ 47 β”‚ 0 β”‚ 407 β”‚ 11923 β”‚ +β”‚ Claiming external WEI (FeeToken: FRI) β”‚ 0 β”‚ 6 β”‚ 0 β”‚ 0 β”‚ 52 β”‚ 4 β”‚ 477 β”‚ 16713 β”‚ +β”‚ Get dust WEI (FeeToken: FRI) β”‚ 0 β”‚ 3 β”‚ 0 β”‚ 0 β”‚ 50 β”‚ 0 β”‚ 514 β”‚ 17757 β”‚ +β”‚ Gifting FRI (FeeToken: WEI) β”‚ 0 β”‚ 3 β”‚ 0 β”‚ 0 β”‚ 64 β”‚ 0 β”‚ 465 β”‚ 20606 β”‚ +β”‚ Claiming FRI (FeeToken: WEI) β”‚ 0 β”‚ 3 β”‚ 0 β”‚ 0 β”‚ 47 β”‚ 0 β”‚ 373 β”‚ 11725 β”‚ +β”‚ Claiming external FRI (FeeToken: WEI) β”‚ 0 β”‚ 6 β”‚ 0 β”‚ 0 β”‚ 52 β”‚ 4 β”‚ 477 β”‚ 16713 β”‚ +β”‚ Get dust FRI (FeeToken: WEI) β”‚ 0 β”‚ 3 β”‚ 0 β”‚ 0 β”‚ 50 β”‚ 0 β”‚ 514 β”‚ 17757 β”‚ +β”‚ Gifting FRI (FeeToken: FRI) β”‚ 0 β”‚ 3 β”‚ 0 β”‚ 0 β”‚ 48 β”‚ 0 β”‚ 339 β”‚ 14625 β”‚ +β”‚ Claiming FRI (FeeToken: FRI) β”‚ 0 β”‚ 3 β”‚ 0 β”‚ 0 β”‚ 47 β”‚ 0 β”‚ 407 β”‚ 11923 β”‚ +β”‚ Claiming external FRI (FeeToken: FRI) β”‚ 0 β”‚ 6 β”‚ 0 β”‚ 0 β”‚ 52 β”‚ 4 β”‚ 477 β”‚ 16713 β”‚ +β”‚ Get dust FRI (FeeToken: FRI) β”‚ 0 β”‚ 3 β”‚ 0 β”‚ 0 β”‚ 49 β”‚ 0 β”‚ 480 β”‚ 16585 β”‚ +β”‚ Get dust 2 (FeeToken: WEI) β”‚ 0 β”‚ 3 β”‚ 0 β”‚ 0 β”‚ 83 β”‚ 0 β”‚ 856 β”‚ 28376 β”‚ +β”‚ Get dust 3 (FeeToken: WEI) β”‚ 0 β”‚ 3 β”‚ 0 β”‚ 0 β”‚ 117 β”‚ 0 β”‚ 1232 β”‚ 40167 β”‚ +β”‚ Get dust 4 (FeeToken: WEI) β”‚ 0 β”‚ 3 β”‚ 0 β”‚ 0 β”‚ 151 β”‚ 0 β”‚ 1608 β”‚ 51958 β”‚ +β”‚ Get dust 5 (FeeToken: WEI) β”‚ 0 β”‚ 3 β”‚ 0 β”‚ 0 β”‚ 185 β”‚ 0 β”‚ 1984 β”‚ 63749 β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”˜ diff --git a/lib/accounts.ts b/lib/accounts.ts new file mode 100644 index 0000000..4c34640 --- /dev/null +++ b/lib/accounts.ts @@ -0,0 +1,60 @@ +import { Account, Call, CallData, RPC, uint256 } from "starknet"; +import { manager } from "./manager"; +import { ethAddress, strkAddress } from "./tokens"; + +export const deployer = (() => { + if (manager.isDevnet) { + const devnetAddress = "0x64b48806902a367c8598f4f95c305e8c1a1acba5f082d294a43793113115691"; + const devnetPrivateKey = "0x71d7bb07b9a64f6f78ac4c816aff4da9"; + return new Account(manager, devnetAddress, devnetPrivateKey, undefined, RPC.ETransactionVersion.V2); + } + const address = process.env.ADDRESS; + const privateKey = process.env.PRIVATE_KEY; + if (address && privateKey) { + return new Account(manager, address, privateKey, undefined, RPC.ETransactionVersion.V2); + } + throw new Error("Missing deployer address or private key, please set ADDRESS and PRIVATE_KEY env variables."); +})(); + +export function devnetAccount() { + if (manager.isDevnet) { + const devnetAddress = "0x78662e7352d062084b0010068b99288486c2d8b914f6e2a55ce945f8792c8b1"; + const devnetPrivateKey = "0xe1406455b7d66b1690803be066cbe5e"; + return new Account(manager, devnetAddress, devnetPrivateKey, undefined, RPC.ETransactionVersion.V2); + } + it.skip("Only works in devnet."); + // gotta keep this line for the return type + throw new Error("Only works in devnet."); +} + +export const deployerV3 = setDefaultTransactionVersionV3(deployer); + +export function setDefaultTransactionVersion(account: Account, newVersion: boolean): Account { + const newDefaultVersion = newVersion ? RPC.ETransactionVersion.V3 : RPC.ETransactionVersion.V2; + if (account.transactionVersion === newDefaultVersion) { + return account; + } + return new Account(account, account.address, account.signer, account.cairoVersion, newDefaultVersion); +} + +export function setDefaultTransactionVersionV3(account: Account): Account { + return setDefaultTransactionVersion(account, true); +} + +console.log("Deployer:", deployer.address); + +export async function fundAccountCall( + recipient: string, + amount: number | bigint, + token: "ETH" | "STRK", +): Promise { + if (amount <= 0n) { + return; + } + const contractAddress = { ETH: ethAddress, STRK: strkAddress }[token]; + if (!contractAddress) { + throw new Error(`Unsupported token ${token}`); + } + const calldata = CallData.compile([recipient, uint256.bnToUint256(amount)]); + return { contractAddress, calldata, entrypoint: "transfer" }; +} diff --git a/lib/claim.ts b/lib/claim.ts new file mode 100644 index 0000000..b16bf0b --- /dev/null +++ b/lib/claim.ts @@ -0,0 +1,211 @@ +import { + Account, + Call, + CallData, + Calldata, + RPC, + TransactionReceipt, + UniversalDetails, + ec, + encode, + hash, + num, + shortString, + uint256, +} from "starknet"; + +import { + LegacyStarknetKeyPair, + StarknetSignature, + calculateEscrowAddress, + deployer, + ethAddress, + manager, + setDefaultTransactionVersionV3, + strkAddress, +} from "."; + +const typesRev1 = { + StarknetDomain: [ + { name: "name", type: "shortstring" }, + { name: "version", type: "shortstring" }, + { name: "chainId", type: "shortstring" }, + { name: "revision", type: "shortstring" }, + ], + ClaimExternal: [ + { name: "receiver", type: "ContractAddress" }, + { name: "dust receiver", type: "ContractAddress" }, + ], +}; + +function getDomain(chainId: string) { + // WARNING! revision is encoded as a number in the StarkNetDomain type and not as shortstring + // This is due to a bug in the Braavos implementation, and has been kept for compatibility + return { + name: "GiftFactory.claim_external", + version: shortString.encodeShortString("1"), + chainId, + revision: "1", + }; +} + +export interface ClaimExternal { + receiver: string; + dustReceiver?: string; +} + +export async function getClaimExternalData(claimExternal: ClaimExternal) { + const chainId = await manager.getChainId(); + return { + types: typesRev1, + primaryType: "ClaimExternal", + domain: getDomain(chainId), + message: { receiver: claimExternal.receiver, "dust receiver": claimExternal.dustReceiver || "0x0" }, + }; +} + +export interface AccountConstructorArguments { + sender: string; + gift_token: string; + gift_amount: bigint; + fee_token: string; + fee_amount: bigint; + gift_pubkey: bigint; +} + +export interface Gift extends AccountConstructorArguments { + factory: string; + escrow_class_hash: string; +} + +export function buildGiftCallData(gift: Gift) { + return { + ...gift, + gift_amount: uint256.bnToUint256(gift.gift_amount), + }; +} + +export async function signExternalClaim(signParams: { + gift: Gift; + receiver: string; + giftPrivateKey: string; + dustReceiver?: string; + forceEscrowAddress?: string; +}): Promise { + const giftSigner = new LegacyStarknetKeyPair(signParams.giftPrivateKey); + const claimExternalData = await getClaimExternalData({ + receiver: signParams.receiver, + dustReceiver: signParams.dustReceiver, + }); + const stringArray = (await giftSigner.signMessage( + claimExternalData, + signParams.forceEscrowAddress || calculateEscrowAddress(signParams.gift), + )) as string[]; + if (stringArray.length !== 2) { + throw new Error("Invalid signature"); + } + return { r: BigInt(stringArray[0]), s: BigInt(stringArray[1]) }; +} + +export async function claimExternal(args: { + gift: Gift; + receiver: string; + giftPrivateKey: string; + useTxV3?: boolean; + dustReceiver?: string; +}): Promise { + const account = args.useTxV3 ? setDefaultTransactionVersionV3(deployer) : deployer; + const signature = await signExternalClaim({ + gift: args.gift, + receiver: args.receiver, + giftPrivateKey: args.giftPrivateKey, + dustReceiver: args.dustReceiver, + }); + + const claimExternalCallData = CallData.compile([ + buildGiftCallData(args.gift), + args.receiver, + args.dustReceiver || "0x0", + signature, + ]); + const response = await account.execute( + executeActionOnAccount("claim_external", calculateEscrowAddress(args.gift), claimExternalCallData), + ); + return manager.waitForTransaction(response.transaction_hash); +} + +export function executeActionOnAccount(functionName: string, accountAddress: string, args: Calldata): Call { + return { + contractAddress: accountAddress, + entrypoint: "execute_action", + calldata: { selector: hash.getSelectorFromName(functionName), calldata: args }, + }; +} + +export async function claimInternal(args: { + gift: Gift; + receiver: string; + giftPrivateKey: string; + overrides?: { escrowAccountAddress?: string; callToAddress?: string }; + details?: UniversalDetails; +}): Promise { + const escrowAddress = args.overrides?.escrowAccountAddress || calculateEscrowAddress(args.gift); + const escrowAccount = getEscrowAccount(args.gift, args.giftPrivateKey, escrowAddress); + const response = await escrowAccount.execute( + [ + { + contractAddress: args.overrides?.callToAddress ?? escrowAddress, + calldata: [buildGiftCallData(args.gift), args.receiver], + entrypoint: "claim_internal", + }, + ], + undefined, + { ...args.details }, + ); + return manager.waitForTransaction(response.transaction_hash); +} + +export async function cancelGift(args: { gift: Gift; senderAccount?: Account }): Promise { + const cancelCallData = CallData.compile([buildGiftCallData(args.gift)]); + const account = args.senderAccount || deployer; + const response = await account.execute( + executeActionOnAccount("cancel", calculateEscrowAddress(args.gift), cancelCallData), + ); + return manager.waitForTransaction(response.transaction_hash); +} + +export async function claimDust(args: { + gift: Gift; + receiver: string; + factoryOwner?: Account; +}): Promise { + const claimDustCallData = CallData.compile([buildGiftCallData(args.gift), args.receiver]); + const account = args.factoryOwner || deployer; + const response = await account.execute( + executeActionOnAccount("claim_dust", calculateEscrowAddress(args.gift), claimDustCallData), + ); + return manager.waitForTransaction(response.transaction_hash); +} + +function useTxv3(tokenAddress: string): boolean { + if (tokenAddress === ethAddress) { + return false; + } else if (tokenAddress === strkAddress) { + return true; + } + throw new Error(`Unsupported token`); +} + +export const randomReceiver = (): string => { + return `0x${encode.buf2hex(ec.starkCurve.utils.randomPrivateKey())}`; +}; + +export function getEscrowAccount(gift: Gift, giftPrivateKey: string, forceEscrowAddress?: string): Account { + return new Account( + manager, + forceEscrowAddress || num.toHex(calculateEscrowAddress(gift)), + giftPrivateKey, + undefined, + useTxv3(gift.fee_token) ? RPC.ETransactionVersion.V3 : RPC.ETransactionVersion.V2, + ); +} diff --git a/lib/contracts.ts b/lib/contracts.ts new file mode 100644 index 0000000..d595879 --- /dev/null +++ b/lib/contracts.ts @@ -0,0 +1,107 @@ +import { readFileSync } from "fs"; +import { + Abi, + AccountInterface, + CompiledSierra, + Contract, + DeclareContractPayload, + ProviderInterface, + UniversalDeployerContractPayload, + UniversalDetails, + json, +} from "starknet"; +import { deployer } from "./accounts"; +import { WithDevnet } from "./devnet"; + +export const contractsFolder = "./target/release/argent_gifting_"; +export const fixturesFolder = "./tests-integration/fixtures/argent_gifting_"; + +export const WithContracts = >(Base: T) => + class extends Base { + protected classCache: Record = {}; + + removeFromClassCache(contractName: string) { + delete this.classCache[contractName]; + } + + clearClassCache() { + for (const contractName of Object.keys(this.classCache)) { + delete this.classCache[contractName]; + } + } + + async restartDevnetAndClearClassCache() { + if (this.isDevnet) { + await this.restart(); + this.clearClassCache(); + } + } + + // Could extends Account to add our specific fn but that's too early. + async declareLocalContract(contractName: string, wait = true, folder = contractsFolder): Promise { + const cachedClass = this.classCache[contractName]; + if (cachedClass) { + return cachedClass; + } + const payload = getDeclareContractPayload(contractName, folder); + const skipSimulation = this.isDevnet; + // max fee avoids slow estimate + const maxFee = skipSimulation ? 1e18 : undefined; + + const { class_hash, transaction_hash } = await deployer.declareIfNot(payload, { maxFee }); + + if (wait && transaction_hash) { + await this.waitForTransaction(transaction_hash); + console.log(`\t${contractName} declared`); + } + this.classCache[contractName] = class_hash; + return class_hash; + } + + async declareFixtureContract(contractName: string, wait = true): Promise { + return await this.declareLocalContract(contractName, wait, fixturesFolder); + } + + async loadContract(contractAddress: string, classHash?: string): Promise { + const { abi } = await this.getClassAt(contractAddress); + classHash ??= await this.getClassHashAt(contractAddress); + return new ContractWithClass(abi, contractAddress, this, classHash); + } + + async deployContract( + contractName: string, + payload: Omit | UniversalDeployerContractPayload[] = {}, + details?: UniversalDetails, + folder = contractsFolder, + ): Promise { + const classHash = await this.declareLocalContract(contractName, true, folder); + const { contract_address } = await deployer.deployContract({ ...payload, classHash }, details); + + // TODO could avoid network request and just create the contract using the ABI + return await this.loadContract(contract_address, classHash); + } + }; + +export class ContractWithClass extends Contract { + constructor( + abi: Abi, + address: string, + providerOrAccount: ProviderInterface | AccountInterface, + public readonly classHash: string, + ) { + super(abi, address, providerOrAccount); + } +} + +export function getDeclareContractPayload(contractName: string, folder = contractsFolder): DeclareContractPayload { + const contract: CompiledSierra = readContract(`${folder}${contractName}.contract_class.json`); + const payload: DeclareContractPayload = { contract }; + if ("sierra_program" in contract) { + payload.casm = readContract(`${folder}${contractName}.compiled_contract_class.json`); + } + return payload; +} + +export function readContract(path: string) { + return json.parse(readFileSync(path).toString("ascii")); +} diff --git a/lib/deposit.ts b/lib/deposit.ts new file mode 100644 index 0000000..39c7f64 --- /dev/null +++ b/lib/deposit.ts @@ -0,0 +1,134 @@ +import { Account, Call, CallData, Contract, InvokeFunctionResponse, TransactionReceipt, hash, uint256 } from "starknet"; +import { AccountConstructorArguments, Gift, LegacyStarknetKeyPair, deployer, manager } from "./"; + +export const STRK_GIFT_MAX_FEE = 200000000000000000n; // 0.2 STRK +export const STRK_GIFT_AMOUNT = STRK_GIFT_MAX_FEE + 1n; +export const ETH_GIFT_MAX_FEE = 200000000000000n; // 0.0002 ETH +export const ETH_GIFT_AMOUNT = ETH_GIFT_MAX_FEE + 1n; + +export function getMaxFee(useTxV3: boolean): bigint { + return useTxV3 ? STRK_GIFT_MAX_FEE : ETH_GIFT_MAX_FEE; +} + +export function getGiftAmount(useTxV3: boolean): bigint { + return useTxV3 ? STRK_GIFT_AMOUNT : ETH_GIFT_AMOUNT; +} + +export async function deposit(depositParams: { + sender: Account; + giftAmount: bigint; + feeAmount: bigint; + factoryAddress: string; + feeTokenAddress: string; + giftTokenAddress: string; + giftSignerPubKey: bigint; + overrides?: { + escrowAccountClassHash?: string; + }; +}): Promise<{ response: InvokeFunctionResponse; gift: Gift }> { + const { sender, giftAmount, feeAmount, factoryAddress, feeTokenAddress, giftTokenAddress, giftSignerPubKey } = + depositParams; + const factory = await manager.loadContract(factoryAddress); + const feeToken = await manager.loadContract(feeTokenAddress); + const giftToken = await manager.loadContract(giftTokenAddress); + + const escrowAccountClassHash = + depositParams.overrides?.escrowAccountClassHash || (await factory.get_latest_escrow_class_hash()); + const gift: Gift = { + factory: factoryAddress, + escrow_class_hash: escrowAccountClassHash, + sender: deployer.address, + gift_token: giftTokenAddress, + gift_amount: giftAmount, + fee_token: feeTokenAddress, + fee_amount: feeAmount, + gift_pubkey: giftSignerPubKey, + }; + const calls: Array = []; + if (feeTokenAddress === giftTokenAddress) { + calls.push(feeToken.populateTransaction.approve(factory.address, giftAmount + feeAmount)); + } else { + calls.push(feeToken.populateTransaction.approve(factory.address, feeAmount)); + calls.push(giftToken.populateTransaction.approve(factory.address, giftAmount)); + } + calls.push( + factory.populateTransaction.deposit( + escrowAccountClassHash, + giftTokenAddress, + giftAmount, + feeTokenAddress, + feeAmount, + giftSignerPubKey, + ), + ); + return { + response: await sender.execute(calls), + gift, + }; +} + +export async function defaultDepositTestSetup(args: { + factory: Contract; + useTxV3?: boolean; + overrides?: { + escrowAccountClassHash?: string; + giftPrivateKey?: bigint; + giftTokenAddress?: string; + feeTokenAddress?: string; + giftAmount?: bigint; + feeAmount?: bigint; + }; +}): Promise<{ + gift: Gift; + giftPrivateKey: string; + txReceipt: TransactionReceipt; +}> { + const escrowAccountClassHash = + args.overrides?.escrowAccountClassHash || (await args.factory.get_latest_escrow_class_hash()); + const useTxV3 = args.useTxV3 || false; + const giftAmount = args.overrides?.giftAmount ?? getGiftAmount(useTxV3); + const feeAmount = args.overrides?.feeAmount ?? getMaxFee(useTxV3); + + const feeToken = args.overrides?.feeTokenAddress + ? await manager.loadContract(args.overrides.feeTokenAddress) + : await manager.tokens.feeTokenContract(useTxV3); + + const giftTokenAddress = args.overrides?.giftTokenAddress || feeToken.address; + const giftSigner = new LegacyStarknetKeyPair(args.overrides?.giftPrivateKey); + const giftPubKey = giftSigner.publicKey; + + const { response, gift } = await deposit({ + sender: deployer, + overrides: { escrowAccountClassHash }, + giftAmount, + feeAmount, + factoryAddress: args.factory.address, + feeTokenAddress: feeToken.address, + giftTokenAddress, + giftSignerPubKey: giftPubKey, + }); + const txReceipt = await manager.waitForTransaction(response.transaction_hash); + return { gift, giftPrivateKey: giftSigner.privateKey, txReceipt }; +} + +export function calculateEscrowAddress(gift: Gift): string { + const constructorArgs: AccountConstructorArguments = { + sender: gift.sender, + gift_token: gift.gift_token, + gift_amount: gift.gift_amount, + fee_token: gift.fee_token, + fee_amount: gift.fee_amount, + gift_pubkey: gift.gift_pubkey, + }; + + const escrowAddress = hash.calculateContractAddressFromHash( + 0, + gift.escrow_class_hash, + CallData.compile({ + ...constructorArgs, + gift_amount: uint256.bnToUint256(gift.gift_amount), + }), + gift.factory, + ); + return escrowAddress; +} diff --git a/lib/devnet.ts b/lib/devnet.ts new file mode 100644 index 0000000..e2ebaa7 --- /dev/null +++ b/lib/devnet.ts @@ -0,0 +1,51 @@ +import { RawArgs, RpcProvider } from "starknet"; +import { Constructor } from "."; + +export const dumpFolderPath = "./dump"; +export const devnetBaseUrl = "http://127.0.0.1:5050"; + +export const WithDevnet = >(Base: T) => + class extends Base { + get isDevnet() { + return this.channel.nodeUrl.startsWith(devnetBaseUrl); + } + + // Polls quickly for a local network + waitForTransaction(transactionHash: string, options = {}) { + const retryInterval = this.isDevnet ? 250 : 1000; + return super.waitForTransaction(transactionHash, { retryInterval, ...options }); + } + + async mintEth(address: string, amount: number | bigint) { + await this.handlePost("mint", { address, amount: Number(amount) }); + } + + async increaseTime(timeInSeconds: number | bigint) { + await this.handlePost("increase_time", { time: Number(timeInSeconds) }); + } + + async setTime(timeInSeconds: number | bigint) { + await this.handlePost("set_time", { time: Number(timeInSeconds), generate_block: true }); + } + + async restart() { + await this.handlePost("restart"); + } + + async dump() { + await this.handlePost("dump", { path: dumpFolderPath }); + } + + async load() { + await this.handlePost("load", { path: dumpFolderPath }); + } + + async handlePost(path: string, payload?: RawArgs) { + const url = `${this.channel.nodeUrl}/${path}`; + const headers = { "Content-Type": "application/json" }; + const response = await fetch(url, { method: "POST", headers, body: JSON.stringify(payload) }); + if (!response.ok) { + throw new Error(`HTTP error! calling ${url} Status: ${response.status} Message: ${await response.text()}`); + } + } + }; diff --git a/lib/expectations.ts b/lib/expectations.ts new file mode 100644 index 0000000..5cbd36c --- /dev/null +++ b/lib/expectations.ts @@ -0,0 +1,110 @@ +import { assert, expect } from "chai"; +import { isEqual } from "lodash-es"; +import { + DeployContractUDCResponse, + GetTransactionReceiptResponse, + InvokeFunctionResponse, + RPC, + hash, + num, + shortString, +} from "starknet"; +import { manager } from "./manager"; +import { ensureSuccess } from "./receipts"; + +export async function expectRevertWithErrorMessage( + errorMessage: string, + execute: () => Promise, +) { + try { + const executionResult = await execute(); + if (!("transaction_hash" in executionResult)) { + throw new Error(`No transaction hash found on ${JSON.stringify(executionResult)}`); + } + await manager.waitForTransaction(executionResult["transaction_hash"]); + } catch (e: any) { + // Sometimes we have some leading zeros added, we slice here the "0x" part and search the whole error stack on this + const encodedErrorMessage = shortString.encodeShortString(errorMessage).slice(2); + if (e.toString().includes(encodedErrorMessage)) { + return; + } + // With e.toString() the error message is not fully displayed + console.log(e); + assert.fail( + `Couldn't find the error '${errorMessage}' (${shortString.encodeShortString(errorMessage)}) see message above`, + ); + } + assert.fail("No error detected"); +} + +export async function expectExecutionRevert(errorMessage: string, execute: () => Promise) { + try { + await waitForTransaction(await execute()); + /* eslint-disable @typescript-eslint/no-explicit-any */ + } catch (e: any) { + expect(e.toString()).to.contain(`Failure reason: ${shortString.encodeShortString(errorMessage)}`); + return; + } + assert.fail("No error detected"); +} + +async function expectEventFromReceipt(receipt: GetTransactionReceiptResponse, event: RPC.Event, eventName?: string) { + receipt = await ensureSuccess(receipt); + expect(event.keys.length).to.be.greaterThan(0, "Unsupported: No keys"); + const events = receipt.events ?? []; + const normalizedEvent = normalizeEvent(event); + const matches = events.filter((e) => isEqual(normalizeEvent(e), normalizedEvent)).length; + if (matches == 0) { + assert.fail(`No matches detected in this transaction: ${eventName}`); + } else if (matches > 1) { + assert.fail(`Multiple matches detected in this transaction: ${eventName}`); + } +} + +function normalizeEvent(event: RPC.Event): RPC.Event { + return { + from_address: event.from_address.toLowerCase(), + keys: event.keys.map(num.toBigInt).map(String), + data: event.data.map(num.toBigInt).map(String), + }; +} + +function convertToEvent(eventWithName: EventWithName): RPC.Event { + const selector = hash.getSelectorFromName(eventWithName.eventName); + return { + from_address: eventWithName.from_address, + keys: [selector].concat(eventWithName.keys ?? []), + data: eventWithName.data ?? [], + }; +} + +export async function expectEvent( + param: string | GetTransactionReceiptResponse | (() => Promise), + event: RPC.Event | EventWithName, +) { + if (typeof param === "function") { + ({ transaction_hash: param } = await param()); + } + if (typeof param === "string") { + param = await manager.waitForTransaction(param); + } + let eventName = ""; + if ("eventName" in event) { + eventName = event.eventName; + event = convertToEvent(event); + } + await expectEventFromReceipt(param, event, eventName); +} + +export async function waitForTransaction({ + transaction_hash, +}: InvokeFunctionResponse): Promise { + return await manager.waitForTransaction(transaction_hash); +} + +export interface EventWithName { + from_address: string; + eventName: string; + keys?: Array; + data?: Array; +} diff --git a/lib/gas.ts b/lib/gas.ts new file mode 100644 index 0000000..dd17b29 --- /dev/null +++ b/lib/gas.ts @@ -0,0 +1,236 @@ +import { exec } from "child_process"; +import fs from "fs"; +import { mapValues, maxBy, sortBy, sum } from "lodash-es"; +import { InvokeFunctionResponse, RpcProvider, shortString } from "starknet"; +import { ensureAccepted, ensureSuccess } from "."; + +const ethUsd = 4000n; +const strkUsd = 2n; + +// from https://docs.starknet.io/documentation/architecture_and_concepts/Network_Architecture/fee-mechanism/ +const gasWeights: Record = { + steps: 0.0025, + pedersen: 0.08, + poseidon: 0.08, + range_check: 0.04, + ecdsa: 5.12, + keccak: 5.12, + bitwise: 0.16, + ec_op: 2.56, +}; + +const l2PayloadsWeights: Record = { + eventKey: 0.256, + eventData: 0.12, + calldata: 0.128, +}; + +async function profileGasUsage(transactionHash: string, provider: RpcProvider, allowFailedTransactions = false) { + const receipt = await ensureAccepted(await provider.waitForTransaction(transactionHash)); + if (!allowFailedTransactions) { + await ensureSuccess(receipt); + } + const actualFee = BigInt(receipt.actual_fee.amount); + const rawResources = receipt.execution_resources!; + + const expectedResources = [ + "steps", + "memory_holes", + "range_check_builtin_applications", + "pedersen_builtin_applications", + "poseidon_builtin_applications", + "ec_op_builtin_applications", + "ecdsa_builtin_applications", + "bitwise_builtin_applications", + "keccak_builtin_applications", + "segment_arena_builtin", + "data_availability", + ]; + // all keys in rawResources must be in expectedResources + if (!Object.keys(rawResources).every((key) => expectedResources.includes(key))) { + throw new Error(`unexpected execution resources: ${Object.keys(rawResources).join()}`); + } + + const executionResources: Record = { + steps: rawResources.steps, + pedersen: rawResources.pedersen_builtin_applications ?? 0, + range_check: rawResources.range_check_builtin_applications ?? 0, + poseidon: rawResources.poseidon_builtin_applications ?? 0, + ecdsa: rawResources.ecdsa_builtin_applications ?? 0, + keccak: rawResources.keccak_builtin_applications ?? 0, + bitwise: rawResources.bitwise_builtin_applications ?? 0, + ec_op: rawResources.ec_op_builtin_applications ?? 0, + }; + + const blockNumber = receipt.block_number; + const blockInfo = await provider.getBlockWithReceipts(blockNumber); + + const stateUpdate = await provider.getStateUpdate(blockNumber); + const storageDiffs = stateUpdate.state_diff.storage_diffs; + const paidInStrk = receipt.actual_fee.unit == "FRI"; + const gasPrice = BigInt(paidInStrk ? blockInfo.l1_gas_price.price_in_fri : blockInfo.l1_gas_price.price_in_wei); + + const gasPerComputationCategory = Object.fromEntries( + Object.entries(executionResources) + .filter(([resource]) => resource in gasWeights) + .map(([resource, usage]) => [resource, Math.ceil(usage * gasWeights[resource])]), + ); + const maxComputationCategory = maxBy(Object.entries(gasPerComputationCategory), ([, gas]) => gas)![0]; + const computationGas = BigInt(gasPerComputationCategory[maxComputationCategory]); + + let gasWithoutDa; + let feeWithoutDa; + let daFee; + if (rawResources.data_availability) { + const dataGasPrice = Number( + paidInStrk ? blockInfo.l1_data_gas_price.price_in_fri : blockInfo.l1_data_gas_price.price_in_wei, + ); + + daFee = (rawResources.data_availability.l1_gas + rawResources.data_availability.l1_data_gas) * dataGasPrice; + feeWithoutDa = actualFee - BigInt(daFee); + gasWithoutDa = feeWithoutDa / gasPrice; + } else { + // This only happens for tx before Dencun + gasWithoutDa = actualFee / gasPrice; + daFee = gasWithoutDa - computationGas; + feeWithoutDa = actualFee; + } + + const sortedResources = Object.fromEntries(sortBy(Object.entries(executionResources), 0)); + + // L2 payloads + const { calldata, signature } = (await provider.getTransaction(receipt.transaction_hash)) as any; + const calldataGas = + calldata && signature ? Math.floor((calldata.length + signature.length) * l2PayloadsWeights.calldata) : undefined; // TODO find workaround for deployment transactions + + const eventGas = Math.floor( + receipt.events.reduce( + (sum, { keys, data }) => + sum + keys.length * l2PayloadsWeights.eventKey + data.length * l2PayloadsWeights.eventData, + 0, + ), + ); + return { + actualFee, + paidInStrk, + gasWithoutDa, + feeWithoutDa, + daFee, + computationGas, + maxComputationCategory, + gasPerComputationCategory, + executionResources: sortedResources, + gasPrice, + storageDiffs, + daMode: blockInfo.l1_da_mode, + calldataGas, + eventGas, + }; +} + +type Profile = Awaited>; + +export function newProfiler(provider: RpcProvider) { + const profiles: Record = {}; + + return { + async profile( + name: string, + transactionHash: InvokeFunctionResponse | string, + { printProfile = false, printStorage = false, allowFailedTransactions = false } = {}, + ) { + if (typeof transactionHash === "object") { + transactionHash = transactionHash.transaction_hash; + } + console.log(`Profiling: ${name} (${transactionHash})`); + const profile = await profileGasUsage(transactionHash, provider, allowFailedTransactions); + if (printProfile) { + console.dir(profile, { depth: null }); + } + if (printStorage) { + this.printStorageDiffs(profile); + } + profiles[name] = profile; + }, + summarizeCost(profile: Profile) { + const usdVal = profile.paidInStrk ? strkUsd : ethUsd; + const feeUsd = Number((10000n * profile.actualFee * usdVal) / 10n ** 18n) / 10000; + return { + "Actual fee": Number(profile.actualFee).toLocaleString("de-DE"), + "Fee usd": Number(feeUsd.toFixed(4)), + "Fee without DA": Number(profile.feeWithoutDa), + "Gas without DA": Number(profile.gasWithoutDa), + "Computation gas": Number(profile.computationGas), + "Event gas": Number(profile.eventGas), + "Calldata gas": Number(profile.calldataGas), + "Max computation per Category": profile.maxComputationCategory, + "Storage diffs": sum(profile.storageDiffs.map(({ storage_entries }) => storage_entries.length)), + "DA fee": Number(profile.daFee), + "DA mode": profile.daMode, + }; + }, + printStorageDiffs({ storageDiffs }: Profile) { + const diffs = storageDiffs.map(({ address, storage_entries }) => + storage_entries.map(({ key, value }) => ({ + address: shortenHex(address), + key: shortenHex(key), + hex: value, + dec: BigInt(value), + str: shortString.decodeShortString(value), + })), + ); + console.table(diffs.flat()); + }, + printSummary() { + console.log("Summary:"); + console.table(mapValues(profiles, this.summarizeCost)); + console.log("Resources:"); + console.table(mapValues(profiles, "executionResources")); + }, + formatReport() { + // Capture console.table output into a variable + let tableString = ""; + const log = console.log; + console.log = (...args) => { + tableString += args.join("") + "\n"; + }; + this.printSummary(); + // Restore console.log to its original function + console.log = log; + // Remove ANSI escape codes (colors) from the tableString + tableString = tableString.replace(/\u001b\[\d+m/g, ""); + return tableString; + }, + updateOrCheckReport() { + const report = this.formatReport(); + const filename = "gas-report.txt"; + const newFilename = "gas-report-new.txt"; + fs.writeFileSync(newFilename, report); + exec(`diff ${filename} ${newFilename}`, (err, stdout) => { + if (stdout) { + console.log(stdout); + console.error("⚠️ Changes to gas report detected.\n"); + } else { + console.log("✨ No changes to gas report."); + } + fs.unlinkSync(newFilename); + if (!stdout) { + return; + } + if (process.argv.includes("--write")) { + fs.writeFileSync(filename, report); + console.log("✨ Gas report updated."); + } else if (process.argv.includes("--check")) { + console.error(`⚠️ Please update ${filename} and commit it in this PR.\n`); + return process.exit(1); + } else { + console.log(`Usage: append either --write or --check to the CLI command.`); + } + }); + }, + }; +} + +function shortenHex(hex: string) { + return `${hex.slice(0, 6)}...${hex.slice(-4)}`; +} diff --git a/lib/index.ts b/lib/index.ts new file mode 100644 index 0000000..04c710a --- /dev/null +++ b/lib/index.ts @@ -0,0 +1,20 @@ +import chai from "chai"; +import chaiAsPromised from "chai-as-promised"; + +chai.use(chaiAsPromised); +chai.should(); + +export * from "./accounts"; +export * from "./claim"; +export * from "./contracts"; +export * from "./deposit"; +export * from "./devnet"; +export * from "./expectations"; +export * from "./manager"; +export * from "./protocol"; +export * from "./receipts"; +export * from "./signers/legacy"; +export * from "./signers/signers"; +export * from "./tokens"; + +export type Constructor = new (...args: any[]) => T; diff --git a/lib/manager.ts b/lib/manager.ts new file mode 100644 index 0000000..94efc7c --- /dev/null +++ b/lib/manager.ts @@ -0,0 +1,20 @@ +import dotenv from "dotenv"; +import { RpcProvider } from "starknet"; +import { WithContracts } from "./contracts"; +import { WithDevnet, devnetBaseUrl } from "./devnet"; +import { TokenManager } from "./tokens"; + +dotenv.config({ override: true }); + +export class Manager extends WithContracts(WithDevnet(RpcProvider)) { + tokens: TokenManager; + + constructor() { + super({ nodeUrl: process.env.RPC_URL || `${devnetBaseUrl}` }); + this.tokens = new TokenManager(this); + } +} + +export const manager = new Manager(); + +console.log("Provider:", manager.channel.nodeUrl); diff --git a/lib/protocol.ts b/lib/protocol.ts new file mode 100644 index 0000000..d081124 --- /dev/null +++ b/lib/protocol.ts @@ -0,0 +1,42 @@ +import { Contract, byteArray, uint256 } from "starknet"; +import { deployer, manager } from "."; + +export const protocolCache: Record = {}; + +export async function deployMockERC20(): Promise { + if (protocolCache["MockERC20"]) { + return protocolCache["MockERC20"]; + } + const mockERC20 = await manager.deployContract("MockERC20", { + unique: true, + constructorCalldata: [ + byteArray.byteArrayFromString("USDC"), + byteArray.byteArrayFromString("USDC"), + uint256.bnToUint256(100e18), + deployer.address, + deployer.address, + ], + }); + protocolCache["MockERC20"] = mockERC20; + return mockERC20; +} + +export async function setupGiftProtocol(): Promise<{ + factory: Contract; + escrowAccountClassHash: string; + escrowLibraryClassHash: string; +}> { + const escrowAccountClassHash = await manager.declareLocalContract("EscrowAccount"); + const escrowLibraryClassHash = await manager.declareLocalContract("EscrowLibrary"); + const cachedFactory = protocolCache["GiftFactory"]; + if (cachedFactory) { + return { factory: cachedFactory, escrowAccountClassHash, escrowLibraryClassHash }; + } + const factory = await manager.deployContract("GiftFactory", { + unique: true, + constructorCalldata: [escrowAccountClassHash, escrowLibraryClassHash, deployer.address], + }); + + protocolCache["GiftFactory"] = factory; + return { factory, escrowAccountClassHash, escrowLibraryClassHash }; +} diff --git a/lib/receipts.ts b/lib/receipts.ts new file mode 100644 index 0000000..359d6cd --- /dev/null +++ b/lib/receipts.ts @@ -0,0 +1,21 @@ +import { assert } from "chai"; +import { GetTransactionReceiptResponse, RPC, TransactionExecutionStatus, TransactionFinalityStatus } from "starknet"; +import { manager } from "./manager"; + +export async function ensureSuccess(receipt: GetTransactionReceiptResponse): Promise { + const tx = await manager.waitForTransaction(receipt.transaction_hash, { + successStates: [TransactionFinalityStatus.ACCEPTED_ON_L1, TransactionFinalityStatus.ACCEPTED_ON_L2], + }); + assert( + tx.execution_status == TransactionExecutionStatus.SUCCEEDED, + `Transaction ${receipt.transaction_hash} REVERTED`, + ); + return receipt as RPC.Receipt; +} + +export async function ensureAccepted(receipt: GetTransactionReceiptResponse): Promise { + await manager.waitForTransaction(receipt.transaction_hash, { + successStates: [TransactionFinalityStatus.ACCEPTED_ON_L1, TransactionFinalityStatus.ACCEPTED_ON_L2], + }); + return receipt as RPC.Receipt; +} diff --git a/lib/signers/legacy.ts b/lib/signers/legacy.ts new file mode 100644 index 0000000..0606b4b --- /dev/null +++ b/lib/signers/legacy.ts @@ -0,0 +1,60 @@ +import { ArraySignatureType, ec, encode, num } from "starknet"; +import { RawSigner } from "./signers"; + +export class LegacyArgentSigner extends RawSigner { + constructor( + public owner: LegacyStarknetKeyPair = new LegacyStarknetKeyPair(), + public guardian?: LegacyStarknetKeyPair, + ) { + super(); + } + + async signRaw(messageHash: string): Promise { + const signature = await this.owner.signRaw(messageHash); + if (this.guardian) { + const [guardianR, guardianS] = await this.guardian.signRaw(messageHash); + signature[2] = guardianR; + signature[3] = guardianS; + } + return signature; + } +} + +export abstract class LegacyKeyPair extends RawSigner { + abstract get privateKey(): string; + abstract get publicKey(): bigint; +} + +export class LegacyStarknetKeyPair extends LegacyKeyPair { + pk: string; + + constructor(pk?: string | bigint) { + super(); + this.pk = pk ? `${num.toHex(pk)}` : `0x${encode.buf2hex(ec.starkCurve.utils.randomPrivateKey())}`; + } + + public get privateKey(): string { + return this.pk; + } + + public get publicKey() { + return BigInt(ec.starkCurve.getStarkKey(this.pk)); + } + + public async signRaw(messageHash: string): Promise { + const { r, s } = ec.starkCurve.sign(messageHash, this.pk); + return [r.toString(), s.toString()]; + } +} + +export class LongSigner extends LegacyStarknetKeyPair { + public async signRaw(messageHash: string): Promise { + return ["", ...(await super.signRaw(messageHash))]; + } +} + +export class WrongSigner extends LegacyStarknetKeyPair { + public async signRaw(messageHash: string): Promise { + return ["0x1", "0x1"]; + } +} diff --git a/lib/signers/signers.ts b/lib/signers/signers.ts new file mode 100644 index 0000000..25baee5 --- /dev/null +++ b/lib/signers/signers.ts @@ -0,0 +1,128 @@ +import { + Call, + CallData, + DeclareSignerDetails, + DeployAccountSignerDetails, + InvocationsSignerDetails, + RPC, + Signature, + SignerInterface, + V2DeclareSignerDetails, + V2DeployAccountSignerDetails, + V2InvocationsSignerDetails, + V3DeclareSignerDetails, + V3DeployAccountSignerDetails, + V3InvocationsSignerDetails, + hash, + stark, + transaction, + typedData, +} from "starknet"; + +export type StarknetSignature = { + r: bigint; + s: bigint; +}; + +/** + * This class allows to easily implement custom signers by overriding the `signRaw` method. + * This is based on Starknet.js implementation of Signer, but it delegates the actual signing to an abstract function + */ +export abstract class RawSigner implements SignerInterface { + abstract signRaw(messageHash: string): Promise; + + public async getPubKey(): Promise { + throw new Error("This signer allows multiple public keys"); + } + + public async signMessage(typedDataArgument: typedData.TypedData, accountAddress: string): Promise { + const messageHash = typedData.getMessageHash(typedDataArgument, accountAddress); + return this.signRaw(messageHash); + } + + public async signTransaction(transactions: Call[], details: InvocationsSignerDetails): Promise { + const compiledCalldata = transaction.getExecuteCalldata(transactions, details.cairoVersion); + let msgHash; + + // TODO: How to do generic union discriminator for all like this + if (Object.values(RPC.ETransactionVersion2).includes(details.version as any)) { + const det = details as V2InvocationsSignerDetails; + msgHash = hash.calculateInvokeTransactionHash({ + ...det, + senderAddress: det.walletAddress, + compiledCalldata, + version: det.version, + }); + } else if (Object.values(RPC.ETransactionVersion3).includes(details.version as any)) { + const det = details as V3InvocationsSignerDetails; + msgHash = hash.calculateInvokeTransactionHash({ + ...det, + senderAddress: det.walletAddress, + compiledCalldata, + version: det.version, + nonceDataAvailabilityMode: stark.intDAM(det.nonceDataAvailabilityMode), + feeDataAvailabilityMode: stark.intDAM(det.feeDataAvailabilityMode), + }); + } else { + throw new Error("unsupported signTransaction version"); + } + return await this.signRaw(msgHash); + } + + public async signDeployAccountTransaction(details: DeployAccountSignerDetails): Promise { + const compiledConstructorCalldata = CallData.compile(details.constructorCalldata); + /* const version = BigInt(details.version).toString(); */ + let msgHash; + + if (Object.values(RPC.ETransactionVersion2).includes(details.version as any)) { + const det = details as V2DeployAccountSignerDetails; + msgHash = hash.calculateDeployAccountTransactionHash({ + ...det, + salt: det.addressSalt, + constructorCalldata: compiledConstructorCalldata, + version: det.version, + }); + } else if (Object.values(RPC.ETransactionVersion3).includes(details.version as any)) { + const det = details as V3DeployAccountSignerDetails; + msgHash = hash.calculateDeployAccountTransactionHash({ + ...det, + salt: det.addressSalt, + compiledConstructorCalldata, + version: det.version, + nonceDataAvailabilityMode: stark.intDAM(det.nonceDataAvailabilityMode), + feeDataAvailabilityMode: stark.intDAM(det.feeDataAvailabilityMode), + }); + } else { + throw new Error(`unsupported signDeployAccountTransaction version: ${details.version}}`); + } + + return await this.signRaw(msgHash); + } + + public async signDeclareTransaction( + // contractClass: ContractClass, // Should be used once class hash is present in ContractClass + details: DeclareSignerDetails, + ): Promise { + let msgHash; + + if (Object.values(RPC.ETransactionVersion2).includes(details.version as any)) { + const det = details as V2DeclareSignerDetails; + msgHash = hash.calculateDeclareTransactionHash({ + ...det, + version: det.version, + }); + } else if (Object.values(RPC.ETransactionVersion3).includes(details.version as any)) { + const det = details as V3DeclareSignerDetails; + msgHash = hash.calculateDeclareTransactionHash({ + ...det, + version: det.version, + nonceDataAvailabilityMode: stark.intDAM(det.nonceDataAvailabilityMode), + feeDataAvailabilityMode: stark.intDAM(det.feeDataAvailabilityMode), + }); + } else { + throw new Error("unsupported signDeclareTransaction version"); + } + + return await this.signRaw(msgHash); + } +} diff --git a/lib/tokens.ts b/lib/tokens.ts new file mode 100644 index 0000000..6d4bc16 --- /dev/null +++ b/lib/tokens.ts @@ -0,0 +1,48 @@ +import { Contract, num } from "starknet"; +import { Manager } from "./manager"; + +export const ethAddress = "0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7"; +export const strkAddress = "0x04718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d"; + +export class TokenManager { + private ethCache?: Contract; + private strkCache?: Contract; + + constructor(private manager: Manager) {} + + async feeTokenContract(useTxV3: boolean): Promise { + return useTxV3 ? this.strkContract() : this.ethContract(); + } + + unitTokenContract(useTxV3: boolean): "FRI" | "WEI" { + return useTxV3 ? "FRI" : "WEI"; + } + + async ethContract(): Promise { + if (this.ethCache) { + return this.ethCache; + } + const ethProxy = await this.manager.loadContract(ethAddress); + if (ethProxy.abi.some((entry) => entry.name == "implementation")) { + const { address } = await ethProxy.implementation(); + const { abi } = await this.manager.loadContract(num.toHex(address)); + this.ethCache = new Contract(abi, ethAddress, ethProxy.providerOrAccount); + } else { + this.ethCache = ethProxy; + } + return this.ethCache; + } + + async strkContract(): Promise { + if (this.strkCache) { + return this.strkCache; + } + this.strkCache = await this.manager.loadContract(strkAddress); + return this.strkCache; + } + + async tokenBalance(accountAddress: string, tokenAddress: string): Promise { + const token = await this.manager.loadContract(tokenAddress); + return await token.balance_of(accountAddress); + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..7411ccd --- /dev/null +++ b/package.json @@ -0,0 +1,32 @@ +{ + "name": "scripts_and_tests", + "version": "1.0.0", + "description": "", + "author": "", + "license": "ISC", + "type": "module", + "dependencies": { + "ethers": "6.8.1", + "starknet": "6.5.0" + }, + "devDependencies": { + "@types/node": "^20.11.30", + "typescript": "^5.4.3", + "ts-node": "^10.9.1", + "dotenv": "^16.3.1", + "@tsconfig/node18": "^2.0.0", + "@types/chai": "^4.3.4", + "@types/chai-as-promised": "^7.1.5", + "@types/lodash-es": "^4.17.8", + "@types/mocha": "^10.0.1", + "@typescript-eslint/eslint-plugin": "^5.61.0", + "@typescript-eslint/parser": "^5.61.0", + "chai": "^4.3.7", + "chai-as-promised": "^7.1.1", + "eslint": "^8.44.0", + "lodash-es": "^4.17.21", + "mocha": "^10.2.0", + "prettier": "^3.0.0", + "prettier-plugin-organize-imports": "^3.2.2" + } +} diff --git a/scripts/cancel_upgrade.ts b/scripts/cancel_upgrade.ts new file mode 100644 index 0000000..034eda8 --- /dev/null +++ b/scripts/cancel_upgrade.ts @@ -0,0 +1,18 @@ +import { logTransactionJson } from "./json_tx_builder"; + +/// To use this script, fill in the following value: +/// - factoryAddress: the address of the factory contract + +const factoryAddress = ""; + +if (!factoryAddress) { + throw new Error("Factory contract address is not set. Please set it in the script file."); +} + +const tx = { + contractAddress: factoryAddress, + entrypoint: "cancel_upgrade", + calldata: [], +}; + +logTransactionJson([tx]); diff --git a/scripts/claim_dust.ts b/scripts/claim_dust.ts new file mode 100644 index 0000000..6b2fb73 --- /dev/null +++ b/scripts/claim_dust.ts @@ -0,0 +1,43 @@ +import { CallData } from "starknet"; +import { calculateEscrowAddress } from "../lib"; +import { Gift, buildGiftCallData, executeActionOnAccount } from "../lib/claim"; +import { logTransactionJson } from "./json_tx_builder"; + +/// To use this script, fill in the following value: +/// - factoryAddress: the address of the factory contract +/// - dustReceiver: the address of the dust receiver +/// - claim: the claim object + +const factoryAddress = ""; +const dustReceiver = ""; +const claim: Gift = { + factory: factoryAddress, + escrow_class_hash: "", + sender: "", + gift_token: "", + gift_amount: 0n, + fee_token: "", + fee_amount: 0n, + gift_pubkey: 0n, +}; + +if (!factoryAddress) { + throw new Error("Factory contract address is not set. Please set it in the script file."); +} + +if (!dustReceiver) { + throw new Error("Dust receiver address is not set. Please set it in the script file."); +} + +for (const key in claim) { + if (key in claim && !claim[key as keyof typeof claim] && key !== "fee_amount" && key !== "gift_amount") { + throw new Error(`The property ${key} is empty in the claim object.`); + } +} + +const tx = executeActionOnAccount( + "claim_dust", + calculateEscrowAddress(claim), + CallData.compile([(buildGiftCallData(claim), dustReceiver)]), +); +logTransactionJson([tx]); diff --git a/scripts/declare.js b/scripts/declare.js new file mode 100644 index 0000000..f9d87b1 --- /dev/null +++ b/scripts/declare.js @@ -0,0 +1,22 @@ +import "dotenv/config"; +import fs from "fs"; +import { Account, RpcProvider, constants, json } from "starknet"; + +// connect provider +const provider = new RpcProvider({ nodeUrl: constants.NetworkName.SN_SEPOLIA }); +const account0 = new Account(provider, process.env.ACCOUNT, process.env.PRIVATE_KEY); + +// Declare Test contract in devnet +const compiledTestSierra = json.parse( + fs.readFileSync("./target/dev/argent_gifting_EscrowAccount.contract_class.json").toString("ascii"), +); +const compiledTestCasm = json.parse( + fs.readFileSync("./target/dev/argent_gifting_EscrowAccount.compiled_contract_class.json").toString("ascii"), +); +const declareResponse = await account0.declare({ + contract: compiledTestSierra, + casm: compiledTestCasm, +}); +console.log("Test Contract declared with classHash =", declareResponse.class_hash); +await provider.waitForTransaction(declareResponse.transaction_hash); +console.log("βœ… Test Completed."); diff --git a/scripts/deploy.ts b/scripts/deploy.ts new file mode 100644 index 0000000..546e776 --- /dev/null +++ b/scripts/deploy.ts @@ -0,0 +1,15 @@ +import { num } from "starknet"; +import { manager, protocolCache, setupGiftProtocol } from "../lib"; + +const { factory, escrowAccountClassHash, escrowLibraryClassHash } = await setupGiftProtocol(); + +console.log("GiftFactory classhash:", await manager.getClassHashAt(factory.address)); +console.log("GiftFactory address:", factory.address); +console.log("GiftFactory owner:", num.toHex(await factory.owner())); +console.log("EscrowAccount class hash:", escrowAccountClassHash); +console.log("EscrowLibrary class hash:", escrowLibraryClassHash); + +// clear from cache just in case +delete protocolCache["GiftFactory"]; +delete protocolCache["EscrowLibrary"]; +delete protocolCache["EscrowAccount"]; diff --git a/scripts/json_tx_builder.ts b/scripts/json_tx_builder.ts new file mode 100644 index 0000000..31f2b8d --- /dev/null +++ b/scripts/json_tx_builder.ts @@ -0,0 +1,5 @@ +import { Call } from "starknet"; + +export function logTransactionJson(transaction: Call[]) { + console.log(JSON.stringify(transaction, null, 2)); +} diff --git a/scripts/pause.ts b/scripts/pause.ts new file mode 100644 index 0000000..c7c1889 --- /dev/null +++ b/scripts/pause.ts @@ -0,0 +1,18 @@ +import { logTransactionJson } from "./json_tx_builder"; + +/// To use this script, fill in the following value: +/// - factoryAddress: the address of the factory contract + +const factoryAddress = ""; + +if (!factoryAddress) { + throw new Error("Factory contract address is not set. Please set it in the script file."); +} + +const tx = { + contractAddress: factoryAddress, + entrypoint: "pause", + calldata: [], +}; + +logTransactionJson([tx]); diff --git a/scripts/profile.ts b/scripts/profile.ts new file mode 100644 index 0000000..05d9516 --- /dev/null +++ b/scripts/profile.ts @@ -0,0 +1,118 @@ +import { CallData } from "starknet"; +import { + buildGiftCallData, + calculateEscrowAddress, + claimDust, + claimExternal, + claimInternal, + defaultDepositTestSetup, + deployer, + executeActionOnAccount, + manager, + randomReceiver, + setDefaultTransactionVersionV3, + setupGiftProtocol, +} from "../lib"; +import { newProfiler } from "../lib/gas"; + +// TODO add this in CI, skipped atm to avoid false failing tests + +const profiler = newProfiler(manager); + +await manager.restart(); +manager.clearClassCache(); + +const ethContract = await manager.tokens.ethContract(); +const strkContract = await manager.tokens.strkContract(); + +const tokens = [ + { giftTokenContract: ethContract, unit: "WEI" }, + { giftTokenContract: strkContract, unit: "FRI" }, +]; + +ethContract.connect(deployer); +await profiler.profile( + `Transfer ETH (FeeToken: ${manager.tokens.unitTokenContract(false)})`, + await ethContract.transfer(randomReceiver(), 1), +); + +strkContract.connect(deployer); +await profiler.profile( + `Transfer STRK (FeeToken: ${manager.tokens.unitTokenContract(false)})`, + await strkContract.transfer(randomReceiver(), 1), +); + +const receiver = "0x42"; +const { factory } = await setupGiftProtocol(); + +for (const { giftTokenContract, unit } of tokens) { + for (const useTxV3 of [false, true]) { + // Profiling deposit + const { txReceipt, gift, giftPrivateKey } = await defaultDepositTestSetup({ + factory, + useTxV3, + overrides: { + giftPrivateKey: 42n, + giftTokenAddress: giftTokenContract.address, + }, + }); + + const { gift: claimExternalGift, giftPrivateKey: giftPrivateKeyExternal } = await defaultDepositTestSetup({ + factory, + useTxV3, + overrides: { + giftPrivateKey: 43n, + giftTokenAddress: giftTokenContract.address, + }, + }); + + await profiler.profile(`Gifting ${unit} (FeeToken: ${manager.tokens.unitTokenContract(useTxV3)})`, txReceipt); + + // Profiling claim internal + await profiler.profile( + `Claiming ${unit} (FeeToken: ${manager.tokens.unitTokenContract(useTxV3)})`, + await claimInternal({ gift, receiver, giftPrivateKey: giftPrivateKey }), + ); + + // Profiling claim external + await profiler.profile( + `Claiming external ${unit} (FeeToken: ${manager.tokens.unitTokenContract(useTxV3)})`, + await claimExternal({ gift: claimExternalGift, receiver, useTxV3, giftPrivateKey: giftPrivateKeyExternal }), + ); + + // Profiling getting the dust + const account = useTxV3 ? setDefaultTransactionVersionV3(deployer) : deployer; + factory.connect(account); + await profiler.profile( + `Get dust ${unit} (FeeToken: ${manager.tokens.unitTokenContract(useTxV3)})`, + await claimDust({ gift, receiver: deployer.address }), + ); + } +} + +const limits = [2, 3, 4, 5]; +for (const limit of limits) { + const claimDustCalls = []; + for (let i = 0; i < limit; i++) { + const { gift, giftPrivateKey } = await defaultDepositTestSetup({ + factory, + overrides: { + giftTokenAddress: ethContract.address, + }, + }); + + await claimInternal({ gift, receiver, giftPrivateKey: giftPrivateKey }); + const claimDustCallData = CallData.compile([buildGiftCallData(gift), receiver]); + const call = executeActionOnAccount("claim_dust", calculateEscrowAddress(gift), claimDustCallData); + + claimDustCalls.push(call); + } + + await profiler.profile( + `Get dust ${limit} (FeeToken: ${manager.tokens.unitTokenContract(false)})`, + await deployer.execute(claimDustCalls), + ); +} + +profiler.printSummary(); +profiler.updateOrCheckReport(); diff --git a/scripts/propose_upgrade.ts b/scripts/propose_upgrade.ts new file mode 100644 index 0000000..49e64bf --- /dev/null +++ b/scripts/propose_upgrade.ts @@ -0,0 +1,33 @@ +import { CallData } from "starknet"; +import { logTransactionJson } from "./json_tx_builder"; + +/// To use this script, fill in the following value: +/// - factoryAddress: the address of the factory contract +/// - newImplementation: the class ahs of the new implementation contract +/// - callData: the call data for the propose_upgrade function + +const factoryAddress = ""; +const newImplementation = ""; +const callData: any[] = []; + +if (!factoryAddress) { + throw new Error("Factory contract address is not set. Please set it in the script file."); +} + +if (!newImplementation) { + throw new Error("New implementation class hash is not set. Please set it in the script file."); +} + +const tx = { + contractAddress: factoryAddress, + entrypoint: "propose_upgrade", + calldata: CallData.compile([newImplementation, callData]), +}; + +// date 7 days from now +const date = new Date(); +date.setDate(date.getDate() + 7); + +logTransactionJson([tx]); + +console.log("Proposed upgrade will be ready at: ", date); diff --git a/scripts/unpause.ts b/scripts/unpause.ts new file mode 100644 index 0000000..3c64e00 --- /dev/null +++ b/scripts/unpause.ts @@ -0,0 +1,18 @@ +import { logTransactionJson } from "./json_tx_builder"; + +/// To use this script, fill in the following value: +/// - factoryAddress: the address of the factory contract + +const factoryAddress = ""; + +if (!factoryAddress) { + throw new Error("Factory contract address is not set. Please set it in the script file."); +} + +const tx = { + contractAddress: factoryAddress, + entrypoint: "unpause", + calldata: [], +}; + +logTransactionJson([tx]); diff --git a/scripts/upgrade.ts b/scripts/upgrade.ts new file mode 100644 index 0000000..9168a60 --- /dev/null +++ b/scripts/upgrade.ts @@ -0,0 +1,22 @@ +import { CallData } from "starknet"; +import { logTransactionJson } from "./json_tx_builder"; + +/// To use this script, fill in the following value: +/// - factoryAddress: the address of the factory contract +/// - callData: upgrade call data + +const factoryAddress = ""; + +const callData: any[] = []; + +if (!factoryAddress) { + throw new Error("Factory contract address is not set. Please set it in the script file."); +} + +const tx = { + contractAddress: factoryAddress, + entrypoint: "upgrade", + calldata: CallData.compile(callData), +}; + +logTransactionJson([tx]); diff --git a/src/contracts/claim_hash.cairo b/src/contracts/claim_hash.cairo new file mode 100644 index 0000000..5b364fb --- /dev/null +++ b/src/contracts/claim_hash.cairo @@ -0,0 +1,104 @@ +use core::hash::HashStateTrait; +use core::poseidon::{poseidon_hash_span, hades_permutation, HashState}; +use starknet::{ContractAddress, get_tx_info}; + +/// @notice Defines the function to generate the SNIP-12 revision 1 compliant message hash +pub trait IOffChainMessageHashRev1 { + fn get_message_hash_rev_1(self: @T, account: ContractAddress) -> felt252; +} + +/// @notice Defines the function to generates the SNIP-12 revision 1 compliant hash on an object +pub trait IStructHashRev1 { + fn get_struct_hash_rev_1(self: @T) -> felt252; +} + +/// @notice StarkNetDomain using SNIP 12 Revision 1 +#[derive(Drop, Copy)] +pub struct StarknetDomain { + pub name: felt252, + pub version: felt252, + pub chain_id: felt252, + pub revision: felt252, +} + +/// @notice The SNIP-12 message that needs to be signed when using claim_external +/// @param receiver The receiver of the gift +#[derive(Drop, Copy)] +pub struct ClaimExternal { + pub receiver: ContractAddress, + pub dust_receiver: ContractAddress, +} + +const STARKNET_DOMAIN_TYPE_HASH_REV_1: felt252 = + selector!( + "\"StarknetDomain\"(\"name\":\"shortstring\",\"version\":\"shortstring\",\"chainId\":\"shortstring\",\"revision\":\"shortstring\")" + ); + +const CLAIM_EXTERNAL_TYPE_HASH_REV_1: felt252 = + selector!("\"ClaimExternal\"(\"receiver\":\"ContractAddress\",\"dust receiver\":\"ContractAddress\")"); + +impl StructHashStarknetDomain of IStructHashRev1 { + fn get_struct_hash_rev_1(self: @StarknetDomain) -> felt252 { + poseidon_hash_span( + array![STARKNET_DOMAIN_TYPE_HASH_REV_1, *self.name, *self.version, *self.chain_id, *self.revision].span() + ) + } +} + +impl StructHashClaimExternal of IStructHashRev1 { + fn get_struct_hash_rev_1(self: @ClaimExternal) -> felt252 { + poseidon_hash_span( + array![ + CLAIM_EXTERNAL_TYPE_HASH_REV_1, + (*self).receiver.try_into().expect('receiver'), + (*self).dust_receiver.try_into().expect('dust receiver') + ] + .span() + ) + } +} + +pub const MAINNET_FIRST_HADES_PERMUTATION: (felt252, felt252, felt252) = + ( + 51327417978415965208169103467166821837258659346127673007877596566411752209, + 404713855488389632083006643023042313437307371031291768022239836903948396963, + 389369424440010405916079789663583430968252485429471935476783216782654849452 + ); + +pub const SEPOLIA_FIRST_HADES_PERMUTATION: (felt252, felt252, felt252) = + ( + 3490629689183768224029659172482831330773656358583155290029264631185823046188, + 2282067178720039168203625096855019793380766562534282834247329930463326923381, + 3105849593939290506670850151949399226662980212920556211540197981933140560183 + ); + + +impl ClaimExternalHash of IOffChainMessageHashRev1 { + fn get_message_hash_rev_1(self: @ClaimExternal, account: ContractAddress) -> felt252 { + let chain_id = get_tx_info().unbox().chain_id; + if chain_id == 'SN_MAIN' { + return get_message_hash_rev_1_with_precalc(MAINNET_FIRST_HADES_PERMUTATION, account, *self); + } + + if chain_id == 'SN_SEPOLIA' { + return get_message_hash_rev_1_with_precalc(SEPOLIA_FIRST_HADES_PERMUTATION, account, *self); + } + + let domain = StarknetDomain { name: 'GiftFactory.claim_external', version: '1', chain_id, revision: 1 }; + poseidon_hash_span( + array!['StarkNet Message', domain.get_struct_hash_rev_1(), account.into(), self.get_struct_hash_rev_1()] + .span() + ) + } +} + +pub fn get_message_hash_rev_1_with_precalc, +IStructHashRev1>( + hades_permutation_state: (felt252, felt252, felt252), account: ContractAddress, rev1_struct: T +) -> felt252 { + // mainnet_domain_hash = domain.get_struct_hash_rev_1() + // hades_permutation_state == hades_permutation('StarkNet Message', mainnet_domain_hash, 0); + let (s0, s1, s2) = hades_permutation_state; + + let (fs0, fs1, fs2) = hades_permutation(s0 + account.into(), s1 + rev1_struct.get_struct_hash_rev_1(), s2); + HashState { s0: fs0, s1: fs1, s2: fs2, odd: false }.finalize() +} diff --git a/src/contracts/escrow_account.cairo b/src/contracts/escrow_account.cairo new file mode 100644 index 0000000..002f2ac --- /dev/null +++ b/src/contracts/escrow_account.cairo @@ -0,0 +1,201 @@ +use starknet::{ContractAddress, ClassHash, account::Call}; + +#[starknet::interface] +pub trait IAccount { + fn __validate__(ref self: TContractState, calls: Array) -> felt252; + fn __execute__(ref self: TContractState, calls: Array) -> Array>; + fn is_valid_signature(self: @TContractState, hash: felt252, signature: Array) -> felt252; + fn supports_interface(self: @TContractState, interface_id: felt252) -> bool; +} + + +#[starknet::interface] +pub trait IEscrowAccount { + /// @notice delegates an action to the account library + fn execute_action(ref self: TContractState, selector: felt252, calldata: Array) -> Span; +} + +/// @notice Struct representing the arguments required for constructing an escrow account +/// @dev This will be used to determine the address of the escrow account +/// @param sender The address of the sender +/// @param gift_token The ERC-20 token address of the gift +/// @param gift_amount The amount of the gift +/// @param fee_token The ERC-20 token address of the fee +/// @param fee_amount The amount of the fee +/// @param gift_pubkey The public key associated with the gift +#[derive(Serde, Drop, Copy)] +pub struct AccountConstructorArguments { + pub sender: ContractAddress, + pub gift_token: ContractAddress, + pub gift_amount: u256, + pub fee_token: ContractAddress, + pub fee_amount: u128, + pub gift_pubkey: felt252 +} + +#[starknet::contract(account)] +mod EscrowAccount { + use argent_gifting::contracts::escrow_library::{IEscrowLibraryLibraryDispatcher, IEscrowLibraryDispatcherTrait}; + use argent_gifting::contracts::gift_data::GiftData; + use argent_gifting::contracts::gift_factory::{IGiftFactory, IGiftFactoryDispatcher, IGiftFactoryDispatcherTrait}; + use argent_gifting::contracts::outside_execution::{ + IOutsideExecution, OutsideExecution, ERC165_OUTSIDE_EXECUTION_INTERFACE_ID_VERSION_2 + }; + + use argent_gifting::contracts::utils::{ + calculate_escrow_account_address, full_deserialize, serialize, STRK_ADDRESS, ETH_ADDRESS, TX_V1_ESTIMATE, TX_V1, + TX_V3, TX_V3_ESTIMATE + }; + use core::ecdsa::check_ecdsa_signature; + use core::num::traits::Zero; + use starknet::{ + TxInfo, account::Call, VALIDATED, syscalls::library_call_syscall, ContractAddress, get_contract_address, + get_execution_info, ClassHash + }; + use super::{IEscrowAccount, IAccount, AccountConstructorArguments}; + + // https://github.com/starknet-io/SNIPs/blob/main/SNIPS/snip-5.md + const SRC5_INTERFACE_ID: felt252 = 0x3f918d17e5ee77373b56385708f855659a07f75997f365cf87748628532a055; + // https://github.com/starknet-io/SNIPs/blob/main/SNIPS/snip-6.md + const SRC5_ACCOUNT_INTERFACE_ID: felt252 = 0x2ceccef7f994940b3962a6c67e0ba4fcd37df7d131417c604f91e03caecc1cd; + + #[storage] + struct Storage { + /// Keeps track of used nonces for outside transactions (`execute_from_outside`) + outside_nonces: LegacyMap, + } + + #[event] + #[derive(Drop, starknet::Event)] + enum Event {} + + #[constructor] + fn constructor(ref self: ContractState, args: AccountConstructorArguments) {} + + #[abi(embed_v0)] + impl IAccountImpl of IAccount { + fn __validate__(ref self: ContractState, calls: Array) -> felt252 { + let execution_info = get_execution_info().unbox(); + assert(execution_info.caller_address.is_zero(), 'escrow/only-protocol'); + let tx_info = execution_info.tx_info.unbox(); + // When paymaster is implemented on starknet it might break the logic in this method + assert(tx_info.paymaster_data.is_empty(), 'escrow/unsupported-paymaster'); + // No need to allow deployment + assert(tx_info.account_deployment_data.is_empty(), 'escrow/invalid-deployment-data'); + + assert(calls.len() == 1, 'escrow/invalid-call-len'); + let Call { to, selector, calldata } = calls.at(0); + assert(*to == get_contract_address(), 'escrow/invalid-call-to'); + assert(*selector == selector!("claim_internal"), 'escrow/invalid-call-selector'); + let (gift, _): (GiftData, ContractAddress) = full_deserialize(*calldata).expect('escrow/invalid-calldata'); + assert_valid_claim(gift); + + assert(tx_info.nonce == 0, 'escrow/invalid-gift-nonce'); + let execution_hash = tx_info.transaction_hash; + let signature = tx_info.signature; + assert(signature.len() == 2, 'escrow/invalid-signature-len'); + + let tx_version = tx_info.version; + assert( + check_ecdsa_signature(execution_hash, gift.gift_pubkey, *signature[0], *signature[1]) + || tx_version == TX_V3_ESTIMATE + || tx_version == TX_V1_ESTIMATE, + 'escrow/invalid-signature' + ); + if gift.fee_token == STRK_ADDRESS() { + assert(tx_version == TX_V3 || tx_version == TX_V3_ESTIMATE, 'escrow/invalid-tx3-version'); + let tx_fee = compute_max_fee_v3(tx_info, tx_info.tip); + assert(tx_fee <= gift.fee_amount, 'escrow/max-fee-too-high-v3'); + } else if gift.fee_token == ETH_ADDRESS() { + assert(tx_version == TX_V1 || tx_version == TX_V1_ESTIMATE, 'escrow/invalid-tx1-version'); + assert(tx_info.max_fee <= gift.fee_amount, 'escrow/max-fee-too-high-v1'); + } else { + core::panic_with_felt252('escrow/invalid-token-fee'); + } + VALIDATED + } + + fn __execute__(ref self: ContractState, calls: Array) -> Array> { + let execution_info = get_execution_info().unbox(); + assert(execution_info.caller_address.is_zero(), 'escrow/only-protocol'); + let tx_version = execution_info.tx_info.unbox().version; + assert( + tx_version == TX_V3 + || tx_version == TX_V1 + || tx_version == TX_V3_ESTIMATE + || tx_version == TX_V1_ESTIMATE, + 'escrow/invalid-tx-version' + ); + let Call { .., calldata }: @Call = calls[0]; + let (gift, receiver): (GiftData, ContractAddress) = full_deserialize(*calldata) + .expect('escrow/invalid-calldata'); + // The __validate__ function already ensures the claim is valid + let library_class_hash: ClassHash = IGiftFactoryDispatcher { contract_address: gift.factory } + .get_escrow_lib_class_hash(gift.escrow_class_hash); + IEscrowLibraryLibraryDispatcher { class_hash: library_class_hash }.claim_internal(gift, receiver) + } + + fn is_valid_signature(self: @ContractState, hash: felt252, signature: Array) -> felt252 { + let mut signature_span = signature.span(); + let gift: GiftData = Serde::deserialize(ref signature_span).expect('escrow/invalid-gift'); + get_validated_lib(gift).is_valid_account_signature(gift, hash, signature_span) + } + + fn supports_interface(self: @ContractState, interface_id: felt252) -> bool { + interface_id == SRC5_INTERFACE_ID + || interface_id == SRC5_ACCOUNT_INTERFACE_ID + || interface_id == ERC165_OUTSIDE_EXECUTION_INTERFACE_ID_VERSION_2 + } + } + + #[abi(embed_v0)] + impl GiftAccountImpl of IEscrowAccount { + fn execute_action(ref self: ContractState, selector: felt252, calldata: Array) -> Span { + let mut calldata_span = calldata.span(); + let gift: GiftData = Serde::deserialize(ref calldata_span).expect('escrow/invalid-gift'); + let lib = get_validated_lib(gift); + lib.execute_action(lib.class_hash, selector, calldata.span()) + } + } + + #[abi(embed_v0)] + impl OutsideExecutionImpl of IOutsideExecution { + fn execute_from_outside_v2( + ref self: ContractState, outside_execution: OutsideExecution, mut signature: Span + ) -> Array> { + let gift: GiftData = Serde::deserialize(ref signature).expect('escrow/invalid-gift'); + get_validated_lib(gift).execute_from_outside_v2(gift, outside_execution, signature) + } + + fn is_valid_outside_execution_nonce(self: @ContractState, nonce: felt252) -> bool { + !self.outside_nonces.read(nonce) + } + } + + fn get_validated_lib(gift: GiftData) -> IEscrowLibraryLibraryDispatcher { + assert_valid_claim(gift); + let library_class_hash = IGiftFactoryDispatcher { contract_address: gift.factory } + .get_escrow_lib_class_hash(gift.escrow_class_hash); + IEscrowLibraryLibraryDispatcher { class_hash: library_class_hash } + } + + fn assert_valid_claim(gift: GiftData) { + let calculated_address = calculate_escrow_account_address(gift); + assert(calculated_address == get_contract_address(), 'escrow/invalid-escrow-address'); + } + + fn compute_max_fee_v3(tx_info: TxInfo, tip: u128) -> u128 { + let mut resource_bounds = tx_info.resource_bounds; + let mut max_fee: u128 = 0; + let mut max_tip: u128 = 0; + while let Option::Some(r) = resource_bounds + .pop_front() { + let max_resource_amount: u128 = (*r.max_amount).into(); + max_fee += *r.max_price_per_unit * max_resource_amount; + if *r.resource == 'L2_GAS' { + max_tip += tip * max_resource_amount; + } + }; + max_fee + max_tip + } +} diff --git a/src/contracts/escrow_library.cairo b/src/contracts/escrow_library.cairo new file mode 100644 index 0000000..b72121a --- /dev/null +++ b/src/contracts/escrow_library.cairo @@ -0,0 +1,202 @@ +use argent_gifting::contracts::gift_data::GiftData; +use argent_gifting::contracts::outside_execution::OutsideExecution; +use argent_gifting::contracts::utils::StarknetSignature; +use starknet::{ContractAddress, ClassHash}; + +#[starknet::interface] +pub trait IEscrowLibrary { + fn execute_action( + ref self: TContractState, this_class_hash: ClassHash, selector: felt252, args: Span + ) -> Span; + + fn claim_internal(ref self: TContractState, gift: GiftData, receiver: ContractAddress) -> Array>; + + fn claim_external( + ref self: TContractState, + gift: GiftData, + receiver: ContractAddress, + dust_receiver: ContractAddress, + signature: StarknetSignature + ); + + /// @notice Allows the sender of a gift to cancel their gift + /// @dev Will refund both the gift and the fee + /// @param gift The data of the gift to cancel + fn cancel(ref self: TContractState, gift: GiftData); + + /// @notice Allows the owner of the factory to claim the dust (leftovers) of a gift + /// @dev Only allowed if the gift has been claimed + /// @param gift The gift data + /// @param receiver The address of the receiver + fn claim_dust(ref self: TContractState, gift: GiftData, receiver: ContractAddress); + + fn is_valid_account_signature( + self: @TContractState, gift: GiftData, hash: felt252, remaining_signature: Span + ) -> felt252; + + fn execute_from_outside_v2( + ref self: TContractState, gift: GiftData, outside_execution: OutsideExecution, signature: Span + ) -> Array>; +} + +#[starknet::contract] +mod EscrowLibrary { + use argent_gifting::contracts::claim_hash::{ClaimExternal, IOffChainMessageHashRev1}; + use argent_gifting::contracts::gift_data::GiftData; + use argent_gifting::contracts::outside_execution::OutsideExecution; + use argent_gifting::contracts::utils::StarknetSignature; + use core::ecdsa::check_ecdsa_signature; + use core::num::traits::zero::Zero; + use core::panic_with_felt252; + use openzeppelin::access::ownable::interface::{IOwnable, IOwnableDispatcherTrait, IOwnableDispatcher}; + use openzeppelin::token::erc20::interface::{IERC20, IERC20DispatcherTrait, IERC20Dispatcher}; + use starknet::{ + ClassHash, ContractAddress, get_caller_address, get_contract_address, syscalls::library_call_syscall + }; + + #[storage] + struct Storage {} + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + GiftClaimed: GiftClaimed, + GiftCancelled: GiftCancelled, + } + + #[derive(Drop, starknet::Event)] + struct GiftClaimed { + receiver: ContractAddress, + dust_receiver: ContractAddress + } + + #[derive(Drop, starknet::Event)] + struct GiftCancelled {} + + #[constructor] + fn constructor(ref self: ContractState) { + // This prevents creating instances of this classhash by mistake, as it's not needed. + // While it is technically possible to create instances by replacing classhashes, this practice is not recommended. + // This contract is intended to be used exclusively through library calls. + panic_with_felt252('escr-lib/instance-not-recommend') + } + + #[abi(embed_v0)] + impl EscrowLibraryImpl of super::IEscrowLibrary { + fn claim_internal(ref self: ContractState, gift: GiftData, receiver: ContractAddress) -> Array> { + self.proceed_with_claim(gift, receiver, Zero::zero()); + array![] + } + + fn execute_action( + ref self: ContractState, this_class_hash: ClassHash, selector: felt252, args: Span + ) -> Span { + // This is needed to make sure no arbitrary methods can be called directly using `execute_action` in the escrow account + // Some methods like `claim_internal` should only be called after some checks are performed in the escrow account + let is_whitelisted = selector == selector!("claim_external") + || selector == selector!("claim_dust") + || selector == selector!("cancel"); + assert(is_whitelisted, 'escr-lib/invalid-selector'); + library_call_syscall(this_class_hash, selector, args).unwrap() + } + + fn claim_external( + ref self: ContractState, + gift: GiftData, + receiver: ContractAddress, + dust_receiver: ContractAddress, + signature: StarknetSignature + ) { + let claim_external_hash = ClaimExternal { receiver, dust_receiver } + .get_message_hash_rev_1(get_contract_address()); + assert( + check_ecdsa_signature(claim_external_hash, gift.gift_pubkey, signature.r, signature.s), + 'escr-lib/invalid-ext-signature' + ); + self.proceed_with_claim(gift, receiver, dust_receiver); + } + + fn cancel(ref self: ContractState, gift: GiftData) { + let contract_address = get_contract_address(); + assert(get_caller_address() == gift.sender, 'escr-lib/wrong-sender'); + let gift_balance = balance_of(gift.gift_token, contract_address); + assert(gift_balance >= gift.gift_amount, 'escr-lib/claimed-or-cancel'); + if gift.gift_token == gift.fee_token { + // Sender also gets the dust + transfer_from_account(gift.gift_token, gift.sender, gift_balance); + } else { + // Transfer both tokens + let fee_balance = balance_of(gift.fee_token, contract_address); + transfer_from_account(gift.gift_token, gift.sender, gift_balance); + transfer_from_account(gift.fee_token, gift.sender, fee_balance); + } + self.emit(GiftCancelled {}); + } + + fn claim_dust(ref self: ContractState, gift: GiftData, receiver: ContractAddress) { + let contract_address = get_contract_address(); + let factory_owner = IOwnableDispatcher { contract_address: gift.factory }.owner(); + assert(factory_owner == get_caller_address(), 'escr-lib/only-factory-owner'); + let gift_balance = balance_of(gift.gift_token, contract_address); + assert(gift_balance < gift.gift_amount, 'escr-lib/not-yet-claimed'); + if gift.gift_token == gift.fee_token { + transfer_from_account(gift.gift_token, receiver, gift_balance); + } else { + let fee_balance = balance_of(gift.fee_token, contract_address); + transfer_from_account(gift.fee_token, gift.sender, fee_balance); + } + } + + fn is_valid_account_signature( + self: @ContractState, gift: GiftData, hash: felt252, mut remaining_signature: Span + ) -> felt252 { + 0 // Accounts don't support off-chain signatures yet + } + + fn execute_from_outside_v2( + ref self: ContractState, gift: GiftData, outside_execution: OutsideExecution, signature: Span + ) -> Array> { + panic_with_felt252('escr-lib/not-allowed-yet') + } + } + + #[generate_trait] + impl Private of PrivateTrait { + fn proceed_with_claim( + ref self: ContractState, gift: GiftData, receiver: ContractAddress, dust_receiver: ContractAddress + ) { + assert(receiver.is_non_zero(), 'escr-lib/zero-receiver'); + let contract_address = get_contract_address(); + let gift_balance = balance_of(gift.gift_token, contract_address); + assert(gift_balance >= gift.gift_amount, 'escr-lib/claimed-or-cancel'); + + // could be optimized to 1 transfer only when the receiver is also the dust receiver, + // and the fee token is the same as the gift token + // but will increase the complexity of the code for a small performance + + // Transfer the gift + transfer_from_account(gift.gift_token, receiver, gift.gift_amount); + + // Transfer the dust + if dust_receiver.is_non_zero() { + let dust = if gift.gift_token == gift.fee_token { + gift_balance - gift.gift_amount + } else { + balance_of(gift.fee_token, contract_address) + }; + if dust > 0 { + transfer_from_account(gift.fee_token, dust_receiver, dust); + } + } + self.emit(GiftClaimed { receiver, dust_receiver }); + } + } + + fn transfer_from_account(token: ContractAddress, receiver: ContractAddress, amount: u256,) { + assert(IERC20Dispatcher { contract_address: token }.transfer(receiver, amount), 'escr-lib/transfer-failed'); + } + + fn balance_of(token: ContractAddress, account: ContractAddress) -> u256 { + IERC20Dispatcher { contract_address: token }.balance_of(account) + } +} diff --git a/src/contracts/gift_data.cairo b/src/contracts/gift_data.cairo new file mode 100644 index 0000000..dd2377a --- /dev/null +++ b/src/contracts/gift_data.cairo @@ -0,0 +1,23 @@ +use starknet::{ContractAddress, ClassHash}; + + +/// @notice Struct representing the data required for a claiming a gift +/// @param factory The address of the factory +/// @param escrow_class_hash The class hash of the escrow account +/// @param sender The address of the sender +/// @param gift_token The ERC-20 token address of the gift +/// @param gift_amount The amount of the gift +/// @param fee_token The ERC-20 token address of the fee +/// @param fee_amount The amount of the fee +/// @param gift_pubkey The public key associated with the gift +#[derive(Serde, Drop, Copy)] +pub struct GiftData { + pub factory: ContractAddress, + pub escrow_class_hash: ClassHash, + pub sender: ContractAddress, + pub gift_token: ContractAddress, + pub gift_amount: u256, + pub fee_token: ContractAddress, + pub fee_amount: u128, + pub gift_pubkey: felt252 +} diff --git a/src/contracts/gift_factory.cairo b/src/contracts/gift_factory.cairo new file mode 100644 index 0000000..5b6e302 --- /dev/null +++ b/src/contracts/gift_factory.cairo @@ -0,0 +1,252 @@ +use starknet::{ContractAddress, ClassHash}; + +#[starknet::interface] +pub trait IGiftFactory { + /// @notice Creates a new gift + /// @dev This function can be paused by the owner of the factory and prevent any further deposits + /// @param escrow_class_hash The class hash of the escrow account (needed in FE to have an optimistic UI) + /// @param gift_token The ERC-20 token address of the gift + /// @param gift_amount The amount of the gift + /// @param fee_token The ERC-20 token address of the fee (can ONLY be ETH or STARK address) used to claim the gift through claim_internal + /// @param fee_amount The amount of the fee + /// @param gift_pubkey The public key associated with the gift + fn deposit( + ref self: TContractState, + escrow_class_hash: ClassHash, + gift_token: ContractAddress, + gift_amount: u256, + fee_token: ContractAddress, + fee_amount: u128, + gift_pubkey: felt252 + ); + + /// @notice Retrieves the current clash hash used for creating an escrow account + fn get_latest_escrow_class_hash(self: @TContractState) -> ClassHash; + + /// @notice Retrieves the current class hash of the escrow account's library + fn get_escrow_lib_class_hash(self: @TContractState, escrow_class_hash: ClassHash) -> ClassHash; + + /// @notice Get the address of the escrow account contract given all parameters + /// @param escrow_class_hash The class hash of the escrow account + /// @param sender The address of the sender + /// @param gift_token The ERC-20 token address of the gift + /// @param gift_amount The amount of the gift + /// @param fee_token The ERC-20 token address of the fee + /// @param fee_amount The amount of the fee + /// @param gift_pubkey The public key associated with the gift + fn get_escrow_address( + self: @TContractState, + escrow_class_hash: ClassHash, + sender: ContractAddress, + gift_token: ContractAddress, + gift_amount: u256, + fee_token: ContractAddress, + fee_amount: u128, + gift_pubkey: felt252 + ) -> ContractAddress; +} + + +#[starknet::contract] +mod GiftFactory { + use argent_gifting::contracts::claim_hash::{ClaimExternal, IOffChainMessageHashRev1}; + use argent_gifting::contracts::escrow_account::{ + IEscrowAccount, IEscrowAccountDispatcher, AccountConstructorArguments + }; + use argent_gifting::contracts::gift_data::GiftData; + use argent_gifting::contracts::gift_factory::IGiftFactory; + use argent_gifting::contracts::timelock_upgrade::{ITimelockUpgradeCallback, TimelockUpgradeComponent}; + use argent_gifting::contracts::utils::{ + calculate_escrow_account_address, STRK_ADDRESS, ETH_ADDRESS, serialize, full_deserialize + }; + use core::ecdsa::check_ecdsa_signature; + use core::num::traits::zero::Zero; + use core::panic_with_felt252; + use openzeppelin::access::ownable::OwnableComponent; + use openzeppelin::security::PausableComponent; + use openzeppelin::token::erc20::interface::{IERC20, IERC20DispatcherTrait, IERC20Dispatcher}; + use starknet::{ + ClassHash, ContractAddress, syscalls::deploy_syscall, get_caller_address, get_contract_address, account::Call, + get_block_timestamp + }; + + // Ownable + component!(path: OwnableComponent, storage: ownable, event: OwnableEvent); + #[abi(embed_v0)] + impl OwnableImpl = OwnableComponent::OwnableImpl; + impl InternalImpl = OwnableComponent::InternalImpl; + impl TimelockUpgradeInternalImpl = TimelockUpgradeComponent::TimelockUpgradeInternalImpl; + + // Pausable + component!(path: PausableComponent, storage: pausable, event: PausableEvent); + #[abi(embed_v0)] + impl PausableImpl = PausableComponent::PausableImpl; + impl PausableInternalImpl = PausableComponent::InternalImpl; + + + // TimelockUpgradeable + component!(path: TimelockUpgradeComponent, storage: timelock_upgrade, event: TimelockUpgradeEvent); + #[abi(embed_v0)] + impl TimelockUpgradeImpl = TimelockUpgradeComponent::TimelockUpgradeImpl; + + #[storage] + struct Storage { + #[substorage(v0)] + ownable: OwnableComponent::Storage, + #[substorage(v0)] + pausable: PausableComponent::Storage, + #[substorage(v0)] + timelock_upgrade: TimelockUpgradeComponent::Storage, + escrow_class_hash: ClassHash, + escrow_lib_class_hash: ClassHash, + } + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + #[flat] + OwnableEvent: OwnableComponent::Event, + #[flat] + PausableEvent: PausableComponent::Event, + #[flat] + TimelockUpgradeEvent: TimelockUpgradeComponent::Event, + GiftCreated: GiftCreated, + } + + #[derive(Drop, starknet::Event)] + struct GiftCreated { + #[key] // If you have the ContractAddress you can find back the gift + escrow_address: ContractAddress, + #[key] // Find all gifts from a specific sender + sender: ContractAddress, + escrow_class_hash: ClassHash, + gift_token: ContractAddress, + gift_amount: u256, + fee_token: ContractAddress, + fee_amount: u128, + gift_pubkey: felt252 + } + + #[constructor] + fn constructor( + ref self: ContractState, escrow_class_hash: ClassHash, escrow_lib_class_hash: ClassHash, owner: ContractAddress + ) { + self.escrow_class_hash.write(escrow_class_hash); + self.escrow_lib_class_hash.write(escrow_lib_class_hash); + self.ownable.initializer(owner); + } + + #[abi(embed_v0)] + impl GiftFactoryImpl of IGiftFactory { + fn deposit( + ref self: ContractState, + escrow_class_hash: ClassHash, + gift_token: ContractAddress, + gift_amount: u256, + fee_token: ContractAddress, + fee_amount: u128, + gift_pubkey: felt252 + ) { + self.pausable.assert_not_paused(); + assert(fee_token == STRK_ADDRESS() || fee_token == ETH_ADDRESS(), 'gift-fac/invalid-fee-token'); + if gift_token == fee_token { + // This is needed so we can tell if a gift has been claimed or not just by looking at the balances + assert(fee_amount.into() < gift_amount, 'gift-fac/fee-too-high'); + } + + let sender = get_caller_address(); + let escrow_class_hash_storage = self.escrow_class_hash.read(); + assert(escrow_class_hash_storage == escrow_class_hash, 'gift-fac/invalid-class-hash'); + let constructor_arguments = AccountConstructorArguments { + sender, gift_token, gift_amount, fee_token, fee_amount, gift_pubkey + }; + let (escrow_contract, _) = deploy_syscall( + escrow_class_hash, 0, // salt + serialize(@constructor_arguments).span(), false // deploy_from_zero + ) + .expect('gift-fac/deploy-failed'); + self + .emit( + GiftCreated { + escrow_address: escrow_contract, + sender, + escrow_class_hash, + gift_token, + gift_amount, + fee_token, + fee_amount, + gift_pubkey + } + ); + + if (gift_token == fee_token) { + let transfer_status = IERC20Dispatcher { contract_address: gift_token } + .transfer_from(get_caller_address(), escrow_contract, gift_amount + fee_amount.into()); + assert(transfer_status, 'gift-fac/transfer-failed'); + } else { + let transfer_gift_status = IERC20Dispatcher { contract_address: gift_token } + .transfer_from(get_caller_address(), escrow_contract, gift_amount); + assert(transfer_gift_status, 'gift-fac/transfer-gift-failed'); + let transfer_fee_status = IERC20Dispatcher { contract_address: fee_token } + .transfer_from(get_caller_address(), escrow_contract, fee_amount.into()); + assert(transfer_fee_status, 'gift-fac/transfer-fee-failed'); + } + } + + fn get_escrow_lib_class_hash(self: @ContractState, escrow_class_hash: ClassHash) -> ClassHash { + self.escrow_lib_class_hash.read() + } + + fn get_latest_escrow_class_hash(self: @ContractState) -> ClassHash { + self.escrow_class_hash.read() + } + + fn get_escrow_address( + self: @ContractState, + escrow_class_hash: ClassHash, + sender: ContractAddress, + gift_token: ContractAddress, + gift_amount: u256, + fee_token: ContractAddress, + fee_amount: u128, + gift_pubkey: felt252 + ) -> ContractAddress { + calculate_escrow_account_address( + GiftData { + factory: get_contract_address(), + escrow_class_hash, + sender, + gift_amount, + gift_token, + fee_token, + fee_amount, + gift_pubkey, + } + ) + } + } + + #[abi(embed_v0)] + impl TimelockUpgradeCallbackImpl of ITimelockUpgradeCallback { + fn perform_upgrade(ref self: ContractState, new_implementation: ClassHash, data: Array) { + self.timelock_upgrade.assert_and_reset_lock(); + // This should do some sanity checks and ensure that the new implementation is a valid implementation, + // then it can call replace_class_syscall and emit the UpgradeExecuted event + panic_with_felt252( + 'gift-fac/downgrade-not-allowed' + ); // since this is the first version nobody should be calling this method + } + } + + #[external(v0)] + fn pause(ref self: ContractState) { + self.ownable.assert_only_owner(); + self.pausable._pause(); + } + + #[external(v0)] + fn unpause(ref self: ContractState) { + self.ownable.assert_only_owner(); + self.pausable._unpause(); + } +} diff --git a/src/contracts/outside_execution.cairo b/src/contracts/outside_execution.cairo new file mode 100644 index 0000000..b94c50f --- /dev/null +++ b/src/contracts/outside_execution.cairo @@ -0,0 +1,33 @@ +use starknet::{ContractAddress, ClassHash, account::Call}; + + +// https://github.com/starknet-io/SNIPs/blob/main/SNIPS/snip-9.md +pub const ERC165_OUTSIDE_EXECUTION_INTERFACE_ID_VERSION_2: felt252 = + 0x1d1144bb2138366ff28d8e9ab57456b1d332ac42196230c3a602003c89872; + +/// @notice As defined in SNIP-9 https://github.com/starknet-io/SNIPs/blob/main/SNIPS/snip-9.md +/// @param caller Only the address specified here will be allowed to call `execute_from_outside` +/// As an exception, to opt-out of this check, the value 'ANY_CALLER' can be used +/// @param nonce It can be any value as long as it's unique. Prevents signature reuse +/// @param execute_after `execute_from_outside` only succeeds if executing after this time +/// @param execute_before `execute_from_outside` only succeeds if executing before this time +/// @param calls The calls that will be executed by the Account +/// Using `Call` here instead of re-declaring `OutsideCall` to avoid the conversion +#[derive(Copy, Drop, Serde)] +pub struct OutsideExecution { + pub caller: ContractAddress, + pub nonce: felt252, + pub execute_after: u64, + pub execute_before: u64, + pub calls: Span +} + +#[starknet::interface] +pub trait IOutsideExecution { + fn execute_from_outside_v2( + ref self: TContractState, outside_execution: OutsideExecution, signature: Span + ) -> Array>; + + /// Get the status of a given nonce, true if the nonce is available to use + fn is_valid_outside_execution_nonce(self: @TContractState, nonce: felt252) -> bool; +} diff --git a/src/contracts/timelock_upgrade.cairo b/src/contracts/timelock_upgrade.cairo new file mode 100644 index 0000000..0688cfb --- /dev/null +++ b/src/contracts/timelock_upgrade.cairo @@ -0,0 +1,191 @@ +use core::num::traits::Zero; +use starknet::{ClassHash}; + +#[derive(Serde, Drop, Copy, Default, PartialEq, starknet::Store)] +struct PendingUpgrade { + // Gets the classhash after the upgrade, 0 if no upgrade ongoing + implementation: ClassHash, + // Gets the timestamp when the upgrade is ready to be performed, 0 if no upgrade ongoing + ready_at: u64, + // Gets the hash of the calldata used for the upgrade, 0 if no upgrade ongoing + calldata_hash: felt252, +} + +#[starknet::interface] +pub trait ITimelockUpgrade { + /// @notice Propose a new implementation for the contract to upgrade to + /// @dev There is a 7-day window before it is possible to do the upgrade. + /// @dev After the 7-day waiting period, the upgrade can be performed within a 7-day window + /// @dev If there is an ongoing upgrade, the previous proposition will be overwritten + /// @param new_implementation The class hash of the new implementation + /// @param calldata The calldata to be used for the upgrade + fn propose_upgrade(ref self: TContractState, new_implementation: ClassHash, calldata: Array); + + /// @notice Cancel the upgrade proposition + /// @dev Will fail if there is no ongoing upgrade + fn cancel_upgrade(ref self: TContractState); + + /// @notice Perform the upgrade to the proposed implementation + /// @dev Can only be called after a 7 day waiting period and is valid only for a 7 day window + /// @param calldata The calldata to be used for the upgrade + fn upgrade(ref self: TContractState, calldata: Array); + + /// @notice Gets the proposed upgrade + fn get_pending_upgrade(self: @TContractState) -> PendingUpgrade; +} + +#[starknet::interface] +pub trait ITimelockUpgradeCallback { + /// @notice Perform the upgrade to the proposed implementation + /// @dev Currently empty as the upgrade logic will be handled in the contract we upgrade to + /// @param new_implementation The class hash of the new implementation + /// @param data The data to be used for the upgrade + fn perform_upgrade(ref self: TContractState, new_implementation: ClassHash, data: Array); +} + +#[starknet::component] +pub mod TimelockUpgradeComponent { + use core::num::traits::Zero; + use core::poseidon::poseidon_hash_span; + use openzeppelin::access::ownable::{OwnableComponent, OwnableComponent::InternalTrait}; + use starknet::{get_block_timestamp, ClassHash}; + use super::{ + ITimelockUpgrade, ITimelockUpgradeCallback, ITimelockUpgradeCallbackLibraryDispatcher, + ITimelockUpgradeCallbackDispatcherTrait, PendingUpgrade + }; + + /// Time before the upgrade can be performed + const MIN_SECURITY_PERIOD: u64 = consteval_int!(7 * 24 * 60 * 60); // 7 days + /// Time window during which the upgrade can be performed + const VALID_WINDOW_PERIOD: u64 = consteval_int!(7 * 24 * 60 * 60); // 7 days + + + #[storage] + pub struct Storage { + pending_upgrade: PendingUpgrade, + /// true only during the upgrade call + upgrade_lock: bool, + } + + #[event] + #[derive(Drop, starknet::Event)] + pub enum Event { + UpgradeProposed: UpgradeProposed, + UpgradeCancelled: UpgradeCancelled, + UpgradeExecuted: UpgradeExecuted, + } + + #[derive(Drop, starknet::Event)] + struct UpgradeProposed { + new_implementation: ClassHash, + ready_at: u64, + calldata: Array + } + + #[derive(Drop, starknet::Event)] + struct UpgradeCancelled { + cancelled_upgrade: PendingUpgrade + } + + #[derive(Drop, starknet::Event)] + struct UpgradeExecuted { + new_implementation: ClassHash, + calldata: Array + } + + #[embeddable_as(TimelockUpgradeImpl)] + impl TimelockUpgrade< + TContractState, + +HasComponent, + +OwnableComponent::HasComponent, + +ITimelockUpgradeCallback, + > of ITimelockUpgrade> { + fn propose_upgrade( + ref self: ComponentState, new_implementation: ClassHash, calldata: Array + ) { + self.assert_only_owner(); + assert(new_implementation.is_non_zero(), 'upgrade/new-implementation-null'); + + let pending_upgrade = self.pending_upgrade.read(); + if pending_upgrade != Default::default() { + self.emit(UpgradeCancelled { cancelled_upgrade: pending_upgrade }) + } + + let ready_at = get_block_timestamp() + MIN_SECURITY_PERIOD; + self + .pending_upgrade + .write( + PendingUpgrade { + implementation: new_implementation, ready_at, calldata_hash: poseidon_hash_span(calldata.span()) + } + ); + self.emit(UpgradeProposed { new_implementation, ready_at, calldata }); + } + + fn cancel_upgrade(ref self: ComponentState) { + self.assert_only_owner(); + let pending_upgrade = self.pending_upgrade.read(); + assert(pending_upgrade != Default::default(), 'upgrade/no-pending-upgrade'); + self.pending_upgrade.write(Default::default()); + self.emit(UpgradeCancelled { cancelled_upgrade: pending_upgrade }); + } + + fn upgrade(ref self: ComponentState, calldata: Array) { + self.assert_only_owner(); + let pending_upgrade: PendingUpgrade = self.pending_upgrade.read(); + assert(pending_upgrade != Default::default(), 'upgrade/no-pending-upgrade'); + let PendingUpgrade { implementation, ready_at, calldata_hash } = pending_upgrade; + + assert(calldata_hash == poseidon_hash_span(calldata.span()), 'upgrade/invalid-calldata'); + + let current_timestamp = get_block_timestamp(); + assert(current_timestamp >= ready_at, 'upgrade/too-early'); + assert(current_timestamp < ready_at + VALID_WINDOW_PERIOD, 'upgrade/upgrade-too-late'); + + self.pending_upgrade.write(Default::default()); + + self.upgrade_lock.write(true); + ITimelockUpgradeCallbackLibraryDispatcher { class_hash: implementation } + .perform_upgrade(implementation, calldata); + assert(!self.upgrade_lock.read(), 'upgrade/lock-not-reset') + } + + + fn get_pending_upgrade(self: @ComponentState) -> PendingUpgrade { + self.pending_upgrade.read() + } + } + + #[generate_trait] + pub impl TimelockUpgradeInternalImpl< + TContractState, +HasComponent + > of ITimelockUpgradeInternal { + /// @notice Should be called by the `perform_upgrade` method to make sure this method can only by called when upgrading + fn assert_and_reset_lock(ref self: ComponentState) { + assert(self.upgrade_lock.read(), 'upgrade/only-during-upgrade'); + self.upgrade_lock.write(false); + } + fn emit_upgrade_executed( + ref self: ComponentState, new_implementation: ClassHash, calldata: Array + ) { + self.emit(UpgradeExecuted { new_implementation, calldata }); + } + } + + + #[generate_trait] + impl PrivateImpl< + TContractState, impl Ownable: OwnableComponent::HasComponent, +HasComponent + > of PrivateTrait { + fn assert_only_owner(self: @ComponentState) { + get_dep_component!(self, Ownable).assert_only_owner(); + } + } +} + + +impl DefaultClassHash of Default { + fn default() -> ClassHash { + Zero::zero() + } +} diff --git a/src/contracts/utils.cairo b/src/contracts/utils.cairo new file mode 100644 index 0000000..a481cf2 --- /dev/null +++ b/src/contracts/utils.cairo @@ -0,0 +1,62 @@ +use argent_gifting::contracts::escrow_account::{AccountConstructorArguments}; +use argent_gifting::contracts::gift_data::{GiftData}; +use openzeppelin::utils::deployments::calculate_contract_address_from_deploy_syscall; +use starknet::{ContractAddress, account::Call, contract_address::contract_address_const}; + +pub const TX_V1: felt252 = 1; // INVOKE +pub const TX_V1_ESTIMATE: felt252 = consteval_int!(0x100000000000000000000000000000000 + 1); // 2**128 + TX_V1 +pub const TX_V3: felt252 = 3; +pub const TX_V3_ESTIMATE: felt252 = consteval_int!(0x100000000000000000000000000000000 + 3); // 2**128 + TX_V3 + +pub fn STRK_ADDRESS() -> ContractAddress { + contract_address_const::<0x04718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d>() +} + +pub fn ETH_ADDRESS() -> ContractAddress { + contract_address_const::<0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7>() +} + +#[derive(Serde, Drop, Copy, starknet::Store)] +pub struct StarknetSignature { + pub r: felt252, + pub s: felt252, +} + +// Tries to deserialize the given data into. +// The data must only contain the returned value and nothing else +pub fn full_deserialize, impl EDrop: Drop>(mut data: Span) -> Option { + let parsed_value: E = ESerde::deserialize(ref data)?; + if data.is_empty() { + Option::Some(parsed_value) + } else { + Option::None + } +} + +pub fn serialize>(value: @E) -> Array { + let mut output = array![]; + ESerde::serialize(value, ref output); + output +} + +/// @notice Computes the ContractAddress of an account for a given gift +/// @dev The salt used is fixed to 0 to ensure there's only one contract for a given gift. +/// @dev The deployer_address is the factory address, as the account contract is deployed by the factory +/// @param gift The gift data for which you need to calculate the account contract address +/// @return The ContractAddress of the account contract corresponding to the gift +pub fn calculate_escrow_account_address(gift: GiftData) -> ContractAddress { + let constructor_arguments = AccountConstructorArguments { + sender: gift.sender, + gift_token: gift.gift_token, + gift_amount: gift.gift_amount, + fee_token: gift.fee_token, + fee_amount: gift.fee_amount, + gift_pubkey: gift.gift_pubkey + }; + calculate_contract_address_from_deploy_syscall( + 0, // salt + gift.escrow_class_hash, // escrow_class_hash + serialize(@constructor_arguments).span(), // constructor_data + gift.factory + ) +} diff --git a/src/lib.cairo b/src/lib.cairo new file mode 100644 index 0000000..e1086e6 --- /dev/null +++ b/src/lib.cairo @@ -0,0 +1,17 @@ +pub mod contracts { + pub mod claim_hash; + pub mod escrow_account; + pub mod escrow_library; + pub mod gift_data; + pub mod gift_factory; + pub mod outside_execution; + pub mod timelock_upgrade; + pub mod utils; +} + +mod mocks { + mod broken_erc20; + mod erc20; + mod future_factory; + mod reentrant_erc20; +} diff --git a/src/mocks/broken_erc20.cairo b/src/mocks/broken_erc20.cairo new file mode 100644 index 0000000..68201d2 --- /dev/null +++ b/src/mocks/broken_erc20.cairo @@ -0,0 +1,63 @@ +#[starknet::contract] +mod BrokenERC20 { + use openzeppelin::token::erc20::interface::IERC20; + use openzeppelin::token::erc20::{ERC20Component, ERC20HooksEmptyImpl}; + use starknet::{get_caller_address, ContractAddress}; + + + component!(path: ERC20Component, storage: erc20, event: ERC20Event); + impl ERC20InternalImpl = ERC20Component::InternalImpl; + + + #[storage] + struct Storage { + #[substorage(v0)] + erc20: ERC20Component::Storage, + } + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + #[flat] + ERC20Event: ERC20Component::Event, + } + + #[abi(embed_v0)] + impl Erc20MockImpl of IERC20 { + fn transfer_from( + ref self: ContractState, sender: ContractAddress, recipient: ContractAddress, amount: u256 + ) -> bool { + false + } + + fn approve(ref self: ContractState, spender: ContractAddress, amount: u256) -> bool { + let caller = get_caller_address(); + self.erc20.ERC20_allowances.write((caller, spender), amount); + true + } + + fn balance_of(self: @ContractState, account: ContractAddress) -> u256 { + self.erc20.ERC20_balances.read(account) + } + + fn allowance(self: @ContractState, owner: ContractAddress, spender: ContractAddress) -> u256 { + self.erc20.ERC20_allowances.read((owner, spender)) + } + + fn transfer(ref self: ContractState, recipient: ContractAddress, amount: u256) -> bool { + let caller = get_caller_address(); + let caller_balance = self.erc20.ERC20_balances.read(caller); + if caller_balance < amount { + return false; + } + self.erc20.ERC20_balances.write(caller, caller_balance - amount); + let recipient_balance = self.erc20.ERC20_balances.read(recipient); + self.erc20.ERC20_balances.write(recipient, recipient_balance + amount); + true + } + + fn total_supply(self: @ContractState) -> u256 { + self.erc20.ERC20_total_supply.read() + } + } +} diff --git a/src/mocks/erc20.cairo b/src/mocks/erc20.cairo new file mode 100644 index 0000000..03cc402 --- /dev/null +++ b/src/mocks/erc20.cairo @@ -0,0 +1,55 @@ +#[starknet::contract] +mod MockERC20 { + use openzeppelin::access::ownable::OwnableComponent; + use openzeppelin::token::erc20::interface::IERC20Metadata; + use openzeppelin::token::erc20::{ERC20Component, ERC20HooksEmptyImpl}; + use starknet::{ContractAddress, ClassHash}; + + component!(path: OwnableComponent, storage: ownable, event: OwnableEvent); + component!(path: ERC20Component, storage: erc20, event: ERC20Event); + + // Ownable Mixin + #[abi(embed_v0)] + impl OwnableMixinImpl = OwnableComponent::OwnableMixinImpl; + impl OwnableInternalImpl = OwnableComponent::InternalImpl; + + // ERC20 Mixin + #[abi(embed_v0)] + impl ERC20MixinImpl = ERC20Component::ERC20MixinImpl; + impl ERC20InternalImpl = ERC20Component::InternalImpl; + + + #[storage] + struct Storage { + #[substorage(v0)] + ownable: OwnableComponent::Storage, + #[substorage(v0)] + erc20: ERC20Component::Storage, + } + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + #[flat] + OwnableEvent: OwnableComponent::Event, + #[flat] + ERC20Event: ERC20Component::Event, + } + + /// Assigns `owner` as the contract owner. + /// Sets the token `name` and `symbol`. + /// Mints `fixed_supply` tokens to `recipient`. + #[constructor] + fn constructor( + ref self: ContractState, + name: ByteArray, + symbol: ByteArray, + fixed_supply: u256, + recipient: ContractAddress, + owner: ContractAddress + ) { + self.ownable.initializer(owner); + self.erc20.initializer(name, symbol); + self.erc20._mint(recipient, fixed_supply); + } +} diff --git a/src/mocks/future_factory.cairo b/src/mocks/future_factory.cairo new file mode 100644 index 0000000..43e143c --- /dev/null +++ b/src/mocks/future_factory.cairo @@ -0,0 +1,59 @@ +use starknet::{ContractAddress, ClassHash}; + +#[starknet::contract] +mod FutureFactory { + use argent_gifting::contracts::timelock_upgrade::{ITimelockUpgradeCallback, TimelockUpgradeComponent}; + use core::panic_with_felt252; + use openzeppelin::access::ownable::OwnableComponent; + use starknet::{ + ClassHash, ContractAddress, syscalls::deploy_syscall, get_caller_address, get_contract_address, account::Call, + get_block_timestamp + }; + + // Ownable + component!(path: OwnableComponent, storage: ownable, event: OwnableEvent); + #[abi(embed_v0)] + impl OwnableImpl = OwnableComponent::OwnableImpl; + + // TimelockUpgradeable + component!(path: TimelockUpgradeComponent, storage: timelock_upgrade, event: TimelockUpgradeEvent); + #[abi(embed_v0)] + impl TimelockUpgradeImpl = TimelockUpgradeComponent::TimelockUpgradeImpl; + impl TimelockUpgradeInternalImpl = TimelockUpgradeComponent::TimelockUpgradeInternalImpl; + + #[storage] + struct Storage { + #[substorage(v0)] + ownable: OwnableComponent::Storage, + #[substorage(v0)] + timelock_upgrade: TimelockUpgradeComponent::Storage, + } + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + #[flat] + OwnableEvent: OwnableComponent::Event, + #[flat] + TimelockUpgradeEvent: TimelockUpgradeComponent::Event, + } + + #[constructor] + fn constructor(ref self: ContractState) {} + + + #[external(v0)] + fn get_num(self: @ContractState) -> u128 { + 1 + } + + #[abi(embed_v0)] + impl TimelockUpgradeCallbackImpl of ITimelockUpgradeCallback { + fn perform_upgrade(ref self: ContractState, new_implementation: ClassHash, data: Array) { + self.timelock_upgrade.assert_and_reset_lock(); + starknet::syscalls::replace_class_syscall(new_implementation).unwrap(); + self.timelock_upgrade.emit_upgrade_executed(new_implementation, data); + } + } +} + diff --git a/src/mocks/reentrant_erc20.cairo b/src/mocks/reentrant_erc20.cairo new file mode 100644 index 0000000..4c98558 --- /dev/null +++ b/src/mocks/reentrant_erc20.cairo @@ -0,0 +1,154 @@ +use argent_gifting::contracts::utils::{StarknetSignature}; +use starknet::{ClassHash, ContractAddress}; + + +#[derive(Serde, Drop, Copy, starknet::Store, Debug)] +struct TestGiftData { + factory: ContractAddress, + escrow_class_hash: ClassHash, + sender: ContractAddress, + gift_token: ContractAddress, + gift_amount: u256, + fee_token: ContractAddress, + fee_amount: u128, + gift_pubkey: felt252 +} + +#[starknet::interface] +trait IMalicious { + fn set_gift_data( + ref self: TContractState, + gift: TestGiftData, + receiver: ContractAddress, + dust_receiver: ContractAddress, + gift_signature: StarknetSignature, + ); +} + + +#[starknet::contract] +mod ReentrantERC20 { + use argent_gifting::contracts::escrow_account::{ + IEscrowAccount, IEscrowAccountDispatcher, IEscrowAccountDispatcherTrait + }; + use argent_gifting::contracts::gift_data::GiftData; + use argent_gifting::contracts::utils::{ETH_ADDRESS, StarknetSignature}; + use argent_gifting::contracts::utils::{calculate_escrow_account_address, serialize}; + use openzeppelin::token::erc20::erc20::ERC20Component::InternalTrait; + use openzeppelin::token::erc20::interface::{IERC20, IERC20Dispatcher, IERC20DispatcherTrait}; + use openzeppelin::token::erc20::{ERC20Component, ERC20HooksEmptyImpl}; + use starknet::{ + get_caller_address, ContractAddress, get_contract_address, contract_address_const, + syscalls::call_contract_syscall + }; + use super::{IMalicious, TestGiftData}; + + + component!(path: ERC20Component, storage: erc20, event: ERC20Event); + impl ERC20InternalImpl = ERC20Component::InternalImpl; + + + #[storage] + struct Storage { + factory: ContractAddress, + gift: TestGiftData, + receiver: ContractAddress, + dust_receiver: ContractAddress, + has_reentered: bool, + signature: StarknetSignature, + #[substorage(v0)] + erc20: ERC20Component::Storage, + } + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + #[flat] + ERC20Event: ERC20Component::Event, + } + + /// Assigns `owner` as the contract owner. + /// Sets the token `name` and `symbol`. + /// Mints `fixed_supply` tokens to `recipient`. + #[constructor] + fn constructor( + ref self: ContractState, + name: ByteArray, + symbol: ByteArray, + fixed_supply: u256, + recipient: ContractAddress, + factory: ContractAddress, + ) { + self.factory.write(factory); + self.erc20.initializer(name, symbol); + self.erc20._mint(recipient, fixed_supply); + } + + #[abi(embed_v0)] + impl Erc20MockImpl of IERC20 { + fn transfer_from( + ref self: ContractState, sender: ContractAddress, recipient: ContractAddress, amount: u256 + ) -> bool { + self.erc20.transfer_from(sender, recipient, amount) + } + + fn approve(ref self: ContractState, spender: ContractAddress, amount: u256) -> bool { + let caller = get_caller_address(); + self.erc20.ERC20_allowances.write((caller, spender), amount); + true + } + + fn balance_of(self: @ContractState, account: ContractAddress) -> u256 { + self.erc20.ERC20_balances.read(account) + } + + fn allowance(self: @ContractState, owner: ContractAddress, spender: ContractAddress) -> u256 { + self.erc20.ERC20_allowances.read((owner, spender)) + } + + fn transfer(ref self: ContractState, recipient: ContractAddress, amount: u256) -> bool { + if (!self.has_reentered.read()) { + self.has_reentered.write(true); + let test_gift: TestGiftData = self.gift.read(); + let gift = GiftData { + factory: test_gift.factory, + escrow_class_hash: test_gift.escrow_class_hash, + sender: test_gift.sender, + gift_token: test_gift.gift_token, + gift_amount: test_gift.gift_amount, + fee_token: test_gift.fee_token, + fee_amount: test_gift.fee_amount, + gift_pubkey: test_gift.gift_pubkey, + }; + let escrow_account_address = calculate_escrow_account_address(gift); + let calldata = serialize( + @(gift, self.receiver.read(), self.dust_receiver.read(), self.signature.read()) + ); + IEscrowAccountDispatcher { contract_address: escrow_account_address } + .execute_action(selector!("claim_external"), calldata); + } + + self.erc20.transfer(recipient, amount) + } + + fn total_supply(self: @ContractState) -> u256 { + self.erc20.ERC20_total_supply.read() + } + } + + #[abi(embed_v0)] + impl MaliciousImpl of IMalicious { + fn set_gift_data( + ref self: ContractState, + gift: TestGiftData, + receiver: ContractAddress, + dust_receiver: ContractAddress, + gift_signature: StarknetSignature, + ) { + self.signature.write(gift_signature); + self.gift.write(gift); + self.receiver.write(receiver); + self.dust_receiver.write(dust_receiver); + } + } +} diff --git a/tests-integration/account.test.ts b/tests-integration/account.test.ts new file mode 100644 index 0000000..10e5c89 --- /dev/null +++ b/tests-integration/account.test.ts @@ -0,0 +1,148 @@ +import { CallData } from "starknet"; +import { + LongSigner, + WrongSigner, + buildGiftCallData, + calculateEscrowAddress, + claimInternal, + defaultDepositTestSetup, + deployer, + executeActionOnAccount, + expectRevertWithErrorMessage, + getEscrowAccount, + manager, + randomReceiver, + setupGiftProtocol, +} from "../lib"; +describe("Escrow Account", function () { + it(`Test only protocol can call validate`, async function () { + const { factory } = await setupGiftProtocol(); + const { gift } = await defaultDepositTestSetup({ factory }); + const escrowAddress = calculateEscrowAddress(gift); + + await expectRevertWithErrorMessage("escrow/only-protocol", () => + deployer.execute([{ contractAddress: escrowAddress, calldata: [0x0], entrypoint: "__validate__" }]), + ); + }); + + it(`Test only protocol can call execute`, async function () { + const { factory } = await setupGiftProtocol(); + const { gift } = await defaultDepositTestSetup({ factory }); + const escrowAddress = calculateEscrowAddress(gift); + + await expectRevertWithErrorMessage("escrow/only-protocol", () => + deployer.execute([{ contractAddress: escrowAddress, calldata: [0x0], entrypoint: "__execute__" }]), + ); + }); + + it(`Test escrow can only do whitelisted lib calls`, async function () { + const { factory } = await setupGiftProtocol(); + const { gift } = await defaultDepositTestSetup({ factory }); + const minimalCallData = CallData.compile([buildGiftCallData(gift)]); + + await expectRevertWithErrorMessage("escr-lib/invalid-selector", () => + deployer.execute(executeActionOnAccount("claim_internal", calculateEscrowAddress(gift), minimalCallData)), + ); + }); + + it(`Test escrow contract cant call another contract`, async function () { + const { factory } = await setupGiftProtocol(); + const { gift, giftPrivateKey } = await defaultDepositTestSetup({ factory }); + const receiver = randomReceiver(); + + await expectRevertWithErrorMessage("escrow/invalid-call-to", () => + claimInternal({ + gift, + receiver, + giftPrivateKey: giftPrivateKey, + details: { skipValidate: false }, + overrides: { callToAddress: "0x2" }, + }), + ); + }); + + it(`Test escrow contract can only call 'escrow_internal'`, async function () { + const { factory } = await setupGiftProtocol(); + const { gift, giftPrivateKey } = await defaultDepositTestSetup({ factory }); + + const escrowAccount = getEscrowAccount(gift, giftPrivateKey); + + await expectRevertWithErrorMessage("escrow/invalid-call-selector", () => + escrowAccount.execute( + [{ contractAddress: escrowAccount.address, calldata: [], entrypoint: "execute_action" }], + undefined, + { skipValidate: false }, + ), + ); + }); + + it(`Test escrow contract cant perform a multicall`, async function () { + const { factory } = await setupGiftProtocol(); + const { gift, giftPrivateKey } = await defaultDepositTestSetup({ factory }); + const escrowAccount = getEscrowAccount(gift, giftPrivateKey); + await expectRevertWithErrorMessage("escrow/invalid-call-len", () => + escrowAccount.execute( + [ + { contractAddress: escrowAccount.address, calldata: [], entrypoint: "execute_action" }, + { contractAddress: escrowAccount.address, calldata: [], entrypoint: "execute_action" }, + ], + undefined, + { skipValidate: false }, + ), + ); + }); + + it(`Test cannot call 'claim_internal' twice`, async function () { + const { factory } = await setupGiftProtocol(); + const { gift, giftPrivateKey } = await defaultDepositTestSetup({ factory }); + const receiver = randomReceiver(); + + // double claim + await claimInternal({ gift, receiver, giftPrivateKey: giftPrivateKey }); + await expectRevertWithErrorMessage("escrow/invalid-gift-nonce", () => + claimInternal({ gift, receiver, giftPrivateKey: giftPrivateKey, details: { skipValidate: false } }), + ); + }); + + it(`Long signature shouldn't be accepted`, async function () { + const { factory } = await setupGiftProtocol(); + const { gift, giftPrivateKey } = await defaultDepositTestSetup({ factory }); + const receiver = randomReceiver(); + + const escrowAccount = getEscrowAccount(gift, giftPrivateKey); + escrowAccount.signer = new LongSigner(); + await expectRevertWithErrorMessage("escrow/invalid-signature-len", () => + escrowAccount.execute([ + { + contractAddress: escrowAccount.address, + calldata: [buildGiftCallData(gift), receiver], + entrypoint: "claim_internal", + }, + ]), + ); + }); + + it(`Wrong signature shouldn't be accepted`, async function () { + const { factory } = await setupGiftProtocol(); + const { gift, giftPrivateKey } = await defaultDepositTestSetup({ factory }); + const receiver = randomReceiver(); + + const escrowAccount = getEscrowAccount(gift, giftPrivateKey); + escrowAccount.signer = new WrongSigner(); + await expectRevertWithErrorMessage("escrow/invalid-signature", () => + escrowAccount.execute([ + { + contractAddress: escrowAccount.address, + calldata: [buildGiftCallData(gift), receiver], + entrypoint: "claim_internal", + }, + ]), + ); + }); + + it(`Shouldn't be possible to instantiate the library account`, async function () { + const classHash = await manager.declareLocalContract("EscrowLibrary"); + + await expectRevertWithErrorMessage("escr-lib/instance-not-recommend", () => deployer.deployContract({ classHash })); + }); +}); diff --git a/tests-integration/cancel.test.ts b/tests-integration/cancel.test.ts new file mode 100644 index 0000000..e87134a --- /dev/null +++ b/tests-integration/cancel.test.ts @@ -0,0 +1,98 @@ +import { + calculateEscrowAddress, + cancelGift, + claimInternal, + defaultDepositTestSetup, + deployMockERC20, + deployer, + devnetAccount, + expectRevertWithErrorMessage, + manager, + randomReceiver, + setupGiftProtocol, +} from "../lib"; + +describe("Cancel Gift", function () { + it(`fee_token == gift_token`, async function () { + const { factory } = await setupGiftProtocol(); + const { gift, giftPrivateKey } = await defaultDepositTestSetup({ factory }); + const receiver = randomReceiver(); + const escrowAddress = calculateEscrowAddress(gift); + + const balanceSenderBefore = await manager.tokens.tokenBalance(deployer.address, gift.gift_token); + + const { transaction_hash } = await cancelGift({ gift }); + + const txFee = BigInt((await manager.getTransactionReceipt(transaction_hash)).actual_fee.amount); + // Check balance of the sender is correct + await manager.tokens + .tokenBalance(deployer.address, gift.gift_token) + .should.eventually.equal(balanceSenderBefore + gift.gift_amount + gift.fee_amount - txFee); + // Check balance gift address address == 0 + await manager.tokens.tokenBalance(escrowAddress, gift.fee_token).should.eventually.equal(0n); + + await expectRevertWithErrorMessage("escr-lib/claimed-or-cancel", () => + claimInternal({ gift, receiver, giftPrivateKey }), + ); + }); + + it(`fee_token != gift_token`, async function () { + const mockERC20 = await deployMockERC20(); + const { factory } = await setupGiftProtocol(); + const { gift, giftPrivateKey } = await defaultDepositTestSetup({ + factory, + overrides: { giftTokenAddress: mockERC20.address }, + }); + const receiver = randomReceiver(); + const escrowAddress = calculateEscrowAddress(gift); + + const balanceSenderBeforeGiftToken = await manager.tokens.tokenBalance(deployer.address, gift.gift_token); + const balanceSenderBeforeFeeToken = await manager.tokens.tokenBalance(deployer.address, gift.fee_token); + const { transaction_hash } = await cancelGift({ gift }); + + const txFee = BigInt((await manager.getTransactionReceipt(transaction_hash)).actual_fee.amount); + // Check balance of the sender is correct + await manager.tokens + .tokenBalance(deployer.address, gift.gift_token) + .should.eventually.equal(balanceSenderBeforeGiftToken + gift.gift_amount); + await manager.tokens + .tokenBalance(deployer.address, gift.fee_token) + .should.eventually.equal(balanceSenderBeforeFeeToken + gift.fee_amount - txFee); + // Check balance gift address address == 0 + await manager.tokens.tokenBalance(escrowAddress, gift.gift_token).should.eventually.equal(0n); + await manager.tokens.tokenBalance(escrowAddress, gift.fee_token).should.eventually.equal(0n); + + await expectRevertWithErrorMessage("escr-lib/claimed-or-cancel", () => + claimInternal({ gift, receiver, giftPrivateKey }), + ); + }); + + it(`wrong sender`, async function () { + const { factory } = await setupGiftProtocol(); + const { gift } = await defaultDepositTestSetup({ factory }); + await expectRevertWithErrorMessage("escr-lib/wrong-sender", () => + cancelGift({ gift, senderAccount: devnetAccount() }), + ); + }); + + it(`already claimed (gift_token == fee_token)`, async function () { + const { factory } = await setupGiftProtocol(); + const { gift, giftPrivateKey } = await defaultDepositTestSetup({ factory }); + const receiver = randomReceiver(); + await claimInternal({ gift, receiver, giftPrivateKey }); + await expectRevertWithErrorMessage("escr-lib/claimed-or-cancel", () => cancelGift({ gift })); + }); + + it(`already claimed (gift_token != fee_token)`, async function () { + const mockERC20 = await deployMockERC20(); + const { factory } = await setupGiftProtocol(); + const { gift, giftPrivateKey } = await defaultDepositTestSetup({ + factory, + overrides: { giftTokenAddress: mockERC20.address }, + }); + const receiver = randomReceiver(); + + await claimInternal({ gift, receiver, giftPrivateKey }); + await expectRevertWithErrorMessage("escr-lib/claimed-or-cancel", () => cancelGift({ gift })); + }); +}); diff --git a/tests-integration/claim_external.test.ts b/tests-integration/claim_external.test.ts new file mode 100644 index 0000000..5959b08 --- /dev/null +++ b/tests-integration/claim_external.test.ts @@ -0,0 +1,169 @@ +import { expect } from "chai"; +import { byteArray, uint256 } from "starknet"; +import { + calculateEscrowAddress, + cancelGift, + claimExternal, + defaultDepositTestSetup, + deployMockERC20, + deployer, + expectRevertWithErrorMessage, + manager, + randomReceiver, + setupGiftProtocol, + signExternalClaim, +} from "../lib"; + +describe("Claim External", function () { + for (const useTxV3 of [false, true]) { + it(`gift_token == fee_token flow using txV3: ${useTxV3} (no dust receiver)`, async function () { + const { factory } = await setupGiftProtocol(); + const { gift, giftPrivateKey } = await defaultDepositTestSetup({ factory }); + const receiver = randomReceiver(); + const escrowAddress = calculateEscrowAddress(gift); + + await claimExternal({ gift, receiver, useTxV3, giftPrivateKey }); + + const finalBalance = await manager.tokens.tokenBalance(escrowAddress, gift.gift_token); + expect(finalBalance).to.equal(gift.fee_amount); + await manager.tokens.tokenBalance(receiver, gift.gift_token).should.eventually.equal(gift.gift_amount); + await manager.tokens.tokenBalance(escrowAddress, gift.fee_token).should.eventually.equal(gift.fee_amount); + }); + + it(`gift_token == fee_token flow using txV3: ${useTxV3} (w/ dust receiver)`, async function () { + const { factory } = await setupGiftProtocol(); + const { gift, giftPrivateKey } = await defaultDepositTestSetup({ factory }); + const receiver = randomReceiver(); + const dustReceiver = randomReceiver(); + const escrowAddress = calculateEscrowAddress(gift); + + const balanceBefore = await manager.tokens.tokenBalance(escrowAddress, gift.gift_token); + await claimExternal({ gift, receiver, giftPrivateKey, useTxV3, dustReceiver }); + + await manager.tokens.tokenBalance(receiver, gift.gift_token).should.eventually.equal(gift.gift_amount); + await manager.tokens + .tokenBalance(dustReceiver, gift.gift_token) + .should.eventually.equal(balanceBefore - gift.gift_amount); + await manager.tokens.tokenBalance(escrowAddress, gift.gift_token).should.eventually.equal(0n); + await manager.tokens.tokenBalance(escrowAddress, gift.fee_token).should.eventually.equal(0n); + }); + } + + it(`gift_token != fee_token (w/ dust receiver)`, async function () { + const { factory } = await setupGiftProtocol(); + const giftToken = await deployMockERC20(); + const { gift, giftPrivateKey } = await defaultDepositTestSetup({ + factory, + overrides: { giftTokenAddress: giftToken.address }, + }); + const receiver = randomReceiver(); + const dustReceiver = randomReceiver(); + const escrowAddress = calculateEscrowAddress(gift); + + await claimExternal({ gift, receiver, giftPrivateKey, dustReceiver }); + + await manager.tokens.tokenBalance(receiver, gift.gift_token).should.eventually.equal(gift.gift_amount); + await manager.tokens.tokenBalance(dustReceiver, gift.fee_token).should.eventually.equal(gift.fee_amount); + await manager.tokens.tokenBalance(escrowAddress, gift.gift_token).should.eventually.equal(0n); + await manager.tokens.tokenBalance(escrowAddress, gift.fee_token).should.eventually.equal(0n); + }); + + it(`gift_token != fee_token (no dust receiver)`, async function () { + const { factory } = await setupGiftProtocol(); + const giftToken = await deployMockERC20(); + const { gift, giftPrivateKey } = await defaultDepositTestSetup({ + factory, + overrides: { giftTokenAddress: giftToken.address }, + }); + const receiver = randomReceiver(); + const escrowAddress = calculateEscrowAddress(gift); + + await claimExternal({ gift, receiver, giftPrivateKey }); + + await manager.tokens.tokenBalance(receiver, gift.gift_token).should.eventually.equal(gift.gift_amount); + await manager.tokens.tokenBalance(escrowAddress, gift.gift_token).should.eventually.equal(0n); + await manager.tokens.tokenBalance(escrowAddress, gift.fee_token).should.eventually.equal(gift.fee_amount); + }); + + it(`Zero Receiver`, async function () { + const { factory } = await setupGiftProtocol(); + const { gift, giftPrivateKey } = await defaultDepositTestSetup({ factory }); + const receiver = "0x0"; + + await expectRevertWithErrorMessage("escr-lib/zero-receiver", () => + claimExternal({ gift, receiver, giftPrivateKey }), + ); + }); + + it(`Cannot call claim external twice`, async function () { + const { factory } = await setupGiftProtocol(); + const { gift, giftPrivateKey } = await defaultDepositTestSetup({ factory }); + const receiver = randomReceiver(); + + await claimExternal({ gift, receiver, giftPrivateKey }); + await expectRevertWithErrorMessage("escr-lib/claimed-or-cancel", () => + claimExternal({ gift, receiver, giftPrivateKey }), + ); + }); + + it(`Invalid Signature`, async function () { + const { factory } = await setupGiftProtocol(); + const { gift } = await defaultDepositTestSetup({ factory }); + const receiver = randomReceiver(); + await expectRevertWithErrorMessage("escr-lib/invalid-ext-signature", () => + claimExternal({ gift: gift, receiver, giftPrivateKey: "0x1234" }), + ); + }); + + it(`Claim gift cancelled`, async function () { + const { factory } = await setupGiftProtocol(); + const { gift, giftPrivateKey } = await defaultDepositTestSetup({ factory }); + const receiver = randomReceiver(); + const escrowAddress = calculateEscrowAddress(gift); + + const balanceSenderBefore = await manager.tokens.tokenBalance(deployer.address, gift.gift_token); + const { transaction_hash } = await cancelGift({ gift }); + const txFee = BigInt((await manager.getTransactionReceipt(transaction_hash)).actual_fee.amount); + // Check balance of the sender is correct + await manager.tokens + .tokenBalance(deployer.address, gift.gift_token) + .should.eventually.equal(balanceSenderBefore + gift.gift_amount + gift.fee_amount - txFee); + // Check balance gift address address == 0 + await manager.tokens.tokenBalance(escrowAddress, gift.gift_token).should.eventually.equal(0n); + + await expectRevertWithErrorMessage("escr-lib/claimed-or-cancel", () => + claimExternal({ gift, receiver, giftPrivateKey }), + ); + }); + + // Commented out to pass CI temporarily + it(`Not possible to claim more via reentrancy`, async function () { + const { factory } = await setupGiftProtocol(); + const receiver = randomReceiver(); + + const reentrant = await manager.deployContract("ReentrantERC20", { + unique: true, + constructorCalldata: [ + byteArray.byteArrayFromString("ReentrantUSDC"), + byteArray.byteArrayFromString("RUSDC"), + uint256.bnToUint256(100e18), + deployer.address, + factory.address, + ], + }); + const { gift, giftPrivateKey } = await defaultDepositTestSetup({ + factory, + overrides: { giftTokenAddress: reentrant.address }, + }); + + const claimSig = await signExternalClaim({ gift, receiver, giftPrivateKey }); + + reentrant.connect(deployer); + const { transaction_hash } = await reentrant.set_gift_data(gift, receiver, "0x0", claimSig); + await manager.waitForTransaction(transaction_hash); + + await expectRevertWithErrorMessage("ERC20: insufficient balance", () => + claimExternal({ gift, receiver, giftPrivateKey }), + ); + }); +}); diff --git a/tests-integration/claim_internal.test.ts b/tests-integration/claim_internal.test.ts new file mode 100644 index 0000000..e1c3112 --- /dev/null +++ b/tests-integration/claim_internal.test.ts @@ -0,0 +1,127 @@ +import { expect } from "chai"; +import { num } from "starknet"; +import { + ETH_GIFT_MAX_FEE, + STRK_GIFT_MAX_FEE, + buildGiftCallData, + calculateEscrowAddress, + claimInternal, + defaultDepositTestSetup, + expectRevertWithErrorMessage, + getEscrowAccount, + manager, + randomReceiver, + setupGiftProtocol, +} from "../lib"; + +describe("Claim Internal", function () { + for (const useTxV3 of [false, true]) { + it(`gift token == fee token using txV3: ${useTxV3}`, async function () { + const { factory } = await setupGiftProtocol(); + const { gift, giftPrivateKey } = await defaultDepositTestSetup({ factory, useTxV3 }); + const receiver = randomReceiver(); + const escrowAddress = calculateEscrowAddress(gift); + + await claimInternal({ gift, receiver, giftPrivateKey }); + + const finalBalance = await manager.tokens.tokenBalance(escrowAddress, gift.gift_token); + expect(finalBalance < gift.fee_amount).to.be.true; + await manager.tokens.tokenBalance(receiver, gift.gift_token).should.eventually.equal(gift.gift_amount); + }); + + it(`Invalid gift data txV3: ${useTxV3}`, async function () { + const { factory } = await setupGiftProtocol(); + const { gift, giftPrivateKey } = await defaultDepositTestSetup({ factory, useTxV3 }); + const receiver = randomReceiver(); + const escrowAddress = calculateEscrowAddress(gift); + + const escrowAccountAddress = getEscrowAccount(gift, giftPrivateKey, escrowAddress).address; + gift.fee_amount = 42n; + await expectRevertWithErrorMessage("escrow/invalid-escrow-address", () => + claimInternal({ gift, receiver, giftPrivateKey, overrides: { escrowAccountAddress } }), + ); + }); + + it(`Invalid calldata using txV3: ${useTxV3}`, async function () { + const { factory } = await setupGiftProtocol(); + const { gift, giftPrivateKey } = await defaultDepositTestSetup({ factory, useTxV3 }); + const receiver = randomReceiver(); + + const escrowAccount = getEscrowAccount(gift, giftPrivateKey); + await expectRevertWithErrorMessage("escrow/invalid-calldata", () => + escrowAccount.execute([ + { + contractAddress: escrowAccount.address, + calldata: [buildGiftCallData(gift), receiver, 1], + entrypoint: "claim_internal", + }, + ]), + ); + }); + + it(`Can't claim if no fee amount deposited (fee token == gift token) using txV3: ${useTxV3}`, async function () { + const { factory } = await setupGiftProtocol(); + const receiver = randomReceiver(); + + const { gift, giftPrivateKey } = await defaultDepositTestSetup({ + factory, + useTxV3, + overrides: { feeAmount: 0n }, + }); + + const errorMsg = useTxV3 ? "escrow/max-fee-too-high-v3" : "escrow/max-fee-too-high-v1"; + await expectRevertWithErrorMessage(errorMsg, () => claimInternal({ gift, receiver, giftPrivateKey })); + }); + + it(`Test max fee too high using txV3: ${useTxV3}`, async function () { + const { factory } = await setupGiftProtocol(); + const { gift, giftPrivateKey } = await defaultDepositTestSetup({ factory, useTxV3 }); + const receiver = randomReceiver(); + if (useTxV3) { + // If you run this test on testnet, it'll fail + // You can then take the value from the error message and replace 1n (given some extra iff the price rises) + const gasPrice = manager.isDevnet ? 36000000000n : 1n; + const newResourceBounds = { + l2_gas: { + max_amount: "0x0", + max_price_per_unit: "0x0", + }, + l1_gas: { + max_amount: num.toHexString(STRK_GIFT_MAX_FEE / gasPrice + 1n), + max_price_per_unit: num.toHexString(gasPrice), + }, + }; + await expectRevertWithErrorMessage("escrow/max-fee-too-high-v3", () => + claimInternal({ + gift, + receiver, + giftPrivateKey, + details: { resourceBounds: newResourceBounds, tip: 1 }, + }), + ); + } else { + await expectRevertWithErrorMessage("escrow/max-fee-too-high-v1", () => + claimInternal({ + gift, + receiver, + giftPrivateKey, + details: { + maxFee: ETH_GIFT_MAX_FEE + 1n, + }, + }), + ); + } + }); + } + + it(`Cant call gift internal twice`, async function () { + const { factory } = await setupGiftProtocol(); + const { gift, giftPrivateKey } = await defaultDepositTestSetup({ factory }); + const receiver = randomReceiver(); + + await claimInternal({ gift, receiver, giftPrivateKey }); + await expectRevertWithErrorMessage("escr-lib/claimed-or-cancel", () => + claimInternal({ gift, receiver, giftPrivateKey }), + ); + }); +}); diff --git a/tests-integration/deposit.test.ts b/tests-integration/deposit.test.ts new file mode 100644 index 0000000..6559f14 --- /dev/null +++ b/tests-integration/deposit.test.ts @@ -0,0 +1,146 @@ +import { expect } from "chai"; +import { + calculateEscrowAddress, + defaultDepositTestSetup, + deployMockERC20, + expectRevertWithErrorMessage, + manager, + randomReceiver, + setupGiftProtocol, +} from "../lib"; + +describe("Deposit", function () { + it(`Double deposit`, async function () { + const { factory } = await setupGiftProtocol(); + const giftPrivateKey = BigInt(randomReceiver()); + await defaultDepositTestSetup({ factory, overrides: { giftPrivateKey } }); + try { + await defaultDepositTestSetup({ factory, overrides: { giftPrivateKey } }); + } catch (e: any) { + expect(e.toString()).to.include("is unavailable for deployment"); + } + }); + + for (const useTxV3 of [false, true]) { + it(`Deposit works using txV3: ${useTxV3} (gift token == gift token)`, async function () { + const { factory } = await setupGiftProtocol(); + + const { gift } = await defaultDepositTestSetup({ factory, useTxV3 }); + + const escrowAddress = calculateEscrowAddress(gift); + + const giftTokenBalance = await manager.tokens.tokenBalance(escrowAddress, gift.gift_token); + expect(giftTokenBalance).to.equal(gift.gift_amount + gift.fee_amount); + }); + + it(`Deposit works using txV3: ${useTxV3} with 0 fee amount set (gift token == gift token)`, async function () { + const { factory } = await setupGiftProtocol(); + + const { gift } = await defaultDepositTestSetup({ + factory, + useTxV3, + overrides: { giftAmount: 100n, feeAmount: 0n }, + }); + + const escrowAddress = calculateEscrowAddress(gift); + + const giftTokenBalance = await manager.tokens.tokenBalance(escrowAddress, gift.gift_token); + expect(giftTokenBalance).to.equal(gift.gift_amount + gift.fee_amount); + }); + + it(`Deposit works using txV3: ${useTxV3} with 0 fee amount set (gift token != gift token)`, async function () { + const { factory } = await setupGiftProtocol(); + const giftToken = await deployMockERC20(); + + const { gift } = await defaultDepositTestSetup({ + factory, + useTxV3, + overrides: { giftAmount: 100n, feeAmount: 0n, giftTokenAddress: giftToken.address }, + }); + + const escrowAddress = calculateEscrowAddress(gift); + + const giftTokenBalance = await manager.tokens.tokenBalance(escrowAddress, gift.gift_token); + expect(giftTokenBalance).to.equal(gift.gift_amount); + + const feeTokenBalance = await manager.tokens.tokenBalance(escrowAddress, gift.fee_token); + expect(feeTokenBalance).to.equal(gift.fee_amount); + }); + + it(`Deposit works using: ${useTxV3} (gift token != gift token)`, async function () { + const { factory } = await setupGiftProtocol(); + const giftToken = await deployMockERC20(); + + const { gift } = await defaultDepositTestSetup({ + factory, + useTxV3, + overrides: { giftTokenAddress: giftToken.address }, + }); + + const escrowAddress = calculateEscrowAddress(gift); + + const giftTokenBalance = await manager.tokens.tokenBalance(escrowAddress, gift.gift_token); + expect(giftTokenBalance).to.equal(gift.gift_amount); + + const feeTokenBalance = await manager.tokens.tokenBalance(escrowAddress, gift.fee_token); + expect(feeTokenBalance).to.equal(gift.fee_amount); + }); + + it(`Max fee too high gift_amount > fee_amount (gift token == fee token)`, async function () { + const { factory } = await setupGiftProtocol(); + + await expectRevertWithErrorMessage("gift-fac/fee-too-high", async () => { + const { txReceipt } = await defaultDepositTestSetup({ + factory, + useTxV3, + overrides: { giftAmount: 100n, feeAmount: 101n }, + }); + return txReceipt; + }); + }); + + it(`Fee not ETH nor STRK`, async function () { + const { factory } = await setupGiftProtocol(); + const mockERC20 = await deployMockERC20(); + + await expectRevertWithErrorMessage("gift-fac/invalid-fee-token", async () => { + const { txReceipt } = await defaultDepositTestSetup({ + factory, + useTxV3, + overrides: { feeTokenAddress: mockERC20.address }, + }); + return txReceipt; + }); + }); + } + + it("Deposit fails class hash passed != class hash in factory storage", async function () { + const { factory } = await setupGiftProtocol(); + const invalidEscrowAccountClassHash = "0x1234"; + + await expectRevertWithErrorMessage("gift-fac/invalid-class-hash", async () => { + const { txReceipt } = await defaultDepositTestSetup({ + factory, + overrides: { + escrowAccountClassHash: invalidEscrowAccountClassHash, + }, + }); + return txReceipt; + }); + }); + + it("Deposit fails if erc reverts", async function () { + const brokenERC20 = await manager.deployContract("BrokenERC20", { + unique: true, + }); + const { factory } = await setupGiftProtocol(); + + await expectRevertWithErrorMessage("gift-fac/transfer-gift-failed", async () => { + const { txReceipt } = await defaultDepositTestSetup({ + factory, + overrides: { giftTokenAddress: brokenERC20.address }, + }); + return txReceipt; + }); + }); +}); diff --git a/tests-integration/events.test.ts b/tests-integration/events.test.ts new file mode 100644 index 0000000..980ce49 --- /dev/null +++ b/tests-integration/events.test.ts @@ -0,0 +1,88 @@ +import { CallData, uint256 } from "starknet"; +import { + calculateEscrowAddress, + cancelGift, + claimExternal, + claimInternal, + defaultDepositTestSetup, + deployer, + expectEvent, + randomReceiver, + setupGiftProtocol, +} from "../lib"; + +describe("All events are emitted", function () { + it("Deposit", async function () { + const { factory, escrowAccountClassHash } = await setupGiftProtocol(); + const { gift, txReceipt } = await defaultDepositTestSetup({ factory }); + + const escrowAddress = calculateEscrowAddress(gift); + + await expectEvent(txReceipt.transaction_hash, { + from_address: factory.address, + eventName: "GiftCreated", + keys: [escrowAddress, deployer.address], + data: CallData.compile([ + escrowAccountClassHash, + gift.gift_token, + uint256.bnToUint256(gift.gift_amount), + gift.fee_token, + gift.fee_amount, + gift.gift_pubkey, + ]), + }); + }); + + it("Cancelled", async function () { + const { factory } = await setupGiftProtocol(); + const { gift } = await defaultDepositTestSetup({ factory }); + + const { transaction_hash } = await cancelGift({ gift }); + + const escrowAddress = calculateEscrowAddress(gift); + + await expectEvent(transaction_hash, { + from_address: escrowAddress, + eventName: "GiftCancelled", + }); + }); + + it("Claim Internal", async function () { + const { factory } = await setupGiftProtocol(); + const { gift, giftPrivateKey } = await defaultDepositTestSetup({ factory }); + const receiver = randomReceiver(); + const dustReceiver = "0x0"; + + const { transaction_hash } = await claimInternal({ gift, receiver, giftPrivateKey: giftPrivateKey }); + + const escrowAddress = calculateEscrowAddress(gift); + + await expectEvent(transaction_hash, { + from_address: escrowAddress, + eventName: "GiftClaimed", + data: [receiver, dustReceiver], + }); + }); + + it("Claim External", async function () { + const { factory } = await setupGiftProtocol(); + const { gift, giftPrivateKey } = await defaultDepositTestSetup({ factory }); + const receiver = randomReceiver(); + const dustReceiver = randomReceiver(); + + const { transaction_hash } = await claimExternal({ + gift, + receiver, + giftPrivateKey: giftPrivateKey, + dustReceiver, + }); + + const escrowAddress = calculateEscrowAddress(gift); + + await expectEvent(transaction_hash, { + from_address: escrowAddress, + eventName: "GiftClaimed", + data: [receiver, dustReceiver], + }); + }); +}); diff --git a/tests-integration/factory.test.ts b/tests-integration/factory.test.ts new file mode 100644 index 0000000..9b8c66e --- /dev/null +++ b/tests-integration/factory.test.ts @@ -0,0 +1,148 @@ +import { expect } from "chai"; +import { num, RPC } from "starknet"; +import { + calculateEscrowAddress, + claimDust, + claimInternal, + defaultDepositTestSetup, + deployer, + deposit, + devnetAccount, + ETH_GIFT_AMOUNT, + ETH_GIFT_MAX_FEE, + expectRevertWithErrorMessage, + getGiftAmount, + getMaxFee, + LegacyStarknetKeyPair, + manager, + randomReceiver, + setupGiftProtocol, +} from "../lib"; + +describe("Test Core Factory Functions", function () { + it(`Calculate escrow address`, async function () { + const { factory } = await setupGiftProtocol(); + const { gift } = await defaultDepositTestSetup({ factory }); + + const escrowAddress = await factory.get_escrow_address( + gift.escrow_class_hash, + deployer.address, + gift.gift_token, + gift.gift_amount, + gift.fee_token, + gift.fee_amount, + gift.gift_pubkey, + ); + + const correctAddress = calculateEscrowAddress(gift); + expect(escrowAddress).to.be.equal(num.toBigInt(correctAddress)); + }); + + for (const useTxV3 of [false, true]) { + it(`claim_dust: ${useTxV3}`, async function () { + const { factory } = await setupGiftProtocol(); + const { gift, giftPrivateKey } = await defaultDepositTestSetup({ factory, useTxV3 }); + const receiver = randomReceiver(); + const dustReceiver = randomReceiver(); + + await claimInternal({ gift, receiver, giftPrivateKey: giftPrivateKey }); + const escrowAddress = calculateEscrowAddress(gift); + + // Final check + const dustBalance = await manager.tokens.tokenBalance(escrowAddress, gift.gift_token); + const maxFee = getMaxFee(useTxV3); + const giftAmount = getGiftAmount(useTxV3); + expect(dustBalance < maxFee).to.be.true; + await manager.tokens.tokenBalance(receiver, gift.gift_token).should.eventually.equal(giftAmount); + + // Test dust + await manager.tokens.tokenBalance(dustReceiver, gift.gift_token).should.eventually.equal(0n); + + await claimDust({ gift, receiver: dustReceiver }); + + await manager.tokens.tokenBalance(escrowAddress, gift.gift_token).should.eventually.equal(0n); + await manager.tokens.tokenBalance(dustReceiver, gift.gift_token).should.eventually.equal(dustBalance); + }); + + it(`Shouldn't be possible to claim_dust for an unclaimed gift: ${useTxV3}`, async function () { + const { factory } = await setupGiftProtocol(); + const { gift } = await defaultDepositTestSetup({ factory, useTxV3 }); + const dustReceiver = randomReceiver(); + + await expectRevertWithErrorMessage("escr-lib/not-yet-claimed", () => claimDust({ gift, receiver: dustReceiver })); + }); + } + + it(`Pausable`, async function () { + // Deploy factory + const { factory } = await setupGiftProtocol(); + const receiver = randomReceiver(); + const giftSigner = new LegacyStarknetKeyPair(); + + const token = await manager.tokens.feeTokenContract(false); + + // pause / unpause + factory.connect(deployer); + const { transaction_hash: txHash1 } = await factory.pause(); + await manager.waitForTransaction(txHash1); + + await expectRevertWithErrorMessage("Pausable: paused", async () => { + const { response } = await deposit({ + sender: deployer, + giftAmount: ETH_GIFT_AMOUNT, + feeAmount: ETH_GIFT_MAX_FEE, + factoryAddress: factory.address, + feeTokenAddress: token.address, + giftTokenAddress: token.address, + giftSignerPubKey: giftSigner.publicKey, + }); + return response; + }); + + const { transaction_hash: txHash2 } = await factory.unpause(); + await manager.waitForTransaction(txHash2); + const { gift } = await defaultDepositTestSetup({ + factory, + overrides: { giftPrivateKey: BigInt(giftSigner.privateKey) }, + }); + const { execution_status } = await claimInternal({ + gift, + receiver, + giftPrivateKey: giftSigner.privateKey, + }); + expect(execution_status).to.be.equal(RPC.ETransactionExecutionStatus.SUCCEEDED); + }); + + describe("Ownable", function () { + it("Pause", async function () { + const { factory } = await setupGiftProtocol(); + + factory.connect(devnetAccount()); + await expectRevertWithErrorMessage("Caller is not the owner", () => factory.pause()); + }); + + it("Unpause", async function () { + const { factory } = await setupGiftProtocol(); + + factory.connect(deployer); + await factory.pause(); + + factory.connect(devnetAccount()); + await expectRevertWithErrorMessage("Caller is not the owner", () => factory.unpause()); + + // needed for next tests + factory.connect(deployer); + await factory.unpause(); + }); + + it("Ownable: Get Dust", async function () { + const { factory } = await setupGiftProtocol(); + const { gift } = await defaultDepositTestSetup({ factory }); + const dustReceiver = randomReceiver(); + + await expectRevertWithErrorMessage("escr-lib/only-factory-owner", () => + claimDust({ gift, receiver: dustReceiver, factoryOwner: devnetAccount() }), + ); + }); + }); +}); diff --git a/tests-integration/upgrade.test.ts b/tests-integration/upgrade.test.ts new file mode 100644 index 0000000..250a03e --- /dev/null +++ b/tests-integration/upgrade.test.ts @@ -0,0 +1,242 @@ +import { CallData, hash, num } from "starknet"; +import { + deployer, + devnetAccount, + expectEvent, + expectRevertWithErrorMessage, + manager, + protocolCache, + setupGiftProtocol, +} from "../lib"; +// Time window which must pass before the upgrade can be performed +const MIN_SECURITY_PERIOD = 7n * 24n * 60n * 60n; // 7 day + +// Time window during which the upgrade can be performed +const VALID_WINDOW_PERIOD = 7n * 24n * 60n * 60n; // 7 day + +const CURRENT_TIME = 1718898082n; + +describe("Test Factory Upgrade", function () { + it("Upgrade", async function () { + const { factory } = await setupGiftProtocol(); + const newFactoryClassHash = await manager.declareLocalContract("FutureFactory"); + const calldata: any[] = []; + + await manager.setTime(CURRENT_TIME); + factory.connect(deployer); + await factory.propose_upgrade(newFactoryClassHash, calldata); + + await factory.get_pending_upgrade().should.eventually.deep.equal({ + ready_at: CURRENT_TIME + MIN_SECURITY_PERIOD, + implementation: num.toBigInt(newFactoryClassHash), + calldata_hash: BigInt(hash.computePoseidonHashOnElements(calldata)), + }); + + await manager.setTime(CURRENT_TIME + MIN_SECURITY_PERIOD + 1n); + await factory.upgrade(calldata); + + // check storage was reset + await factory.get_pending_upgrade().should.eventually.deep.equal({ + ready_at: 0n, + implementation: 0n, + calldata_hash: 0n, + }); + + await manager.getClassHashAt(factory.address).should.eventually.equal(newFactoryClassHash); + + // test new factory has new method + const newFactory = await manager.loadContract(factory.address, newFactoryClassHash); + newFactory.connect(deployer); + await newFactory.get_num().should.eventually.equal(1n); + + // we can't call the perform_upgrade method directly + await expectRevertWithErrorMessage("upgrade/only-during-upgrade", () => + factory.perform_upgrade(newFactoryClassHash, []), + ); + + // clear deployment cache + delete protocolCache["GiftFactory"]; + }); + + it("cannot downgrade", async function () { + const { factory } = await setupGiftProtocol(); + const oldFactoryClassHash = await manager.getClassHashAt(factory.address); + const calldata: any[] = []; + + await manager.setTime(CURRENT_TIME); + factory.connect(deployer); + await factory.propose_upgrade(oldFactoryClassHash, calldata); + + await manager.setTime(CURRENT_TIME + MIN_SECURITY_PERIOD + 1n); + await expectRevertWithErrorMessage("gift-fac/downgrade-not-allowed", () => factory.upgrade([])); + }); + + it("only-owner", async function () { + const { factory } = await setupGiftProtocol(); + const newFactoryClassHash = "0x1"; + + await manager.setTime(CURRENT_TIME); + factory.connect(deployer); + await factory.propose_upgrade(newFactoryClassHash, []); + + await manager.setTime(CURRENT_TIME + MIN_SECURITY_PERIOD + 1n); + factory.connect(devnetAccount()); + await expectRevertWithErrorMessage("Caller is not the owner", () => factory.upgrade([])); + }); + + it("no calls to perform_upgrade", async function () { + const { factory } = await setupGiftProtocol(); + const newFactoryClassHash = "0x1"; + await expectRevertWithErrorMessage("upgrade/only-during-upgrade", () => + factory.perform_upgrade(newFactoryClassHash, []), + ); + }); + + it("Invalid Calldata", async function () { + const { factory } = await setupGiftProtocol(); + const newFactoryClassHash = "0x1"; + const calldata = [1, 2, 3]; + + await manager.setTime(CURRENT_TIME); + factory.connect(deployer); + await factory.propose_upgrade(newFactoryClassHash, calldata); + + await manager.setTime(CURRENT_TIME + MIN_SECURITY_PERIOD + 1n); + const newCalldata = [4, 5, 6]; + await expectRevertWithErrorMessage("upgrade/invalid-calldata", () => factory.upgrade(newCalldata)); + }); + + it("Too Early", async function () { + const { factory } = await setupGiftProtocol(); + const newFactoryClassHash = "0x1"; + + await manager.setTime(CURRENT_TIME); + factory.connect(deployer); + await factory.propose_upgrade(newFactoryClassHash, []); + + await manager.setTime(CURRENT_TIME + MIN_SECURITY_PERIOD - 1n); + await expectRevertWithErrorMessage("upgrade/too-early", () => factory.upgrade([])); + }); + + it("Too Late", async function () { + const { factory } = await setupGiftProtocol(); + const newFactoryClassHash = "0x1"; + + await manager.setTime(CURRENT_TIME); + factory.connect(deployer); + await factory.propose_upgrade(newFactoryClassHash, []); + + const pendingUpgrade = await factory.get_pending_upgrade(); + const readyAt = pendingUpgrade.ready_at; + await manager.setTime(CURRENT_TIME + readyAt + VALID_WINDOW_PERIOD); + await expectRevertWithErrorMessage("upgrade/upgrade-too-late", () => factory.upgrade([])); + }); + + describe("Propose Upgrade", function () { + it("implementation-null", async function () { + const { factory } = await setupGiftProtocol(); + const zeroClassHash = "0x0"; + + factory.connect(deployer); + await expectRevertWithErrorMessage("upgrade/new-implementation-null", () => + factory.propose_upgrade(zeroClassHash, []), + ); + }); + + it("only-owner", async function () { + const { factory } = await setupGiftProtocol(); + const newFactoryClassHash = "0x1"; + + factory.connect(devnetAccount()); + await expectRevertWithErrorMessage("Caller is not the owner", () => + factory.propose_upgrade(newFactoryClassHash, []), + ); + }); + + it("replace pending implementation /w events", async function () { + const { factory } = await setupGiftProtocol(); + const newClassHash = 12345n; + const replacementClassHash = 54321n; + const calldata: any[] = [123n]; + + await manager.setTime(CURRENT_TIME); + factory.connect(deployer); + const { transaction_hash: tx1 } = await factory.propose_upgrade(newClassHash, calldata); + + const expectedPendingUpgrade = { + ready_at: CURRENT_TIME + MIN_SECURITY_PERIOD, + implementation: num.toBigInt(newClassHash), + calldata_hash: BigInt(hash.computePoseidonHashOnElements(calldata)), + }; + await factory.get_pending_upgrade().should.eventually.deep.equal(expectedPendingUpgrade); + + await expectEvent(tx1, { + from_address: factory.address, + eventName: "UpgradeProposed", + data: CallData.compile([newClassHash.toString(), expectedPendingUpgrade.ready_at.toString(), calldata]), + }); + + const { transaction_hash: tx2 } = await factory.propose_upgrade(replacementClassHash, calldata); + + await factory.get_pending_upgrade().should.eventually.deep.equal({ + ...expectedPendingUpgrade, + implementation: num.toBigInt(replacementClassHash), + }); + + await expectEvent(tx2, { + from_address: factory.address, + eventName: "UpgradeCancelled", + data: [ + newClassHash.toString(), + expectedPendingUpgrade.ready_at.toString(), + expectedPendingUpgrade.calldata_hash.toString(), + ], + }); + }); + }); + + describe("Cancel Upgrade", function () { + it("Normal flow /w events", async function () { + const { factory } = await setupGiftProtocol(); + const newClassHash = 12345n; + const calldata: any[] = []; + + await manager.setTime(CURRENT_TIME); + factory.connect(deployer); + await factory.propose_upgrade(newClassHash, calldata); + + const { transaction_hash } = await factory.cancel_upgrade(); + + await expectEvent(transaction_hash, { + from_address: factory.address, + eventName: "UpgradeCancelled", + data: [ + newClassHash.toString(), + (CURRENT_TIME + MIN_SECURITY_PERIOD).toString(), + BigInt(hash.computePoseidonHashOnElements(calldata)).toString(), + ], + }); + + // check storage was reset + await factory.get_pending_upgrade().should.eventually.deep.equal({ + ready_at: 0n, + implementation: 0n, + calldata_hash: 0n, + }); + }); + + it("No new implementation", async function () { + const { factory } = await setupGiftProtocol(); + + factory.connect(deployer); + await expectRevertWithErrorMessage("upgrade/no-pending-upgrade", () => factory.cancel_upgrade()); + }); + + it("Only Owner", async function () { + const { factory } = await setupGiftProtocol(); + + factory.connect(devnetAccount()); + await expectRevertWithErrorMessage("Caller is not the owner", () => factory.cancel_upgrade()); + }); + }); +}); diff --git a/tests/constants.cairo b/tests/constants.cairo new file mode 100644 index 0000000..fa45502 --- /dev/null +++ b/tests/constants.cairo @@ -0,0 +1,25 @@ +use snforge_std::signature::{ + KeyPair, KeyPairTrait, stark_curve::{StarkCurveKeyPairImpl, StarkCurveSignerImpl, StarkCurveVerifierImpl}, +}; +use starknet::ContractAddress; + +pub fn OWNER() -> ContractAddress { + 'OWNER'.try_into().unwrap() +} + +pub fn DEPOSITOR() -> ContractAddress { + 'DEPOSITOR'.try_into().unwrap() +} + +pub fn CLAIMER() -> ContractAddress { + 'CLAIMER'.try_into().unwrap() +} + +pub fn CLAIM_PUB_KEY() -> felt252 { + let new_owner = KeyPairTrait::from_secret_key('CLAIM'); + new_owner.public_key +} + +pub fn UNAUTHORIZED_ERC20() -> ContractAddress { + 'UNAUTHORIZED ERC20'.try_into().unwrap() +} diff --git a/tests/lib.cairo b/tests/lib.cairo new file mode 100644 index 0000000..1e8af34 --- /dev/null +++ b/tests/lib.cairo @@ -0,0 +1,4 @@ +mod constants; +mod setup; +mod test_claim_hash; +mod test_deposit; diff --git a/tests/setup.cairo b/tests/setup.cairo new file mode 100644 index 0000000..19e187a --- /dev/null +++ b/tests/setup.cairo @@ -0,0 +1,127 @@ +use argent_gifting::contracts::gift_factory::{IGiftFactory, IGiftFactoryDispatcher, IGiftFactoryDispatcherTrait}; + +use argent_gifting::contracts::utils::{STRK_ADDRESS, ETH_ADDRESS}; +use openzeppelin::token::erc20::interface::{IERC20Dispatcher, IERC20DispatcherTrait}; +use openzeppelin::utils::serde::SerializedAppend; + +use snforge_std::{declare, ContractClassTrait, ContractClass, start_cheat_caller_address, stop_cheat_caller_address}; +use starknet::{ClassHash, ContractAddress}; + +use super::constants::{OWNER, DEPOSITOR, CLAIMER}; + +const ERC20_SUPPLY: u256 = 1_000_000_000_000_000_000; + +pub struct GiftingSetup { + pub mock_eth: IERC20Dispatcher, + pub mock_strk: IERC20Dispatcher, + pub gift_factory: IGiftFactoryDispatcher, + pub escrow_class_hash: ClassHash, +} + +// This will return a valid ETH but a broken STRK at their respective addresses +pub fn deploy_gifting_broken_erc20() -> GiftingSetup { + let mock_erc20 = declare("MockERC20").expect('Failed to declare ERC20'); + + // mock ETH contract + let mock_eth = deploy_erc20_at(mock_erc20, "ETHER", "ETH", ETH_ADDRESS()); + + let broken_erc20 = deploy_broken_erc20_at(STRK_ADDRESS()); + + // escrow contract + let escrow_contract = declare("EscrowAccount").expect('Failed to declare escrow'); + + // escrow lib contract + let escrow_lib_contract = declare("EscrowLibrary").expect('Failed to declare escrow lib'); + + // gift factory + let factory_contract = declare("GiftFactory").expect('Failed to declare factory'); + let mut factory_calldata: Array = array![ + escrow_contract.class_hash.try_into().unwrap(), + escrow_lib_contract.class_hash.try_into().unwrap(), + OWNER().try_into().unwrap() + ]; + let (factory_contract_address, _) = factory_contract.deploy(@factory_calldata).expect('Failed to deploy factory'); + let gift_factory = IGiftFactoryDispatcher { contract_address: factory_contract_address }; + assert(gift_factory.get_latest_escrow_class_hash() == escrow_contract.class_hash, 'Incorrect factory setup'); + + // Approving eth transfers + start_cheat_caller_address(mock_eth.contract_address, OWNER()); + mock_eth.transfer(DEPOSITOR(), 1000); + start_cheat_caller_address(mock_eth.contract_address, DEPOSITOR()); + mock_eth.approve(factory_contract_address, 1000); + stop_cheat_caller_address(mock_eth.contract_address); + + GiftingSetup { mock_eth, mock_strk: broken_erc20, gift_factory, escrow_class_hash: escrow_contract.class_hash } +} + +pub fn deploy_broken_erc20_at(at: ContractAddress) -> IERC20Dispatcher { + let broken_erc20 = declare("BrokenERC20").expect('Failed to declare broken ERC20'); + let mut broken_erc20_calldata: Array = array![]; + let (broken_erc20_address, _) = broken_erc20 + .deploy_at(@broken_erc20_calldata, at) + .expect('Failed to deploy broken ERC20'); + IERC20Dispatcher { contract_address: broken_erc20_address } +} + + +pub fn deploy_erc20_at( + mock_erc20: ContractClass, name: ByteArray, symbol: ByteArray, at: ContractAddress +) -> IERC20Dispatcher { + // mock ETH contract + let mut mock_eth_calldata: Array = array![]; + + mock_eth_calldata.append_serde(name); + mock_eth_calldata.append_serde(symbol); + mock_eth_calldata.append_serde(ERC20_SUPPLY); + mock_eth_calldata.append_serde(OWNER()); + mock_eth_calldata.append_serde(OWNER()); + let (mock_eth_address, _) = mock_erc20.deploy_at(@mock_eth_calldata, at).expect('Failed to deploy'); + IERC20Dispatcher { contract_address: mock_eth_address } +} + +pub fn deploy_gifting_normal() -> GiftingSetup { + let mock_erc20 = declare("MockERC20").expect('Failed to declare ERC20'); + + // mock ETH contract + let mock_eth = deploy_erc20_at(mock_erc20, "ETHER", "ETH", ETH_ADDRESS()); + assert(mock_eth.balance_of(OWNER()) == ERC20_SUPPLY, 'Failed to mint ETH'); + + // mock STRK contract + let mock_strk = deploy_erc20_at(mock_erc20, "STARK", "STRK", STRK_ADDRESS()); + assert(mock_strk.balance_of(OWNER()) == ERC20_SUPPLY, 'Failed to mint STRK'); + + // escrow contract + let escrow_contract = declare("EscrowAccount").expect('Failed to declare escrow'); + + // escrow lib contract + let escrow_lib_contract = declare("EscrowLibrary").expect('Failed to declare escrow lib'); + + // gift factory + let factory_contract = declare("GiftFactory").expect('Failed to declare factory'); + let mut factory_calldata: Array = array![ + escrow_contract.class_hash.try_into().unwrap(), + escrow_lib_contract.class_hash.try_into().unwrap(), + OWNER().try_into().unwrap() + ]; + let (factory_contract_address, _) = factory_contract.deploy(@factory_calldata).expect('Failed to deploy factory'); + let gift_factory = IGiftFactoryDispatcher { contract_address: factory_contract_address }; + assert(gift_factory.get_latest_escrow_class_hash() == escrow_contract.class_hash, 'Incorrect factory setup'); + + start_cheat_caller_address(mock_eth.contract_address, OWNER()); + start_cheat_caller_address(mock_strk.contract_address, OWNER()); + mock_eth.transfer(DEPOSITOR(), 1000); + mock_strk.transfer(DEPOSITOR(), 1000); + start_cheat_caller_address(mock_eth.contract_address, DEPOSITOR()); + start_cheat_caller_address(mock_strk.contract_address, DEPOSITOR()); + mock_eth.approve(factory_contract_address, 1000); + mock_strk.approve(factory_contract_address, 1000); + stop_cheat_caller_address(mock_eth.contract_address); + stop_cheat_caller_address(mock_strk.contract_address); + + assert(mock_eth.balance_of(DEPOSITOR()) == 1000, 'Failed to transfer ETH'); + assert(mock_strk.balance_of(DEPOSITOR()) == 1000, 'Failed to transfer STRK'); + assert(mock_eth.allowance(DEPOSITOR(), factory_contract_address) == 1000, 'Failed to approve ETH'); + assert(mock_strk.allowance(DEPOSITOR(), factory_contract_address) == 1000, 'Failed to approve STRK'); + + GiftingSetup { mock_eth, mock_strk, gift_factory, escrow_class_hash: escrow_contract.class_hash } +} diff --git a/tests/test_claim_hash.cairo b/tests/test_claim_hash.cairo new file mode 100644 index 0000000..ac2c1c3 --- /dev/null +++ b/tests/test_claim_hash.cairo @@ -0,0 +1,48 @@ +use argent_gifting::contracts::claim_hash::{ + IStructHashRev1, StarknetDomain, MAINNET_FIRST_HADES_PERMUTATION, SEPOLIA_FIRST_HADES_PERMUTATION +}; +use core::poseidon::hades_permutation; +use snforge_std::cheat_chain_id_global; +use starknet::get_tx_info; + + +fn get_domain_hash() -> felt252 { + let domain = StarknetDomain { + name: 'GiftFactory.claim_external', version: '1', chain_id: get_tx_info().unbox().chain_id, revision: 1, + }; + domain.get_struct_hash_rev_1() +} + +#[test] +fn precalculated_hash_sepolia() { + cheat_chain_id_global('SN_SEPOLIA'); + let domain_hash = get_domain_hash(); + + assert_eq!( + domain_hash, + 1044702367038635622945218048687216661819128576871663722017781331499517520675, + "Precalculated domain hash is incorrect" + ); + let (ch0, ch1, ch2) = hades_permutation('StarkNet Message', domain_hash, 0); + let (pch0, pch1, pch2) = SEPOLIA_FIRST_HADES_PERMUTATION; + assert_eq!(ch0, pch0, "pch0 incorrect"); + assert_eq!(ch1, pch1, "pch1 incorrect"); + assert_eq!(ch2, pch2, "pch2 incorrect"); +} + +#[test] +fn precalculated_hash_mainnet() { + cheat_chain_id_global('SN_MAIN'); + let domain_hash = get_domain_hash(); + + assert_eq!( + domain_hash, + 234325029197410387606259685107849809841952619146295364245967447938203337307, + "Precalculated domain hash is incorrect" + ); + let (ch0, ch1, ch2) = hades_permutation('StarkNet Message', domain_hash, 0); + let (pch0, pch1, pch2) = MAINNET_FIRST_HADES_PERMUTATION; + assert_eq!(ch0, pch0, "pch0 incorrect"); + assert_eq!(ch1, pch1, "pch1 incorrect"); + assert_eq!(ch2, pch2, "pch2 incorrect"); +} diff --git a/tests/test_deposit.cairo b/tests/test_deposit.cairo new file mode 100644 index 0000000..1a07529 --- /dev/null +++ b/tests/test_deposit.cairo @@ -0,0 +1,30 @@ +use argent_gifting::contracts::gift_factory::{IGiftFactoryDispatcherTrait}; +use openzeppelin::token::erc20::interface::{IERC20Dispatcher, IERC20DispatcherTrait}; +use snforge_std::{start_cheat_caller_address}; +use super::constants::DEPOSITOR; +use super::setup::{GiftingSetup, deploy_gifting_broken_erc20}; + +#[test] +#[should_panic(expected: ('gift-fac/transfer-failed',))] +fn test_deposit_same_token_failing_transfer() { + let GiftingSetup { .., mock_strk, gift_factory, escrow_class_hash } = deploy_gifting_broken_erc20(); + gift_factory.deposit(escrow_class_hash, mock_strk.contract_address, 101, mock_strk.contract_address, 100, 12); +} + +#[test] +#[should_panic(expected: ('gift-fac/transfer-gift-failed',))] +fn test_deposit_different_token_failing_gift_transfer() { + let GiftingSetup { mock_eth, mock_strk, gift_factory, escrow_class_hash } = deploy_gifting_broken_erc20(); + let broken_erc20 = mock_strk; + start_cheat_caller_address(gift_factory.contract_address, DEPOSITOR()); + gift_factory.deposit(escrow_class_hash, broken_erc20.contract_address, 100, mock_eth.contract_address, 100, 42); +} + +#[test] +#[should_panic(expected: ('gift-fac/transfer-fee-failed',))] +fn test_deposit_different_token_failing_fee_transfer() { + let GiftingSetup { mock_eth, mock_strk, gift_factory, escrow_class_hash } = deploy_gifting_broken_erc20(); + let broken_erc20 = mock_strk; + start_cheat_caller_address(gift_factory.contract_address, DEPOSITOR()); + gift_factory.deposit(escrow_class_hash, mock_eth.contract_address, 100, broken_erc20.contract_address, 100, 42); +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..5a32d0d --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,16 @@ +{ + "extends": "@tsconfig/node18/tsconfig.json", + "compilerOptions": { + "outDir": "dist", + "esModuleInterop": true, + "module": "ESNext", + "moduleResolution": "Node", + "lib": ["es2020", "dom"] + }, + "ts-node": { + "esm": true, + "experimentalSpecifierResolution": "node" + }, + "include": ["/**/*.ts"], + "exclude": ["node_modules", "cairo", "examples"] +} diff --git a/yarn.lock b/yarn.lock new file mode 100644 index 0000000..69c01b2 --- /dev/null +++ b/yarn.lock @@ -0,0 +1,1700 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@adraffy/ens-normalize@1.10.0": + version "1.10.0" + resolved "https://registry.yarnpkg.com/@adraffy/ens-normalize/-/ens-normalize-1.10.0.tgz#d2a39395c587e092d77cbbc80acf956a54f38bf7" + integrity sha512-nA9XHtlAkYfJxY7bce8DcN7eKxWWCWkU+1GR9d+U6MbNpfwQp8TI7vqOsBsMcHoT4mBu2kypKoSKnghEzOOq5Q== + +"@cspotcode/source-map-support@^0.8.0": + version "0.8.1" + resolved "https://registry.yarnpkg.com/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz#00629c35a688e05a88b1cda684fb9d5e73f000a1" + integrity sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw== + dependencies: + "@jridgewell/trace-mapping" "0.3.9" + +"@eslint-community/eslint-utils@^4.2.0": + version "4.4.0" + resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz#a23514e8fb9af1269d5f7788aa556798d61c6b59" + integrity sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA== + dependencies: + eslint-visitor-keys "^3.3.0" + +"@eslint-community/regexpp@^4.4.0", "@eslint-community/regexpp@^4.6.1": + version "4.10.0" + resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.10.0.tgz#548f6de556857c8bb73bbee70c35dc82a2e74d63" + integrity sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA== + +"@eslint/eslintrc@^2.1.4": + version "2.1.4" + resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-2.1.4.tgz#388a269f0f25c1b6adc317b5a2c55714894c70ad" + integrity sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ== + dependencies: + ajv "^6.12.4" + debug "^4.3.2" + espree "^9.6.0" + globals "^13.19.0" + ignore "^5.2.0" + import-fresh "^3.2.1" + js-yaml "^4.1.0" + minimatch "^3.1.2" + strip-json-comments "^3.1.1" + +"@eslint/js@8.57.0": + version "8.57.0" + resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.57.0.tgz#a5417ae8427873f1dd08b70b3574b453e67b5f7f" + integrity sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g== + +"@humanwhocodes/config-array@^0.11.14": + version "0.11.14" + resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.14.tgz#d78e481a039f7566ecc9660b4ea7fe6b1fec442b" + integrity sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg== + dependencies: + "@humanwhocodes/object-schema" "^2.0.2" + debug "^4.3.1" + minimatch "^3.0.5" + +"@humanwhocodes/module-importer@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz#af5b2691a22b44be847b0ca81641c5fb6ad0172c" + integrity sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA== + +"@humanwhocodes/object-schema@^2.0.2": + version "2.0.3" + resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz#4a2868d75d6d6963e423bcf90b7fd1be343409d3" + integrity sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA== + +"@jridgewell/resolve-uri@^3.0.3": + version "3.1.2" + resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz#7a0ee601f60f99a20c7c7c5ff0c80388c1189bd6" + integrity sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw== + +"@jridgewell/sourcemap-codec@^1.4.10": + version "1.4.15" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz#d7c6e6755c78567a951e04ab52ef0fd26de59f32" + integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg== + +"@jridgewell/trace-mapping@0.3.9": + version "0.3.9" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz#6534fd5933a53ba7cbf3a17615e273a0d1273ff9" + integrity sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ== + dependencies: + "@jridgewell/resolve-uri" "^3.0.3" + "@jridgewell/sourcemap-codec" "^1.4.10" + +"@noble/curves@1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.2.0.tgz#92d7e12e4e49b23105a2555c6984d41733d65c35" + integrity sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw== + dependencies: + "@noble/hashes" "1.3.2" + +"@noble/curves@~1.3.0": + version "1.3.0" + resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.3.0.tgz#01be46da4fd195822dab821e72f71bf4aeec635e" + integrity sha512-t01iSXPuN+Eqzb4eBX0S5oubSqXbK/xXa1Ne18Hj8f9pStxztHCE2gfboSp/dZRLSqfuLpRK2nDXDK+W9puocA== + dependencies: + "@noble/hashes" "1.3.3" + +"@noble/hashes@1.3.2": + version "1.3.2" + resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.3.2.tgz#6f26dbc8fbc7205873ce3cee2f690eba0d421b39" + integrity sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ== + +"@noble/hashes@1.3.3", "@noble/hashes@~1.3.3": + version "1.3.3" + resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.3.3.tgz#39908da56a4adc270147bb07968bf3b16cfe1699" + integrity sha512-V7/fPHgl+jsVPXqqeOzT8egNj2iBIVt+ECeMMG8TdcnTikP3oaBtUVqpT/gYCR68aEBJSF+XbYUxStjbFMqIIA== + +"@nodelib/fs.scandir@2.1.5": + version "2.1.5" + resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" + integrity sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g== + dependencies: + "@nodelib/fs.stat" "2.0.5" + run-parallel "^1.1.9" + +"@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2": + version "2.0.5" + resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz#5bd262af94e9d25bd1e71b05deed44876a222e8b" + integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A== + +"@nodelib/fs.walk@^1.2.3", "@nodelib/fs.walk@^1.2.8": + version "1.2.8" + resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz#e95737e8bb6746ddedf69c556953494f196fe69a" + integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg== + dependencies: + "@nodelib/fs.scandir" "2.1.5" + fastq "^1.6.0" + +"@scure/base@~1.1.3": + version "1.1.6" + resolved "https://registry.yarnpkg.com/@scure/base/-/base-1.1.6.tgz#8ce5d304b436e4c84f896e0550c83e4d88cb917d" + integrity sha512-ok9AWwhcgYuGG3Zfhyqg+zwl+Wn5uE+dwC0NV/2qQkx4dABbb/bx96vWu8NSj+BNjjSjno+JRYRjle1jV08k3g== + +"@scure/starknet@~1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@scure/starknet/-/starknet-1.0.0.tgz#4419bc2fdf70f3dd6cb461d36c878c9ef4419f8c" + integrity sha512-o5J57zY0f+2IL/mq8+AYJJ4Xpc1fOtDhr+mFQKbHnYFmm3WQrC+8zj2HEgxak1a+x86mhmBC1Kq305KUpVf0wg== + dependencies: + "@noble/curves" "~1.3.0" + "@noble/hashes" "~1.3.3" + +"@tsconfig/node10@^1.0.7": + version "1.0.11" + resolved "https://registry.yarnpkg.com/@tsconfig/node10/-/node10-1.0.11.tgz#6ee46400685f130e278128c7b38b7e031ff5b2f2" + integrity sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw== + +"@tsconfig/node12@^1.0.7": + version "1.0.11" + resolved "https://registry.yarnpkg.com/@tsconfig/node12/-/node12-1.0.11.tgz#ee3def1f27d9ed66dac6e46a295cffb0152e058d" + integrity sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag== + +"@tsconfig/node14@^1.0.0": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@tsconfig/node14/-/node14-1.0.3.tgz#e4386316284f00b98435bf40f72f75a09dabf6c1" + integrity sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow== + +"@tsconfig/node16@^1.0.2": + version "1.0.4" + resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.4.tgz#0b92dcc0cc1c81f6f306a381f28e31b1a56536e9" + integrity sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA== + +"@tsconfig/node18@^2.0.0": + version "2.0.1" + resolved "https://registry.yarnpkg.com/@tsconfig/node18/-/node18-2.0.1.tgz#2d2e11333ef2b75a4623203daca264e6697d693b" + integrity sha512-UqdfvuJK0SArA2CxhKWwwAWfnVSXiYe63bVpMutc27vpngCntGUZQETO24pEJ46zU6XM+7SpqYoMgcO3bM11Ew== + +"@types/chai-as-promised@^7.1.5": + version "7.1.8" + resolved "https://registry.yarnpkg.com/@types/chai-as-promised/-/chai-as-promised-7.1.8.tgz#f2b3d82d53c59626b5d6bbc087667ccb4b677fe9" + integrity sha512-ThlRVIJhr69FLlh6IctTXFkmhtP3NpMZ2QGq69StYLyKZFp/HOp1VdKZj7RvfNWYYcJ1xlbLGLLWj1UvP5u/Gw== + dependencies: + "@types/chai" "*" + +"@types/chai@*", "@types/chai@^4.3.4": + version "4.3.16" + resolved "https://registry.yarnpkg.com/@types/chai/-/chai-4.3.16.tgz#b1572967f0b8b60bf3f87fe1d854a5604ea70c82" + integrity sha512-PatH4iOdyh3MyWtmHVFXLWCCIhUbopaltqddG9BzB+gMIzee2MJrvd+jouii9Z3wzQJruGWAm7WOMjgfG8hQlQ== + +"@types/json-schema@^7.0.9": + version "7.0.15" + resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841" + integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== + +"@types/lodash-es@^4.17.8": + version "4.17.12" + resolved "https://registry.yarnpkg.com/@types/lodash-es/-/lodash-es-4.17.12.tgz#65f6d1e5f80539aa7cfbfc962de5def0cf4f341b" + integrity sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ== + dependencies: + "@types/lodash" "*" + +"@types/lodash@*": + version "4.17.4" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.17.4.tgz#0303b64958ee070059e3a7184048a55159fe20b7" + integrity sha512-wYCP26ZLxaT3R39kiN2+HcJ4kTd3U1waI/cY7ivWYqFP6pW3ZNpvi6Wd6PHZx7T/t8z0vlkXMg3QYLa7DZ/IJQ== + +"@types/mocha@^10.0.1": + version "10.0.6" + resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-10.0.6.tgz#818551d39113081048bdddbef96701b4e8bb9d1b" + integrity sha512-dJvrYWxP/UcXm36Qn36fxhUKu8A/xMRXVT2cliFF1Z7UA9liG5Psj3ezNSZw+5puH2czDXRLcXQxf8JbJt0ejg== + +"@types/node@18.15.13": + version "18.15.13" + resolved "https://registry.yarnpkg.com/@types/node/-/node-18.15.13.tgz#f64277c341150c979e42b00e4ac289290c9df469" + integrity sha512-N+0kuo9KgrUQ1Sn/ifDXsvg0TTleP7rIy4zOBGECxAljqvqfqpTfzx0Q1NUedOixRMBfe2Whhb056a42cWs26Q== + +"@types/node@^20.11.30": + version "20.12.13" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.12.13.tgz#90ed3b8a4e52dd3c5dc5a42dde5b85b74ad8ed88" + integrity sha512-gBGeanV41c1L171rR7wjbMiEpEI/l5XFQdLLfhr/REwpgDy/4U8y89+i8kRiLzDyZdOkXh+cRaTetUnCYutoXA== + dependencies: + undici-types "~5.26.4" + +"@types/semver@^7.3.12": + version "7.5.8" + resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.5.8.tgz#8268a8c57a3e4abd25c165ecd36237db7948a55e" + integrity sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ== + +"@typescript-eslint/eslint-plugin@^5.61.0": + version "5.62.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.62.0.tgz#aeef0328d172b9e37d9bab6dbc13b87ed88977db" + integrity sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag== + dependencies: + "@eslint-community/regexpp" "^4.4.0" + "@typescript-eslint/scope-manager" "5.62.0" + "@typescript-eslint/type-utils" "5.62.0" + "@typescript-eslint/utils" "5.62.0" + debug "^4.3.4" + graphemer "^1.4.0" + ignore "^5.2.0" + natural-compare-lite "^1.4.0" + semver "^7.3.7" + tsutils "^3.21.0" + +"@typescript-eslint/parser@^5.61.0": + version "5.62.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.62.0.tgz#1b63d082d849a2fcae8a569248fbe2ee1b8a56c7" + integrity sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA== + dependencies: + "@typescript-eslint/scope-manager" "5.62.0" + "@typescript-eslint/types" "5.62.0" + "@typescript-eslint/typescript-estree" "5.62.0" + debug "^4.3.4" + +"@typescript-eslint/scope-manager@5.62.0": + version "5.62.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.62.0.tgz#d9457ccc6a0b8d6b37d0eb252a23022478c5460c" + integrity sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w== + dependencies: + "@typescript-eslint/types" "5.62.0" + "@typescript-eslint/visitor-keys" "5.62.0" + +"@typescript-eslint/type-utils@5.62.0": + version "5.62.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.62.0.tgz#286f0389c41681376cdad96b309cedd17d70346a" + integrity sha512-xsSQreu+VnfbqQpW5vnCJdq1Z3Q0U31qiWmRhr98ONQmcp/yhiPJFPq8MXiJVLiksmOKSjIldZzkebzHuCGzew== + dependencies: + "@typescript-eslint/typescript-estree" "5.62.0" + "@typescript-eslint/utils" "5.62.0" + debug "^4.3.4" + tsutils "^3.21.0" + +"@typescript-eslint/types@5.62.0": + version "5.62.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.62.0.tgz#258607e60effa309f067608931c3df6fed41fd2f" + integrity sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ== + +"@typescript-eslint/typescript-estree@5.62.0": + version "5.62.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.62.0.tgz#7d17794b77fabcac615d6a48fb143330d962eb9b" + integrity sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA== + dependencies: + "@typescript-eslint/types" "5.62.0" + "@typescript-eslint/visitor-keys" "5.62.0" + debug "^4.3.4" + globby "^11.1.0" + is-glob "^4.0.3" + semver "^7.3.7" + tsutils "^3.21.0" + +"@typescript-eslint/utils@5.62.0": + version "5.62.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.62.0.tgz#141e809c71636e4a75daa39faed2fb5f4b10df86" + integrity sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ== + dependencies: + "@eslint-community/eslint-utils" "^4.2.0" + "@types/json-schema" "^7.0.9" + "@types/semver" "^7.3.12" + "@typescript-eslint/scope-manager" "5.62.0" + "@typescript-eslint/types" "5.62.0" + "@typescript-eslint/typescript-estree" "5.62.0" + eslint-scope "^5.1.1" + semver "^7.3.7" + +"@typescript-eslint/visitor-keys@5.62.0": + version "5.62.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.62.0.tgz#2174011917ce582875954ffe2f6912d5931e353e" + integrity sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw== + dependencies: + "@typescript-eslint/types" "5.62.0" + eslint-visitor-keys "^3.3.0" + +"@ungap/structured-clone@^1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.2.0.tgz#756641adb587851b5ccb3e095daf27ae581c8406" + integrity sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ== + +abi-wan-kanabi@^2.2.1: + version "2.2.2" + resolved "https://registry.yarnpkg.com/abi-wan-kanabi/-/abi-wan-kanabi-2.2.2.tgz#82c48e8fa08d9016cf92d3d81d494cc60e934693" + integrity sha512-sTCv2HyNIj1x2WFUoc9oL8ZT9liosrL+GoqEGZJK1kDND096CfA7lwx06vLxLWMocQ41FQXO3oliwoh/UZHYdQ== + dependencies: + ansicolors "^0.3.2" + cardinal "^2.1.1" + fs-extra "^10.0.0" + yargs "^17.7.2" + +acorn-jsx@^5.3.2: + version "5.3.2" + resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" + integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== + +acorn-walk@^8.1.1: + version "8.3.2" + resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.3.2.tgz#7703af9415f1b6db9315d6895503862e231d34aa" + integrity sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A== + +acorn@^8.4.1, acorn@^8.9.0: + version "8.11.3" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.11.3.tgz#71e0b14e13a4ec160724b38fb7b0f233b1b81d7a" + integrity sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg== + +aes-js@4.0.0-beta.5: + version "4.0.0-beta.5" + resolved "https://registry.yarnpkg.com/aes-js/-/aes-js-4.0.0-beta.5.tgz#8d2452c52adedebc3a3e28465d858c11ca315873" + integrity sha512-G965FqalsNyrPqgEGON7nIx1e/OVENSgiEIzyC63haUMuvNnwIgIjMs52hlTCKhkBny7A2ORNlfY9Zu+jmGk1Q== + +ajv@^6.12.4: + version "6.12.6" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" + integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== + dependencies: + fast-deep-equal "^3.1.1" + fast-json-stable-stringify "^2.0.0" + json-schema-traverse "^0.4.1" + uri-js "^4.2.2" + +ansi-colors@4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.1.tgz#cbb9ae256bf750af1eab344f229aa27fe94ba348" + integrity sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA== + +ansi-regex@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" + integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== + +ansi-styles@^4.0.0, ansi-styles@^4.1.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" + integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== + dependencies: + color-convert "^2.0.1" + +ansicolors@^0.3.2, ansicolors@~0.3.2: + version "0.3.2" + resolved "https://registry.yarnpkg.com/ansicolors/-/ansicolors-0.3.2.tgz#665597de86a9ffe3aa9bfbe6cae5c6ea426b4979" + integrity sha512-QXu7BPrP29VllRxH8GwB7x5iX5qWKAAMLqKQGWTeLWVlNHNOpVMJ91dsxQAIWXpjuW5wqvxu3Jd/nRjrJ+0pqg== + +anymatch@~3.1.2: + version "3.1.3" + resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e" + integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw== + dependencies: + normalize-path "^3.0.0" + picomatch "^2.0.4" + +arg@^4.1.0: + version "4.1.3" + resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089" + integrity sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA== + +argparse@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" + integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== + +array-union@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d" + integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== + +assertion-error@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/assertion-error/-/assertion-error-1.1.0.tgz#e60b6b0e8f301bd97e5375215bda406c85118c0b" + integrity sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw== + +balanced-match@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" + integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== + +binary-extensions@^2.0.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.3.0.tgz#f6e14a97858d327252200242d4ccfe522c445522" + integrity sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw== + +brace-expansion@^1.1.7: + version "1.1.11" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" + integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + +brace-expansion@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.1.tgz#1edc459e0f0c548486ecf9fc99f2221364b9a0ae" + integrity sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA== + dependencies: + balanced-match "^1.0.0" + +braces@^3.0.3, braces@~3.0.2: + version "3.0.3" + resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789" + integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA== + dependencies: + fill-range "^7.1.1" + +browser-stdout@1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/browser-stdout/-/browser-stdout-1.3.1.tgz#baa559ee14ced73452229bad7326467c61fabd60" + integrity sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw== + +callsites@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" + integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== + +camelcase@^6.0.0: + version "6.3.0" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a" + integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== + +cardinal@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/cardinal/-/cardinal-2.1.1.tgz#7cc1055d822d212954d07b085dea251cc7bc5505" + integrity sha512-JSr5eOgoEymtYHBjNWyjrMqet9Am2miJhlfKNdqLp6zoeAh0KN5dRAcxlecj5mAJrmQomgiOBj35xHLrFjqBpw== + dependencies: + ansicolors "~0.3.2" + redeyed "~2.1.0" + +chai-as-promised@^7.1.1: + version "7.1.2" + resolved "https://registry.yarnpkg.com/chai-as-promised/-/chai-as-promised-7.1.2.tgz#70cd73b74afd519754161386421fb71832c6d041" + integrity sha512-aBDHZxRzYnUYuIAIPBH2s511DjlKPzXNlXSGFC8CwmroWQLfrW0LtE1nK3MAwwNhJPa9raEjNCmRoFpG0Hurdw== + dependencies: + check-error "^1.0.2" + +chai@^4.3.7: + version "4.4.1" + resolved "https://registry.yarnpkg.com/chai/-/chai-4.4.1.tgz#3603fa6eba35425b0f2ac91a009fe924106e50d1" + integrity sha512-13sOfMv2+DWduEU+/xbun3LScLoqN17nBeTLUsmDfKdoiC1fr0n9PU4guu4AhRcOVFk/sW8LyZWHuhWtQZiF+g== + dependencies: + assertion-error "^1.1.0" + check-error "^1.0.3" + deep-eql "^4.1.3" + get-func-name "^2.0.2" + loupe "^2.3.6" + pathval "^1.1.1" + type-detect "^4.0.8" + +chalk@^4.0.0, chalk@^4.1.0: + version "4.1.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" + integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + +check-error@^1.0.2, check-error@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/check-error/-/check-error-1.0.3.tgz#a6502e4312a7ee969f646e83bb3ddd56281bd694" + integrity sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg== + dependencies: + get-func-name "^2.0.2" + +chokidar@3.5.3: + version "3.5.3" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd" + integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw== + dependencies: + anymatch "~3.1.2" + braces "~3.0.2" + glob-parent "~5.1.2" + is-binary-path "~2.1.0" + is-glob "~4.0.1" + normalize-path "~3.0.0" + readdirp "~3.6.0" + optionalDependencies: + fsevents "~2.3.2" + +cliui@^7.0.2: + version "7.0.4" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-7.0.4.tgz#a0265ee655476fc807aea9df3df8df7783808b4f" + integrity sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ== + dependencies: + string-width "^4.2.0" + strip-ansi "^6.0.0" + wrap-ansi "^7.0.0" + +cliui@^8.0.1: + version "8.0.1" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-8.0.1.tgz#0c04b075db02cbfe60dc8e6cf2f5486b1a3608aa" + integrity sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ== + dependencies: + string-width "^4.2.0" + strip-ansi "^6.0.1" + wrap-ansi "^7.0.0" + +color-convert@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" + integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== + dependencies: + color-name "~1.1.4" + +color-name@~1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" + integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== + +create-require@^1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333" + integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ== + +cross-spawn@^7.0.2: + version "7.0.3" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" + integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== + dependencies: + path-key "^3.1.0" + shebang-command "^2.0.0" + which "^2.0.1" + +debug@4.3.4, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4: + version "4.3.4" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" + integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== + dependencies: + ms "2.1.2" + +decamelize@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-4.0.0.tgz#aa472d7bf660eb15f3494efd531cab7f2a709837" + integrity sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ== + +deep-eql@^4.1.3: + version "4.1.3" + resolved "https://registry.yarnpkg.com/deep-eql/-/deep-eql-4.1.3.tgz#7c7775513092f7df98d8df9996dd085eb668cc6d" + integrity sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw== + dependencies: + type-detect "^4.0.0" + +deep-is@^0.1.3: + version "0.1.4" + resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831" + integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ== + +diff@5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/diff/-/diff-5.0.0.tgz#7ed6ad76d859d030787ec35855f5b1daf31d852b" + integrity sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w== + +diff@^4.0.1: + version "4.0.2" + resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" + integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== + +dir-glob@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f" + integrity sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA== + dependencies: + path-type "^4.0.0" + +doctrine@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-3.0.0.tgz#addebead72a6574db783639dc87a121773973961" + integrity sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w== + dependencies: + esutils "^2.0.2" + +dotenv@^16.3.1: + version "16.4.5" + resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.4.5.tgz#cdd3b3b604cb327e286b4762e13502f717cb099f" + integrity sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg== + +emoji-regex@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" + integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== + +escalade@^3.1.1: + version "3.1.2" + resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.2.tgz#54076e9ab29ea5bf3d8f1ed62acffbb88272df27" + integrity sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA== + +escape-string-regexp@4.0.0, escape-string-regexp@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" + integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== + +eslint-scope@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.1.tgz#e786e59a66cb92b3f6c1fb0d508aab174848f48c" + integrity sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw== + dependencies: + esrecurse "^4.3.0" + estraverse "^4.1.1" + +eslint-scope@^7.2.2: + version "7.2.2" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-7.2.2.tgz#deb4f92563390f32006894af62a22dba1c46423f" + integrity sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg== + dependencies: + esrecurse "^4.3.0" + estraverse "^5.2.0" + +eslint-visitor-keys@^3.3.0, eslint-visitor-keys@^3.4.1, eslint-visitor-keys@^3.4.3: + version "3.4.3" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz#0cd72fe8550e3c2eae156a96a4dddcd1c8ac5800" + integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag== + +eslint@^8.44.0: + version "8.57.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.57.0.tgz#c786a6fd0e0b68941aaf624596fb987089195668" + integrity sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ== + dependencies: + "@eslint-community/eslint-utils" "^4.2.0" + "@eslint-community/regexpp" "^4.6.1" + "@eslint/eslintrc" "^2.1.4" + "@eslint/js" "8.57.0" + "@humanwhocodes/config-array" "^0.11.14" + "@humanwhocodes/module-importer" "^1.0.1" + "@nodelib/fs.walk" "^1.2.8" + "@ungap/structured-clone" "^1.2.0" + ajv "^6.12.4" + chalk "^4.0.0" + cross-spawn "^7.0.2" + debug "^4.3.2" + doctrine "^3.0.0" + escape-string-regexp "^4.0.0" + eslint-scope "^7.2.2" + eslint-visitor-keys "^3.4.3" + espree "^9.6.1" + esquery "^1.4.2" + esutils "^2.0.2" + fast-deep-equal "^3.1.3" + file-entry-cache "^6.0.1" + find-up "^5.0.0" + glob-parent "^6.0.2" + globals "^13.19.0" + graphemer "^1.4.0" + ignore "^5.2.0" + imurmurhash "^0.1.4" + is-glob "^4.0.0" + is-path-inside "^3.0.3" + js-yaml "^4.1.0" + json-stable-stringify-without-jsonify "^1.0.1" + levn "^0.4.1" + lodash.merge "^4.6.2" + minimatch "^3.1.2" + natural-compare "^1.4.0" + optionator "^0.9.3" + strip-ansi "^6.0.1" + text-table "^0.2.0" + +espree@^9.6.0, espree@^9.6.1: + version "9.6.1" + resolved "https://registry.yarnpkg.com/espree/-/espree-9.6.1.tgz#a2a17b8e434690a5432f2f8018ce71d331a48c6f" + integrity sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ== + dependencies: + acorn "^8.9.0" + acorn-jsx "^5.3.2" + eslint-visitor-keys "^3.4.1" + +esprima@~4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" + integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== + +esquery@^1.4.2: + version "1.5.0" + resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.5.0.tgz#6ce17738de8577694edd7361c57182ac8cb0db0b" + integrity sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg== + dependencies: + estraverse "^5.1.0" + +esrecurse@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.3.0.tgz#7ad7964d679abb28bee72cec63758b1c5d2c9921" + integrity sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag== + dependencies: + estraverse "^5.2.0" + +estraverse@^4.1.1: + version "4.3.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d" + integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw== + +estraverse@^5.1.0, estraverse@^5.2.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123" + integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA== + +esutils@^2.0.2: + version "2.0.3" + resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" + integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== + +ethers@6.8.1: + version "6.8.1" + resolved "https://registry.yarnpkg.com/ethers/-/ethers-6.8.1.tgz#ee2a1a39b5f62a13678f90ccd879175391d0a2b4" + integrity sha512-iEKm6zox5h1lDn6scuRWdIdFJUCGg3+/aQWu0F4K0GVyEZiktFkqrJbRjTn1FlYEPz7RKA707D6g5Kdk6j7Ljg== + dependencies: + "@adraffy/ens-normalize" "1.10.0" + "@noble/curves" "1.2.0" + "@noble/hashes" "1.3.2" + "@types/node" "18.15.13" + aes-js "4.0.0-beta.5" + tslib "2.4.0" + ws "8.5.0" + +fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" + integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== + +fast-glob@^3.2.9: + version "3.3.2" + resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.2.tgz#a904501e57cfdd2ffcded45e99a54fef55e46129" + integrity sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow== + dependencies: + "@nodelib/fs.stat" "^2.0.2" + "@nodelib/fs.walk" "^1.2.3" + glob-parent "^5.1.2" + merge2 "^1.3.0" + micromatch "^4.0.4" + +fast-json-stable-stringify@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" + integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== + +fast-levenshtein@^2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" + integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw== + +fastq@^1.6.0: + version "1.17.1" + resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.17.1.tgz#2a523f07a4e7b1e81a42b91b8bf2254107753b47" + integrity sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w== + dependencies: + reusify "^1.0.4" + +fetch-cookie@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/fetch-cookie/-/fetch-cookie-3.0.1.tgz#6a77f7495e1a639ae019db916a234db8c85d5963" + integrity sha512-ZGXe8Y5Z/1FWqQ9q/CrJhkUD73DyBU9VF0hBQmEO/wPHe4A9PKTjplFDLeFX8aOsYypZUcX5Ji/eByn3VCVO3Q== + dependencies: + set-cookie-parser "^2.4.8" + tough-cookie "^4.0.0" + +file-entry-cache@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz#211b2dd9659cb0394b073e7323ac3c933d522027" + integrity sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg== + dependencies: + flat-cache "^3.0.4" + +fill-range@^7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292" + integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg== + dependencies: + to-regex-range "^5.0.1" + +find-up@5.0.0, find-up@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc" + integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng== + dependencies: + locate-path "^6.0.0" + path-exists "^4.0.0" + +flat-cache@^3.0.4: + version "3.2.0" + resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.2.0.tgz#2c0c2d5040c99b1632771a9d105725c0115363ee" + integrity sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw== + dependencies: + flatted "^3.2.9" + keyv "^4.5.3" + rimraf "^3.0.2" + +flat@^5.0.2: + version "5.0.2" + resolved "https://registry.yarnpkg.com/flat/-/flat-5.0.2.tgz#8ca6fe332069ffa9d324c327198c598259ceb241" + integrity sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ== + +flatted@^3.2.9: + version "3.3.1" + resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.3.1.tgz#21db470729a6734d4997002f439cb308987f567a" + integrity sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw== + +fs-extra@^10.0.0: + version "10.1.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-10.1.0.tgz#02873cfbc4084dde127eaa5f9905eef2325d1abf" + integrity sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ== + dependencies: + graceful-fs "^4.2.0" + jsonfile "^6.0.1" + universalify "^2.0.0" + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== + +fsevents@~2.3.2: + version "2.3.3" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" + integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== + +get-caller-file@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" + integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== + +get-func-name@^2.0.1, get-func-name@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/get-func-name/-/get-func-name-2.0.2.tgz#0d7cf20cd13fda808669ffa88f4ffc7a3943fc41" + integrity sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ== + +glob-parent@^5.1.2, glob-parent@~5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" + integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== + dependencies: + is-glob "^4.0.1" + +glob-parent@^6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-6.0.2.tgz#6d237d99083950c79290f24c7642a3de9a28f9e3" + integrity sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A== + dependencies: + is-glob "^4.0.3" + +glob@8.1.0: + version "8.1.0" + resolved "https://registry.yarnpkg.com/glob/-/glob-8.1.0.tgz#d388f656593ef708ee3e34640fdfb99a9fd1c33e" + integrity sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^5.0.1" + once "^1.3.0" + +glob@^7.1.3: + version "7.2.3" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" + integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.1.1" + once "^1.3.0" + path-is-absolute "^1.0.0" + +globals@^13.19.0: + version "13.24.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-13.24.0.tgz#8432a19d78ce0c1e833949c36adb345400bb1171" + integrity sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ== + dependencies: + type-fest "^0.20.2" + +globby@^11.1.0: + version "11.1.0" + resolved "https://registry.yarnpkg.com/globby/-/globby-11.1.0.tgz#bd4be98bb042f83d796f7e3811991fbe82a0d34b" + integrity sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g== + dependencies: + array-union "^2.1.0" + dir-glob "^3.0.1" + fast-glob "^3.2.9" + ignore "^5.2.0" + merge2 "^1.4.1" + slash "^3.0.0" + +graceful-fs@^4.1.6, graceful-fs@^4.2.0: + version "4.2.11" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" + integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== + +graphemer@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/graphemer/-/graphemer-1.4.0.tgz#fb2f1d55e0e3a1849aeffc90c4fa0dd53a0e66c6" + integrity sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag== + +has-flag@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" + integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== + +he@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" + integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== + +ignore@^5.2.0: + version "5.3.1" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.1.tgz#5073e554cd42c5b33b394375f538b8593e34d4ef" + integrity sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw== + +import-fresh@^3.2.1: + version "3.3.0" + resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b" + integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw== + dependencies: + parent-module "^1.0.0" + resolve-from "^4.0.0" + +imurmurhash@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" + integrity sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA== + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA== + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@2: + version "2.0.4" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + +is-binary-path@~2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" + integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== + dependencies: + binary-extensions "^2.0.0" + +is-extglob@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" + integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== + +is-fullwidth-code-point@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" + integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== + +is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1: + version "4.0.3" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" + integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== + dependencies: + is-extglob "^2.1.1" + +is-number@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" + integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== + +is-path-inside@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283" + integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ== + +is-plain-obj@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-2.1.0.tgz#45e42e37fccf1f40da8e5f76ee21515840c09287" + integrity sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA== + +is-unicode-supported@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz#3f26c76a809593b52bfa2ecb5710ed2779b522a7" + integrity sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw== + +isexe@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" + integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== + +isomorphic-fetch@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/isomorphic-fetch/-/isomorphic-fetch-3.0.0.tgz#0267b005049046d2421207215d45d6a262b8b8b4" + integrity sha512-qvUtwJ3j6qwsF3jLxkZ72qCgjMysPzDfeV240JHiGZsANBYd+EEuu35v7dfrJ9Up0Ak07D7GGSkGhCHTqg/5wA== + dependencies: + node-fetch "^2.6.1" + whatwg-fetch "^3.4.1" + +js-yaml@4.1.0, js-yaml@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" + integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== + dependencies: + argparse "^2.0.1" + +json-buffer@3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.1.tgz#9338802a30d3b6605fbe0613e094008ca8c05a13" + integrity sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ== + +json-schema-traverse@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" + integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== + +json-stable-stringify-without-jsonify@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" + integrity sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw== + +jsonfile@^6.0.1: + version "6.1.0" + resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.1.0.tgz#bc55b2634793c679ec6403094eb13698a6ec0aae" + integrity sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ== + dependencies: + universalify "^2.0.0" + optionalDependencies: + graceful-fs "^4.1.6" + +keyv@^4.5.3: + version "4.5.4" + resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.5.4.tgz#a879a99e29452f942439f2a405e3af8b31d4de93" + integrity sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw== + dependencies: + json-buffer "3.0.1" + +levn@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/levn/-/levn-0.4.1.tgz#ae4562c007473b932a6200d403268dd2fffc6ade" + integrity sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ== + dependencies: + prelude-ls "^1.2.1" + type-check "~0.4.0" + +locate-path@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-6.0.0.tgz#55321eb309febbc59c4801d931a72452a681d286" + integrity sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw== + dependencies: + p-locate "^5.0.0" + +lodash-es@^4.17.21: + version "4.17.21" + resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.21.tgz#43e626c46e6591b7750beb2b50117390c609e3ee" + integrity sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw== + +lodash.merge@^4.6.2: + version "4.6.2" + resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" + integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== + +log-symbols@4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-4.1.0.tgz#3fbdbb95b4683ac9fc785111e792e558d4abd503" + integrity sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg== + dependencies: + chalk "^4.1.0" + is-unicode-supported "^0.1.0" + +lossless-json@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/lossless-json/-/lossless-json-4.0.1.tgz#d45229e3abb213a0235812780ca894ea8c5b2c6b" + integrity sha512-l0L+ppmgPDnb+JGxNLndPtJZGNf6+ZmVaQzoxQm3u6TXmhdnsA+YtdVR8DjzZd/em58686CQhOFDPewfJ4l7MA== + +loupe@^2.3.6: + version "2.3.7" + resolved "https://registry.yarnpkg.com/loupe/-/loupe-2.3.7.tgz#6e69b7d4db7d3ab436328013d37d1c8c3540c697" + integrity sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA== + dependencies: + get-func-name "^2.0.1" + +make-error@^1.1.1: + version "1.3.6" + resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2" + integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw== + +merge2@^1.3.0, merge2@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" + integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== + +micromatch@^4.0.4: + version "4.0.7" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.7.tgz#33e8190d9fe474a9895525f5618eee136d46c2e5" + integrity sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q== + dependencies: + braces "^3.0.3" + picomatch "^2.3.1" + +minimatch@5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.0.1.tgz#fb9022f7528125187c92bd9e9b6366be1cf3415b" + integrity sha512-nLDxIFRyhDblz3qMuq+SoRZED4+miJ/G+tdDrjkkkRnjAsBexeGpgjLEQ0blJy7rHhR2b93rhQY4SvyWu9v03g== + dependencies: + brace-expansion "^2.0.1" + +minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" + integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== + dependencies: + brace-expansion "^1.1.7" + +minimatch@^5.0.1: + version "5.1.6" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.1.6.tgz#1cfcb8cf5522ea69952cd2af95ae09477f122a96" + integrity sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g== + dependencies: + brace-expansion "^2.0.1" + +mocha@^10.2.0: + version "10.4.0" + resolved "https://registry.yarnpkg.com/mocha/-/mocha-10.4.0.tgz#ed03db96ee9cfc6d20c56f8e2af07b961dbae261" + integrity sha512-eqhGB8JKapEYcC4ytX/xrzKforgEc3j1pGlAXVy3eRwrtAy5/nIfT1SvgGzfN0XZZxeLq0aQWkOUAmqIJiv+bA== + dependencies: + ansi-colors "4.1.1" + browser-stdout "1.3.1" + chokidar "3.5.3" + debug "4.3.4" + diff "5.0.0" + escape-string-regexp "4.0.0" + find-up "5.0.0" + glob "8.1.0" + he "1.2.0" + js-yaml "4.1.0" + log-symbols "4.1.0" + minimatch "5.0.1" + ms "2.1.3" + serialize-javascript "6.0.0" + strip-json-comments "3.1.1" + supports-color "8.1.1" + workerpool "6.2.1" + yargs "16.2.0" + yargs-parser "20.2.4" + yargs-unparser "2.0.0" + +ms@2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" + integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== + +ms@2.1.3: + version "2.1.3" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" + integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== + +natural-compare-lite@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz#17b09581988979fddafe0201e931ba933c96cbb4" + integrity sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g== + +natural-compare@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" + integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw== + +node-fetch@^2.6.1: + version "2.7.0" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.7.0.tgz#d0f0fa6e3e2dc1d27efcd8ad99d550bda94d187d" + integrity sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A== + dependencies: + whatwg-url "^5.0.0" + +normalize-path@^3.0.0, normalize-path@~3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" + integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== + +once@^1.3.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== + dependencies: + wrappy "1" + +optionator@^0.9.3: + version "0.9.4" + resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.4.tgz#7ea1c1a5d91d764fb282139c88fe11e182a3a734" + integrity sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g== + dependencies: + deep-is "^0.1.3" + fast-levenshtein "^2.0.6" + levn "^0.4.1" + prelude-ls "^1.2.1" + type-check "^0.4.0" + word-wrap "^1.2.5" + +p-limit@^3.0.2: + version "3.1.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b" + integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== + dependencies: + yocto-queue "^0.1.0" + +p-locate@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-5.0.0.tgz#83c8315c6785005e3bd021839411c9e110e6d834" + integrity sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw== + dependencies: + p-limit "^3.0.2" + +pako@^2.0.4: + version "2.1.0" + resolved "https://registry.yarnpkg.com/pako/-/pako-2.1.0.tgz#266cc37f98c7d883545d11335c00fbd4062c9a86" + integrity sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug== + +parent-module@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2" + integrity sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g== + dependencies: + callsites "^3.0.0" + +path-exists@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" + integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== + +path-is-absolute@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg== + +path-key@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" + integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== + +path-type@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" + integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== + +pathval@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/pathval/-/pathval-1.1.1.tgz#8534e77a77ce7ac5a2512ea21e0fdb8fcf6c3d8d" + integrity sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ== + +picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" + integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== + +prelude-ls@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" + integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== + +prettier-plugin-organize-imports@^3.2.2: + version "3.2.4" + resolved "https://registry.yarnpkg.com/prettier-plugin-organize-imports/-/prettier-plugin-organize-imports-3.2.4.tgz#77967f69d335e9c8e6e5d224074609309c62845e" + integrity sha512-6m8WBhIp0dfwu0SkgfOxJqh+HpdyfqSSLfKKRZSFbDuEQXDDndb8fTpRWkUrX/uBenkex3MgnVk0J3b3Y5byog== + +prettier@^3.0.0: + version "3.2.5" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.2.5.tgz#e52bc3090586e824964a8813b09aba6233b28368" + integrity sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A== + +psl@^1.1.33: + version "1.9.0" + resolved "https://registry.yarnpkg.com/psl/-/psl-1.9.0.tgz#d0df2a137f00794565fcaf3b2c00cd09f8d5a5a7" + integrity sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag== + +punycode@^2.1.0, punycode@^2.1.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5" + integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg== + +querystringify@^2.1.1: + version "2.2.0" + resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.2.0.tgz#3345941b4153cb9d082d8eee4cda2016a9aef7f6" + integrity sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ== + +queue-microtask@^1.2.2: + version "1.2.3" + resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" + integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== + +randombytes@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" + integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ== + dependencies: + safe-buffer "^5.1.0" + +readdirp@~3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7" + integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA== + dependencies: + picomatch "^2.2.1" + +redeyed@~2.1.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/redeyed/-/redeyed-2.1.1.tgz#8984b5815d99cb220469c99eeeffe38913e6cc0b" + integrity sha512-FNpGGo1DycYAdnrKFxCMmKYgo/mILAqtRYbkdQD8Ep/Hk2PQ5+aEAEx+IU713RTDmuBaH0c8P5ZozurNu5ObRQ== + dependencies: + esprima "~4.0.0" + +require-directory@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" + integrity sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q== + +requires-port@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" + integrity sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ== + +resolve-from@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" + integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== + +reusify@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" + integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== + +rimraf@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" + integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== + dependencies: + glob "^7.1.3" + +run-parallel@^1.1.9: + version "1.2.0" + resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee" + integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA== + dependencies: + queue-microtask "^1.2.2" + +safe-buffer@^5.1.0: + version "5.2.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== + +semver@^7.3.7: + version "7.6.2" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.2.tgz#1e3b34759f896e8f14d6134732ce798aeb0c6e13" + integrity sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w== + +serialize-javascript@6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.0.tgz#efae5d88f45d7924141da8b5c3a7a7e663fefeb8" + integrity sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag== + dependencies: + randombytes "^2.1.0" + +set-cookie-parser@^2.4.8: + version "2.6.0" + resolved "https://registry.yarnpkg.com/set-cookie-parser/-/set-cookie-parser-2.6.0.tgz#131921e50f62ff1a66a461d7d62d7b21d5d15a51" + integrity sha512-RVnVQxTXuerk653XfuliOxBP81Sf0+qfQE73LIYKcyMYHG94AuH0kgrQpRDuTZnSmjpysHmzxJXKNfa6PjFhyQ== + +shebang-command@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" + integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== + dependencies: + shebang-regex "^3.0.0" + +shebang-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" + integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== + +slash@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" + integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== + +starknet@6.5.0: + version "6.5.0" + resolved "https://registry.yarnpkg.com/starknet/-/starknet-6.5.0.tgz#1b984dcf6e4f1960a64d83a84391e98b9926b345" + integrity sha512-3W7cpMPE6u1TAjZoT1gfqAtTpSTkAFXwwVbt9IG3oyk8gxBwzpadcMXZ5JRBOv9p06qfnivRkWl2Q1B4tIrSAg== + dependencies: + "@noble/curves" "~1.3.0" + "@scure/base" "~1.1.3" + "@scure/starknet" "~1.0.0" + abi-wan-kanabi "^2.2.1" + fetch-cookie "^3.0.0" + isomorphic-fetch "^3.0.0" + lossless-json "^4.0.1" + pako "^2.0.4" + ts-mixer "^6.0.3" + url-join "^4.0.1" + +string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +strip-ansi@^6.0.0, strip-ansi@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +strip-json-comments@3.1.1, strip-json-comments@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" + integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== + +supports-color@8.1.1: + version "8.1.1" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c" + integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q== + dependencies: + has-flag "^4.0.0" + +supports-color@^7.1.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" + integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== + dependencies: + has-flag "^4.0.0" + +text-table@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" + integrity sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw== + +to-regex-range@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" + integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== + dependencies: + is-number "^7.0.0" + +tough-cookie@^4.0.0: + version "4.1.4" + resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-4.1.4.tgz#945f1461b45b5a8c76821c33ea49c3ac192c1b36" + integrity sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag== + dependencies: + psl "^1.1.33" + punycode "^2.1.1" + universalify "^0.2.0" + url-parse "^1.5.3" + +tr46@~0.0.3: + version "0.0.3" + resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" + integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw== + +ts-mixer@^6.0.3: + version "6.0.4" + resolved "https://registry.yarnpkg.com/ts-mixer/-/ts-mixer-6.0.4.tgz#1da39ceabc09d947a82140d9f09db0f84919ca28" + integrity sha512-ufKpbmrugz5Aou4wcr5Wc1UUFWOLhq+Fm6qa6P0w0K5Qw2yhaUoiWszhCVuNQyNwrlGiscHOmqYoAox1PtvgjA== + +ts-node@^10.9.1: + version "10.9.2" + resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.9.2.tgz#70f021c9e185bccdca820e26dc413805c101c71f" + integrity sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ== + dependencies: + "@cspotcode/source-map-support" "^0.8.0" + "@tsconfig/node10" "^1.0.7" + "@tsconfig/node12" "^1.0.7" + "@tsconfig/node14" "^1.0.0" + "@tsconfig/node16" "^1.0.2" + acorn "^8.4.1" + acorn-walk "^8.1.1" + arg "^4.1.0" + create-require "^1.1.0" + diff "^4.0.1" + make-error "^1.1.1" + v8-compile-cache-lib "^3.0.1" + yn "3.1.1" + +tslib@2.4.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.4.0.tgz#7cecaa7f073ce680a05847aa77be941098f36dc3" + integrity sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ== + +tslib@^1.8.1: + version "1.14.1" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" + integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== + +tsutils@^3.21.0: + version "3.21.0" + resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.21.0.tgz#b48717d394cea6c1e096983eed58e9d61715b623" + integrity sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA== + dependencies: + tslib "^1.8.1" + +type-check@^0.4.0, type-check@~0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1" + integrity sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew== + dependencies: + prelude-ls "^1.2.1" + +type-detect@^4.0.0, type-detect@^4.0.8: + version "4.0.8" + resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c" + integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g== + +type-fest@^0.20.2: + version "0.20.2" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4" + integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ== + +typescript@^5.4.3: + version "5.4.5" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.4.5.tgz#42ccef2c571fdbd0f6718b1d1f5e6e5ef006f611" + integrity sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ== + +undici-types@~5.26.4: + version "5.26.5" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" + integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== + +universalify@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.2.0.tgz#6451760566fa857534745ab1dde952d1b1761be0" + integrity sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg== + +universalify@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.1.tgz#168efc2180964e6386d061e094df61afe239b18d" + integrity sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw== + +uri-js@^4.2.2: + version "4.4.1" + resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e" + integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg== + dependencies: + punycode "^2.1.0" + +url-join@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/url-join/-/url-join-4.0.1.tgz#b642e21a2646808ffa178c4c5fda39844e12cde7" + integrity sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA== + +url-parse@^1.5.3: + version "1.5.10" + resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.5.10.tgz#9d3c2f736c1d75dd3bd2be507dcc111f1e2ea9c1" + integrity sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ== + dependencies: + querystringify "^2.1.1" + requires-port "^1.0.0" + +v8-compile-cache-lib@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz#6336e8d71965cb3d35a1bbb7868445a7c05264bf" + integrity sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg== + +webidl-conversions@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" + integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ== + +whatwg-fetch@^3.4.1: + version "3.6.20" + resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-3.6.20.tgz#580ce6d791facec91d37c72890995a0b48d31c70" + integrity sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg== + +whatwg-url@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d" + integrity sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw== + dependencies: + tr46 "~0.0.3" + webidl-conversions "^3.0.0" + +which@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" + integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== + dependencies: + isexe "^2.0.0" + +word-wrap@^1.2.5: + version "1.2.5" + resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34" + integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA== + +workerpool@6.2.1: + version "6.2.1" + resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.2.1.tgz#46fc150c17d826b86a008e5a4508656777e9c343" + integrity sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw== + +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + +wrappy@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== + +ws@8.5.0: + version "8.5.0" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.5.0.tgz#bfb4be96600757fe5382de12c670dab984a1ed4f" + integrity sha512-BWX0SWVgLPzYwF8lTzEy1egjhS4S4OEAHfsO8o65WOVsrnSRGaSiUaa9e0ggGlkMTtBlmOpEXiie9RUcBO86qg== + +y18n@^5.0.5: + version "5.0.8" + resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55" + integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA== + +yargs-parser@20.2.4: + version "20.2.4" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.4.tgz#b42890f14566796f85ae8e3a25290d205f154a54" + integrity sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA== + +yargs-parser@^20.2.2: + version "20.2.9" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee" + integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w== + +yargs-parser@^21.1.1: + version "21.1.1" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35" + integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw== + +yargs-unparser@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/yargs-unparser/-/yargs-unparser-2.0.0.tgz#f131f9226911ae5d9ad38c432fe809366c2325eb" + integrity sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA== + dependencies: + camelcase "^6.0.0" + decamelize "^4.0.0" + flat "^5.0.2" + is-plain-obj "^2.1.0" + +yargs@16.2.0: + version "16.2.0" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-16.2.0.tgz#1c82bf0f6b6a66eafce7ef30e376f49a12477f66" + integrity sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw== + dependencies: + cliui "^7.0.2" + escalade "^3.1.1" + get-caller-file "^2.0.5" + require-directory "^2.1.1" + string-width "^4.2.0" + y18n "^5.0.5" + yargs-parser "^20.2.2" + +yargs@^17.7.2: + version "17.7.2" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.7.2.tgz#991df39aca675a192b816e1e0363f9d75d2aa269" + integrity sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w== + dependencies: + cliui "^8.0.1" + escalade "^3.1.1" + get-caller-file "^2.0.5" + require-directory "^2.1.1" + string-width "^4.2.3" + y18n "^5.0.5" + yargs-parser "^21.1.1" + +yn@3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50" + integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q== + +yocto-queue@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" + integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==