From 6c0056d535e080474c7f28036a6fd458741b139e Mon Sep 17 00:00:00 2001 From: Sergio Garcia Date: Wed, 5 Jun 2024 15:21:22 +0100 Subject: [PATCH 01/29] initial commit --- .env.example | 3 + .eslintrc.json | 12 + .github/workflows/cairo-ci.yml | 26 + .github/workflows/integration-ci.yml | 46 + .gitignore | 45 + .mocharc.json | 7 + .nvmrc | 1 + .prettierignore | 8 + .prettierrc | 9 + .tool-versions | 2 + Dockerfile | 8 + README.md | 53 + Scarb.lock | 54 + Scarb.toml | 27 + deployments.md | 4 + gas-report.txt | 18 + lib/accounts.ts | 395 +++++ lib/claim.ts | 37 + lib/contracts.ts | 107 ++ lib/devnet.ts | 51 + lib/expectations.ts | 110 ++ lib/gas.ts | 236 +++ lib/index.ts | 26 + lib/manager.ts | 20 + lib/multisig.ts | 126 ++ lib/openZeppelinAccount.ts | 60 + lib/outsideExecution.ts | 155 ++ lib/receipts.ts | 21 + lib/recovery.ts | 62 + lib/signers/cairo0-sha256.patch | 31 + lib/signers/legacy.ts | 87 ++ lib/signers/secp256.ts | 229 +++ lib/signers/signers.ts | 302 ++++ lib/signers/webauthn.ts | 160 ++ lib/tokens.ts | 49 + lib/udc.ts | 19 + lib/upgrade.ts | 12 + package-lock.json | 677 +++++++++ package.json | 32 + scripts/declare.js | 22 + scripts/double-transfer.js | 69 + scripts/monitor-nonce.js | 17 + scripts/profile.ts | 61 + src/contracts/claim_account.cairo | 102 ++ src/contracts/claim_hash.cairo | 63 + src/contracts/claim_utils.cairo | 20 + src/contracts/gift_factory.cairo | 242 +++ src/contracts/interface.cairo | 57 + src/contracts/utils.cairo | 82 ++ src/lib.cairo | 12 + src/mocks/erc20.cairo | 120 ++ tests-integration/account.test.ts | 210 +++ tests-integration/claim_external.test.ts | 50 + tests-integration/factory.test.ts | 202 +++ tests/constants.cairo | 25 + tests/lib.cairo | 3 + tests/setup.cairo | 105 ++ tests/test_gift_factory.cairo | 108 ++ tsconfig.json | 16 + yarn.lock | 1700 ++++++++++++++++++++++ 60 files changed, 6613 insertions(+) create mode 100644 .env.example create mode 100644 .eslintrc.json create mode 100644 .github/workflows/cairo-ci.yml create mode 100644 .github/workflows/integration-ci.yml create mode 100644 .gitignore create mode 100644 .mocharc.json create mode 100644 .nvmrc create mode 100644 .prettierignore create mode 100644 .prettierrc create mode 100644 .tool-versions create mode 100644 Dockerfile create mode 100644 README.md create mode 100644 Scarb.lock create mode 100644 Scarb.toml create mode 100644 deployments.md create mode 100644 gas-report.txt create mode 100644 lib/accounts.ts create mode 100644 lib/claim.ts create mode 100644 lib/contracts.ts create mode 100644 lib/devnet.ts create mode 100644 lib/expectations.ts create mode 100644 lib/gas.ts create mode 100644 lib/index.ts create mode 100644 lib/manager.ts create mode 100644 lib/multisig.ts create mode 100644 lib/openZeppelinAccount.ts create mode 100644 lib/outsideExecution.ts create mode 100644 lib/receipts.ts create mode 100644 lib/recovery.ts create mode 100644 lib/signers/cairo0-sha256.patch create mode 100644 lib/signers/legacy.ts create mode 100644 lib/signers/secp256.ts create mode 100644 lib/signers/signers.ts create mode 100644 lib/signers/webauthn.ts create mode 100644 lib/tokens.ts create mode 100644 lib/udc.ts create mode 100644 lib/upgrade.ts create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 scripts/declare.js create mode 100644 scripts/double-transfer.js create mode 100644 scripts/monitor-nonce.js create mode 100644 scripts/profile.ts create mode 100644 src/contracts/claim_account.cairo create mode 100644 src/contracts/claim_hash.cairo create mode 100644 src/contracts/claim_utils.cairo create mode 100644 src/contracts/gift_factory.cairo create mode 100644 src/contracts/interface.cairo create mode 100644 src/contracts/utils.cairo create mode 100644 src/lib.cairo create mode 100644 src/mocks/erc20.cairo create mode 100644 tests-integration/account.test.ts create mode 100644 tests-integration/claim_external.test.ts create mode 100644 tests-integration/factory.test.ts create mode 100644 tests/constants.cairo create mode 100644 tests/lib.cairo create mode 100644 tests/setup.cairo create mode 100644 tests/test_gift_factory.cairo create mode 100644 tsconfig.json create mode 100644 yarn.lock 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..ef623b9 --- /dev/null +++ b/.github/workflows/integration-ci.yml @@ -0,0 +1,46 @@ +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 . diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a19dc79 --- /dev/null +++ b/.gitignore @@ -0,0 +1,45 @@ +# 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 \ No newline at end of file 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..3487c75 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,8 @@ +# Use the base image +FROM shardlabs/starknet-devnet-rs:c4185522228f61ba04619151eb5706d4610fb00f + +# Expose port 5050 +EXPOSE 5050 + +# Set default command to run the container +CMD ["--gas-price", "36000000000", "--data-gas-price", "1", "--timeout", "320", "--seed", "0"] \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..62eb50a --- /dev/null +++ b/README.md @@ -0,0 +1,53 @@ +// TODO This is outdated + +# Starknet Gifting + +The protocol implemented in this repository can be used for gifting tokens to a recipient without giving custody of the tokens to a third party. But it can also be used to transfer tokens to a repicient identified by an email or a phone number. Both products are supported by the same smart-contract and depends solely on the client being built on top. + +## High level Flow + +- The sender creates a key pair `claim_key` locally and deposits the tokens to transfer to the escrow account together with a small amount of fee token (ETH or STRK) to cover the claim. He provides the public key `claim_key.pub` as an identifier for the transfer. +- The sender shares the private key `claim_key.priv` with the recipient over an external channel such as email or phone. +- The recipient claims the tokens by transferring them from the escrow to an account he controls using `claim_key.priv` to sign the transaction. + +## Fee + +Because the escrow is an account, it can pay for its own transactions and the recipient doesn't need to have funds to initiate the claim. This makes it great to onboard new users that can claim a gift to a newly created and undeployed account. + +The Escrow contract can operate in 2 modes depending on the value of the `use_fee` flag. + +When `use_fee = false` the sender doesn't need to cover the fee for the claim because the operator of the escrow sponsors the transfers by depositing sufficient ETH or STRK on the escrow contract. + +When `use_fee = true` the fee of the claim must be covered by the sender, and he is required to deposit some ETH or STRK together with the token being gifted. The amount of fee token he provides must be sufficient to cover the claim. The recipient of the claim can use up to that value as specified in the max fee of the claim transaction. + +If the max fee of the claim transaction is less than the max fee allocated to the claim, the difference is added to a counter and can be later retrieved by the operator of the escrow contract as a paiement for his operation of the protocol. However, the difference between the max fee of the claim transaction and the actual fee of the transaction cannot be acounted for in the contract and will be stuck. We can imagine using that dust later by setting `use_fee = true` and sponsoring gifts for a limited period. + +## Canceling Gifts + +Gifts can be canceled by the sender provided that they have not been claimed yet. If the gift covers the claim fee the sender can recover both the gift and the claim fee he provided. + +In the unlikely event that the recipient tried to claim a gift but the transaction failed in execution, some of the claim fee will have been used. The gift can no longer be claimed but can be canceled by the sender. Canceling the gift will only recover the gift but not the remaining claim fee. + +## Development + +### asdf + +Install asdf following [instructions](https://asdf-vm.com/guide/getting-started.html) and run this + +``` +asdf plugin add scarb +asdf plugin add starknet-foundry +asdf install +``` + +### Setup scarb and foundry + +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. + +### Build the contracts + +`scarb build` + +### Test the contracts + +`snforge test` diff --git a/Scarb.lock b/Scarb.lock new file mode 100644 index 0000000..8272570 --- /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 = "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" + +[[package]] +name = "starknet_gifting" +version = "0.1.0" +dependencies = [ + "alexandria_math", + "openzeppelin", + "snforge_std", +] diff --git a/Scarb.toml b/Scarb.toml new file mode 100644 index 0000000..cfb6cac --- /dev/null +++ b/Scarb.toml @@ -0,0 +1,27 @@ +[package] +name = "starknet_gifting" +version = "0.1.0" +edition = "2023_10" + + +[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 ." \ No newline at end of file diff --git a/deployments.md b/deployments.md new file mode 100644 index 0000000..7712604 --- /dev/null +++ b/deployments.md @@ -0,0 +1,4 @@ +## Sepolia + +classhash: 0x5908a9e0785fe551d52413f2289c5c1436a8a7086d569829d0ff10443bf790 +Escrow: 0x1748e6a718e66dd6602be9424fc206988cf8ff807d0a5922bbc2ad4253a2a2f diff --git a/gas-report.txt b/gas-report.txt new file mode 100644 index 0000000..5a9b9c6 --- /dev/null +++ b/gas-report.txt @@ -0,0 +1,18 @@ +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 │ +├───────────────────────┼─────────────────────┼─────────┼────────────────┼────────────────┼─────────────────┼───────────┼──────────────┼──────────────────────────────┼───────────────┼────────┼─────────┤ +│ Deposit (txV3: false) │ '1.152.000.000.352' │ 0.0046 │ 1152000000000 │ 32 │ 31 │ 4 │ 1 │ 'steps' │ 4 │ 352 │ 'BLOB' │ +│ Claim (txV3: false) │ '1.260.000.000.192' │ 0.005 │ 1260000000000 │ 35 │ 34 │ 2 │ 1 │ 'steps' │ 3 │ 192 │ 'BLOB' │ +│ Deposit (txV3: true) │ '1.152.000.000.480' │ 0.0046 │ 1152000000000 │ 32 │ 31 │ 4 │ 1 │ 'steps' │ 5 │ 480 │ 'BLOB' │ +│ Claim (txV3: true) │ '1.260.000.000.192' │ 0 │ 1260000000000 │ 35 │ 34 │ 2 │ 1 │ 'steps' │ 3 │ 192 │ 'BLOB' │ +└───────────────────────┴─────────────────────┴─────────┴────────────────┴────────────────┴─────────────────┴───────────┴──────────────┴──────────────────────────────┴───────────────┴────────┴─────────┘ +Resources: +┌───────────────────────┬─────────┬───────┬───────┬────────┬──────────┬──────────┬─────────────┬───────┐ +│ (index) │ bitwise │ ec_op │ ecdsa │ keccak │ pedersen │ poseidon │ range_check │ steps │ +├───────────────────────┼─────────┼───────┼───────┼────────┼──────────┼──────────┼─────────────┼───────┤ +│ Deposit (txV3: false) │ 0 │ 3 │ 0 │ 0 │ 38 │ 0 │ 277 │ 12098 │ +│ Claim (txV3: false) │ 0 │ 3 │ 0 │ 0 │ 71 │ 0 │ 456 │ 13231 │ +│ Deposit (txV3: true) │ 0 │ 3 │ 0 │ 0 │ 38 │ 0 │ 277 │ 12099 │ +│ Claim (txV3: true) │ 0 │ 3 │ 0 │ 0 │ 71 │ 0 │ 490 │ 13429 │ +└───────────────────────┴─────────┴───────┴───────┴────────┴──────────┴──────────┴─────────────┴───────┘ diff --git a/lib/accounts.ts b/lib/accounts.ts new file mode 100644 index 0000000..e9858d6 --- /dev/null +++ b/lib/accounts.ts @@ -0,0 +1,395 @@ +import { + Abi, + Account, + AllowArray, + ArraySignatureType, + CairoOption, + CairoOptionVariant, + Call, + CallData, + Contract, + DeployAccountContractPayload, + DeployContractResponse, + GetTransactionReceiptResponse, + InvocationsSignerDetails, + InvokeFunctionResponse, + RPC, + RawCalldata, + Signature, + UniversalDetails, + V2InvocationsSignerDetails, + V3InvocationsSignerDetails, + hash, + num, + shortString, + stark, + transaction, + uint256, +} from "starknet"; +import { manager } from "./manager"; +import { ensureSuccess } from "./receipts"; +import { LegacyArgentSigner, LegacyKeyPair, LegacyMultisigSigner, LegacyStarknetKeyPair } from "./signers/legacy"; +import { ArgentSigner, KeyPair, RawSigner, randomStarknetKeyPair } from "./signers/signers"; +import { ethAddress, strkAddress } from "./tokens"; + +export const VALID = BigInt(shortString.encodeShortString("VALID")); + +export class ArgentAccount extends Account { + // Increase the gas limit by 30% to avoid failures due to gas estimation being too low with tx v3 and transactions the use escaping + override async deployAccount( + payload: DeployAccountContractPayload, + details?: UniversalDetails, + ): Promise { + details ||= {}; + if (!details.skipValidate) { + details.skipValidate = false; + } + return super.deployAccount(payload, details); + } + + override async execute( + calls: AllowArray, + abis?: Abi[], + details: UniversalDetails = {}, + ): Promise { + details ||= {}; + if (!details.skipValidate) { + details.skipValidate = false; + } + if (details.resourceBounds) { + return super.execute(calls, abis, details); + } + const estimate = await this.estimateFee(calls, details); + return super.execute(calls, abis, { + ...details, + resourceBounds: { + ...estimate.resourceBounds, + l1_gas: { + ...estimate.resourceBounds.l1_gas, + max_amount: num.toHexString(num.addPercent(estimate.resourceBounds.l1_gas.max_amount, 30)), + }, + }, + }); + } +} + +export interface ArgentWallet { + account: ArgentAccount; + accountContract: Contract; + owner: KeyPair; +} + +export interface ArgentWalletWithGuardian extends ArgentWallet { + guardian: KeyPair; +} + +export interface LegacyArgentWallet { + account: ArgentAccount; + accountContract: Contract; + owner: LegacyKeyPair; + guardian: LegacyKeyPair; +} + +export interface ArgentWalletWithGuardianAndBackup extends ArgentWalletWithGuardian { + guardianBackup: KeyPair; +} + +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 const genericAccount = (() => { + if (manager.isDevnet) { + const devnetAddress = "0x78662e7352d062084b0010068b99288486c2d8b914f6e2a55ce945f8792c8b1"; + const devnetPrivateKey = "0xe1406455b7d66b1690803be066cbe5e"; + return new Account(manager, devnetAddress, devnetPrivateKey, undefined, RPC.ETransactionVersion.V2); + } + throw new Error("Only works in devnet."); +})(); + +export const deployerV3 = setDefaultTransactionVersionV3(deployer); + +export function setDefaultTransactionVersion(account: ArgentAccount, newVersion: boolean): Account { + const newDefaultVersion = newVersion ? RPC.ETransactionVersion.V3 : RPC.ETransactionVersion.V2; + if (account.transactionVersion === newDefaultVersion) { + return account; + } + return new ArgentAccount(account, account.address, account.signer, account.cairoVersion, newDefaultVersion); +} + +export function setDefaultTransactionVersionV3(account: ArgentAccount): ArgentAccount { + return setDefaultTransactionVersion(account, true); +} + +console.log("Deployer:", deployer.address); + +export async function deployOldAccount( + owner = new LegacyStarknetKeyPair(), + guardian = new LegacyStarknetKeyPair(), + salt = num.toHex(randomStarknetKeyPair().privateKey), +): Promise { + const proxyClassHash = await manager.declareFixtureContract("Proxy"); + const oldArgentAccountClassHash = await manager.declareFixtureContract("OldArgentAccount"); + + const constructorCalldata = CallData.compile({ + implementation: oldArgentAccountClassHash, + selector: hash.getSelectorFromName("initialize"), + calldata: CallData.compile({ owner: owner.publicKey, guardian: guardian.publicKey }), + }); + + const contractAddress = hash.calculateContractAddressFromHash(salt, proxyClassHash, constructorCalldata, 0); + + const account = new Account(manager, contractAddress, owner); + account.signer = new LegacyMultisigSigner([owner, guardian]); + + await fundAccount(account.address, 1e16, "ETH"); // 0.01 ETH + + const { transaction_hash } = await account.deployAccount({ + classHash: proxyClassHash, + constructorCalldata, + contractAddress, + addressSalt: salt, + }); + await manager.waitForTransaction(transaction_hash); + const accountContract = await manager.loadContract(account.address); + accountContract.connect(account); + return { account, accountContract, owner, guardian }; +} + +async function deployAccountInner(params: DeployAccountParams): Promise< + DeployAccountParams & { + account: Account; + classHash: string; + owner: KeyPair; + guardian?: KeyPair; + salt: string; + transactionHash: string; + } +> { + const finalParams = { + ...params, + classHash: params.classHash ?? (await manager.declareLocalContract("ArgentAccount")), + salt: params.salt ?? num.toHex(randomStarknetKeyPair().privateKey), + owner: params.owner ?? randomStarknetKeyPair(), + useTxV3: params.useTxV3 ?? false, + selfDeploy: params.selfDeploy ?? false, + }; + const guardian = finalParams.guardian + ? finalParams.guardian.signerAsOption + : new CairoOption(CairoOptionVariant.None); + const constructorCalldata = CallData.compile({ owner: finalParams.owner.signer, guardian }); + + const { classHash, salt } = finalParams; + const contractAddress = hash.calculateContractAddressFromHash(salt, classHash, constructorCalldata, 0); + const fundingCall = finalParams.useTxV3 + ? await fundAccountCall(contractAddress, finalParams.fundingAmount ?? 1e16, "STRK") // 0.01 STRK + : await fundAccountCall(contractAddress, finalParams.fundingAmount ?? 1e18, "ETH"); // 1 ETH + const calls = fundingCall ? [fundingCall] : []; + + const transactionVersion = finalParams.useTxV3 ? RPC.ETransactionVersion.V3 : RPC.ETransactionVersion.V2; + const signer = new ArgentSigner(finalParams.owner, finalParams.guardian); + const account = new ArgentAccount(manager, contractAddress, signer, "1", transactionVersion); + + let transactionHash; + if (finalParams.selfDeploy) { + const response = await deployer.execute(calls); + await manager.waitForTransaction(response.transaction_hash); + const { transaction_hash } = await account.deploySelf({ classHash, constructorCalldata, addressSalt: salt }); + transactionHash = transaction_hash; + } else { + const udcCalls = deployer.buildUDCContractPayload({ classHash, salt, constructorCalldata, unique: false }); + const { transaction_hash } = await deployer.execute([...calls, ...udcCalls]); + transactionHash = transaction_hash; + } + + await manager.waitForTransaction(transactionHash); + return { ...finalParams, account, transactionHash }; +} + +export type DeployAccountParams = { + useTxV3?: boolean; + classHash?: string; + owner?: KeyPair; + guardian?: KeyPair; + salt?: string; + fundingAmount?: number | bigint; + selfDeploy?: boolean; +}; + +export async function deployAccount( + params: DeployAccountParams = {}, +): Promise { + params.guardian ||= randomStarknetKeyPair(); + const { account, owner, transactionHash } = await deployAccountInner(params); + const accountContract = await manager.loadContract(account.address); + accountContract.connect(account); + return { account, accountContract, owner, guardian: params.guardian, transactionHash }; +} + +export async function deployAccountWithoutGuardian( + params: Omit = {}, +): Promise { + const { account, owner, transactionHash } = await deployAccountInner(params); + const accountContract = await manager.loadContract(account.address); + accountContract.connect(account); + return { account, accountContract, owner, transactionHash }; +} + +export async function deployAccountWithGuardianBackup( + params: DeployAccountParams & { guardianBackup?: KeyPair } = {}, +): Promise { + const guardianBackup = params.guardianBackup ?? randomStarknetKeyPair(); + + const wallet = (await deployAccount(params)) as ArgentWalletWithGuardianAndBackup & { transactionHash: string }; + await wallet.accountContract.change_guardian_backup(guardianBackup.compiledSignerAsOption); + + wallet.account.signer = new ArgentSigner(wallet.owner, guardianBackup); + wallet.guardianBackup = guardianBackup; + wallet.accountContract.connect(wallet.account); + return wallet; +} + +export async function deployLegacyAccount(classHash: string) { + const owner = new LegacyStarknetKeyPair(); + const guardian = new LegacyStarknetKeyPair(); + const salt = num.toHex(owner.privateKey); + const constructorCalldata = CallData.compile({ owner: owner.publicKey, guardian: guardian.publicKey }); + const contractAddress = hash.calculateContractAddressFromHash(salt, classHash, constructorCalldata, 0); + await fundAccount(contractAddress, 1e15, "ETH"); // 0.001 ETH + const account = new Account(manager, contractAddress, owner, "1"); + account.signer = new LegacyArgentSigner(owner, guardian); + + const { transaction_hash } = await account.deploySelf({ + classHash, + constructorCalldata, + addressSalt: salt, + }); + await manager.waitForTransaction(transaction_hash); + + const accountContract = await manager.loadContract(account.address); + accountContract.connect(account); + return { account, accountContract, owner, guardian }; +} + +export async function upgradeAccount( + accountToUpgrade: Account, + newClassHash: string, + calldata: RawCalldata = [], +): Promise { + const { transaction_hash } = await accountToUpgrade.execute( + { + contractAddress: accountToUpgrade.address, + entrypoint: "upgrade", + calldata: CallData.compile({ implementation: newClassHash, calldata }), + }, + undefined, + { maxFee: 1e14 }, + ); + return await ensureSuccess(await manager.waitForTransaction(transaction_hash)); +} + +export async function executeWithCustomSig( + account: ArgentAccount, + transactions: AllowArray, + signature: ArraySignatureType, + transactionsDetail: UniversalDetails = {}, +): Promise { + const signer = new (class extends RawSigner { + public async signRaw(messageHash: string): Promise { + return signature; + } + })(); + const newAccount = new ArgentAccount( + manager, + account.address, + signer, + account.cairoVersion, + account.transactionVersion, + ); + + return await newAccount.execute(transactions, undefined, transactionsDetail); +} + +export async function getSignerDetails(account: ArgentAccount, calls: Call[]): Promise { + const newAccount = new ArgentAccount( + manager, + account.address, + account.signer, + account.cairoVersion, + account.transactionVersion, + ); + const customSigner = new (class extends RawSigner { + public signerDetails?: InvocationsSignerDetails; + public async signTransaction(calls: Call[], signerDetails: InvocationsSignerDetails): Promise { + this.signerDetails = signerDetails; + throw Error("Should not execute"); + } + public async signRaw(messageHash: string): Promise { + throw Error("Not implemented"); + } + })(); + newAccount.signer = customSigner; + try { + await newAccount.execute(calls, undefined); + throw Error("Should not execute"); + } catch (customError) { + return customSigner.signerDetails!; + } +} + +export function calculateTransactionHash(transactionDetail: InvocationsSignerDetails, calls: Call[]): string { + const compiledCalldata = transaction.getExecuteCalldata(calls, transactionDetail.cairoVersion); + let transactionHash; + if (Object.values(RPC.ETransactionVersion2).includes(transactionDetail.version as any)) { + const transactionDetailV2 = transactionDetail as V2InvocationsSignerDetails; + transactionHash = hash.calculateInvokeTransactionHash({ + ...transactionDetailV2, + senderAddress: transactionDetailV2.walletAddress, + compiledCalldata, + }); + } else if (Object.values(RPC.ETransactionVersion3).includes(transactionDetail.version as any)) { + const transactionDetailV3 = transactionDetail as V3InvocationsSignerDetails; + transactionHash = hash.calculateInvokeTransactionHash({ + ...transactionDetailV3, + senderAddress: transactionDetailV3.walletAddress, + compiledCalldata, + nonceDataAvailabilityMode: stark.intDAM(transactionDetailV3.nonceDataAvailabilityMode), + feeDataAvailabilityMode: stark.intDAM(transactionDetailV3.feeDataAvailabilityMode), + }); + } else { + throw Error("unsupported transaction version"); + } + return transactionHash; +} + +export async function fundAccount(recipient: string, amount: number | bigint, token: "ETH" | "STRK") { + const call = await fundAccountCall(recipient, amount, token); + const response = await deployer.execute(call ? [call] : []); + await manager.waitForTransaction(response.transaction_hash); +} + +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..1f26b67 --- /dev/null +++ b/lib/claim.ts @@ -0,0 +1,37 @@ +import { shortString } from "starknet"; +import { manager } 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" }], +}; + +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: "GiftAccount.claim_external", + version: shortString.encodeShortString("1"), + chainId, + revision: "1", + }; +} + +export interface ClaimExternal { + receiver: string; +} + +export async function getClaimExternalData(claimExternal: ClaimExternal) { + const chainId = await manager.getChainId(); + return { + types: typesRev1, + primaryType: "ClaimExternal", + domain: getDomain(chainId), + message: { ...claimExternal }, + }; +} diff --git a/lib/contracts.ts b/lib/contracts.ts new file mode 100644 index 0000000..a79855e --- /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/starknet_gifting_"; +export const fixturesFolder = "./tests-integration/fixtures/starknet_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/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..ab50d20 --- /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) { + if (!e.toString().includes(shortString.encodeShortString(errorMessage))) { + const match = e.toString().match(/\[([^\]]+)]/); + if (match && match.length > 1) { + console.log(e); + assert.fail(`"${errorMessage}" not detected, instead got: "${shortString.decodeShortString(match[1])}"`); + } else { + assert.fail(`No error detected in: ${e.toString()}`); + } + } + return; + } + 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.additionalKeys ?? []), + 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; + additionalKeys?: 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..3116f82 --- /dev/null +++ b/lib/index.ts @@ -0,0 +1,26 @@ +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 "./devnet"; +export * from "./expectations"; +export * from "./manager"; +export * from "./multisig"; +export * from "./openZeppelinAccount"; +export * from "./outsideExecution"; +export * from "./receipts"; +export * from "./recovery"; +export * from "./signers/legacy"; +export * from "./signers/secp256"; +export * from "./signers/signers"; +export * from "./signers/webauthn"; +export * from "./tokens"; +export * from "./udc"; +export * from "./upgrade"; + +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/multisig.ts b/lib/multisig.ts new file mode 100644 index 0000000..5855bdc --- /dev/null +++ b/lib/multisig.ts @@ -0,0 +1,126 @@ +import { Account, CallData, Contract, GetTransactionReceiptResponse, RPC, hash, num } from "starknet"; +import { + ArgentAccount, + KeyPair, + LegacyMultisigSigner, + MultisigSigner, + deployer, + fundAccount, + fundAccountCall, + manager, + randomLegacyMultisigKeyPairs, + randomStarknetKeyPair, + randomStarknetKeyPairs, + sortByGuid, +} from "."; + +export interface MultisigWallet { + account: Account; + accountContract: Contract; + keys: KeyPair[]; + threshold: bigint; + receipt: GetTransactionReceiptResponse; +} + +export type DeployMultisigParams = { + threshold: number; + signersLength?: number; + keys?: KeyPair[]; + useTxV3?: boolean; + classHash?: string; + salt?: string; + fundingAmount?: number | bigint; + selfDeploy?: boolean; + selfDeploymentIndexes?: number[]; +}; + +export async function deployMultisig(params: DeployMultisigParams): Promise { + const finalParams = { + ...params, + classHash: params.classHash ?? (await manager.declareLocalContract("ArgentMultisigAccount")), + salt: params.salt ?? num.toHex(randomStarknetKeyPair().privateKey), + useTxV3: params.useTxV3 ?? false, + selfDeploy: params.selfDeploy ?? false, + selfDeploymentIndexes: params.selfDeploymentIndexes ?? [0], + }; + + if (params.selfDeploymentIndexes && !finalParams.selfDeploy) { + throw new Error("selfDeploymentIndexes can only be used with selfDeploy"); + } + + if (!params.keys && !finalParams.signersLength) { + throw new Error("Fill in one of 'keys' or 'signersLength'"); + } + const keys = params.keys ?? sortedKeyPairs(finalParams.signersLength!); + const signers = keysToSigners(keys); + const constructorCalldata = CallData.compile({ threshold: finalParams.threshold, signers }); + + const { classHash, salt, selfDeploymentIndexes } = finalParams; + const accountAddress = hash.calculateContractAddressFromHash(salt, classHash, constructorCalldata, 0); + + const fundingCall = finalParams.useTxV3 + ? await fundAccountCall(accountAddress, finalParams.fundingAmount ?? 1e16, "STRK") // 0.01 STRK + : await fundAccountCall(accountAddress, finalParams.fundingAmount ?? 1e15, "ETH"); // 0.001 ETH + const calls = fundingCall ? [fundingCall] : []; + + const transactionVersion = finalParams.useTxV3 ? RPC.ETransactionVersion.V3 : RPC.ETransactionVersion.V2; + + let transactionHash; + if (finalParams.selfDeploy) { + const response = await deployer.execute(calls); + await manager.waitForTransaction(response.transaction_hash); + + const selfDeploymentSigner = new MultisigSigner(keys.filter((_, i) => selfDeploymentIndexes.includes(i))); + const account = new Account(manager, accountAddress, selfDeploymentSigner, "1", transactionVersion); + + const { transaction_hash } = await account.deploySelf({ classHash, constructorCalldata, addressSalt: salt }); + transactionHash = transaction_hash; + } else { + const udcCalls = deployer.buildUDCContractPayload({ classHash, salt, constructorCalldata, unique: false }); + const { transaction_hash } = await deployer.execute([...calls, ...udcCalls]); + transactionHash = transaction_hash; + } + + const receipt = await manager.waitForTransaction(transactionHash); + const signer = new MultisigSigner(keys.slice(0, finalParams.threshold)); + const account = new ArgentAccount(manager, accountAddress, signer, "1", transactionVersion); + const accountContract = await manager.loadContract(account.address); + accountContract.connect(account); + return { account, accountContract, keys, receipt, threshold: BigInt(finalParams.threshold) }; +} + +export async function deployMultisig1_3( + params: Omit = {}, +): Promise { + return deployMultisig({ ...params, threshold: 1, signersLength: 3 }); +} + +export async function deployMultisig1_1( + params: Omit = {}, +): Promise { + return deployMultisig({ ...params, threshold: 1, signersLength: 1 }); +} + +const sortedKeyPairs = (length: number) => sortByGuid(randomStarknetKeyPairs(length)); + +const keysToSigners = (keys: KeyPair[]) => keys.map(({ signer }) => signer); + +export async function deployLegacyMultisig(classHash: string, threshold = 1) { + const keys = randomLegacyMultisigKeyPairs(threshold); + const signersPublicKeys = keys.map((key) => key.publicKey); + const salt = num.toHex(randomStarknetKeyPair().privateKey); + const constructorCalldata = CallData.compile({ threshold, signers: signersPublicKeys }); + const contractAddress = hash.calculateContractAddressFromHash(salt, classHash, constructorCalldata, 0); + await fundAccount(contractAddress, 1e15, "ETH"); // 0.001 ETH + const deploySigner = new LegacyMultisigSigner([keys[0]]); + const account = new Account(manager, contractAddress, deploySigner, "1"); + + const { transaction_hash } = await account.deploySelf({ classHash, constructorCalldata, addressSalt: salt }); + await manager.waitForTransaction(transaction_hash); + + const signers = new LegacyMultisigSigner(keys); + account.signer = signers; + const accountContract = await manager.loadContract(account.address); + accountContract.connect(account); + return { account, accountContract, deploySigner, signers }; +} diff --git a/lib/openZeppelinAccount.ts b/lib/openZeppelinAccount.ts new file mode 100644 index 0000000..b47df9c --- /dev/null +++ b/lib/openZeppelinAccount.ts @@ -0,0 +1,60 @@ +import { Account, CallData, RPC, hash, num } from "starknet"; +import { deployer, fundAccountCall } from "./accounts"; +import { ContractWithClass } from "./contracts"; +import { manager } from "./manager"; +import { LegacyMultisigSigner, LegacyStarknetKeyPair } from "./signers/legacy"; +import { randomStarknetKeyPair } from "./signers/signers"; + +export type DeployOzAccountParams = { + useTxV3?: boolean; + owner?: LegacyStarknetKeyPair; + salt?: string; + fundingAmount?: number | bigint; +}; + +export type DeployOzAccountResult = { + account: Account; + accountContract: ContractWithClass; + deployTxHash: string; + useTxV3: boolean; + owner: LegacyStarknetKeyPair; + salt: string; +}; + +export async function deployOpenZeppelinAccount(params: DeployOzAccountParams): Promise { + const classHash = await manager.declareLocalContract("AccountUpgradeable"); + const finalParams = { + ...params, + salt: params.salt ?? num.toHex(randomStarknetKeyPair().privateKey), + owner: params.owner ?? new LegacyStarknetKeyPair(), + useTxV3: params.useTxV3 ?? false, + }; + + const constructorCalldata = CallData.compile({ + owner: finalParams.owner.publicKey, + }); + + const contractAddress = hash.calculateContractAddressFromHash(finalParams.salt, classHash, constructorCalldata, 0); + + const fundingCall = finalParams.useTxV3 + ? await fundAccountCall(contractAddress, finalParams.fundingAmount ?? 1e18, "STRK") + : await fundAccountCall(contractAddress, finalParams.fundingAmount ?? 1e16, "ETH"); + const response = await deployer.execute([fundingCall!]); + await manager.waitForTransaction(response.transaction_hash); + + const defaultTxVersion = finalParams.useTxV3 ? RPC.ETransactionVersion.V3 : RPC.ETransactionVersion.V2; + const signer = new LegacyMultisigSigner([finalParams.owner]); + const account = new Account(manager, contractAddress, signer, "1", defaultTxVersion); + + const { transaction_hash: deployTxHash } = await account.deploySelf({ + classHash, + constructorCalldata, + addressSalt: finalParams.salt, + }); + + await manager.waitForTransaction(deployTxHash); + const accountContract = await manager.loadContract(account.address, classHash); + accountContract.connect(account); + + return { ...finalParams, account, accountContract, deployTxHash }; +} diff --git a/lib/outsideExecution.ts b/lib/outsideExecution.ts new file mode 100644 index 0000000..348c66a --- /dev/null +++ b/lib/outsideExecution.ts @@ -0,0 +1,155 @@ +import { Call, CallData, hash, num, RawArgs, SignerInterface, typedData } from "starknet"; +import { manager } from "./"; + +const typesRev0 = { + StarkNetDomain: [ + { name: "name", type: "felt" }, + { name: "version", type: "felt" }, + { name: "chainId", type: "felt" }, + ], + OutsideExecution: [ + { name: "caller", type: "felt" }, + { name: "nonce", type: "felt" }, + { name: "execute_after", type: "felt" }, + { name: "execute_before", type: "felt" }, + { name: "calls_len", type: "felt" }, + { name: "calls", type: "OutsideCall*" }, + ], + OutsideCall: [ + { name: "to", type: "felt" }, + { name: "selector", type: "felt" }, + { name: "calldata_len", type: "felt" }, + { name: "calldata", type: "felt*" }, + ], +}; + +const typesRev1 = { + StarknetDomain: [ + { name: "name", type: "shortstring" }, + { name: "version", type: "shortstring" }, + { name: "chainId", type: "shortstring" }, + { name: "revision", type: "shortstring" }, + ], + OutsideExecution: [ + { name: "Caller", type: "ContractAddress" }, + { name: "Nonce", type: "felt" }, + { name: "Execute After", type: "u128" }, + { name: "Execute Before", type: "u128" }, + { name: "Calls", type: "Call*" }, + ], + Call: [ + { name: "To", type: "ContractAddress" }, + { name: "Selector", type: "selector" }, + { name: "Calldata", type: "felt*" }, + ], +}; + +function getDomain(chainId: string, revision: typedData.TypedDataRevision) { + if (revision == typedData.TypedDataRevision.Active) { + // WARNING! Version and revision are encoded as numbers 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: "Account.execute_from_outside", + version: "2", + chainId: chainId, + revision: "1", + }; + } + return { + name: "Account.execute_from_outside", + version: "1", + chainId: chainId, + }; +} + +export interface OutsideExecution { + caller: string; + nonce: num.BigNumberish; + execute_after: num.BigNumberish; + execute_before: num.BigNumberish; + calls: OutsideCall[]; +} + +export interface OutsideCall { + to: string; + selector: num.BigNumberish; + calldata: RawArgs; +} + +export function getOutsideCall(call: Call): OutsideCall { + return { + to: call.contractAddress, + selector: hash.getSelectorFromName(call.entrypoint), + calldata: call.calldata ?? [], + }; +} + +export function getTypedDataHash( + outsideExecution: OutsideExecution, + accountAddress: num.BigNumberish, + chainId: string, + revision: typedData.TypedDataRevision, +): string { + return typedData.getMessageHash(getTypedData(outsideExecution, chainId, revision), accountAddress); +} + +export function getTypedData( + outsideExecution: OutsideExecution, + chainId: string, + revision: typedData.TypedDataRevision, +) { + if (revision == typedData.TypedDataRevision.Active) { + return { + types: typesRev1, + primaryType: "OutsideExecution", + domain: getDomain(chainId, revision), + message: { + Caller: outsideExecution.caller, + Nonce: outsideExecution.nonce, + "Execute After": outsideExecution.execute_after, + "Execute Before": outsideExecution.execute_before, + Calls: outsideExecution.calls.map((call) => { + return { + To: call.to, + Selector: call.selector, + Calldata: call.calldata, + }; + }), + }, + }; + } + + return { + types: typesRev0, + primaryType: "OutsideExecution", + domain: getDomain(chainId, revision), + message: { + ...outsideExecution, + calls_len: outsideExecution.calls.length, + calls: outsideExecution.calls.map((call) => { + return { + ...call, + calldata_len: call.calldata.length, + calldata: call.calldata, + }; + }), + }, + }; +} + +export async function getOutsideExecutionCall( + outsideExecution: OutsideExecution, + accountAddress: string, + signer: SignerInterface, + revision: typedData.TypedDataRevision, + chainId?: string, +): Promise { + chainId = chainId ?? (await manager.getChainId()); + const currentTypedData = getTypedData(outsideExecution, chainId, revision); + const signature = await signer.signMessage(currentTypedData, accountAddress); + return { + contractAddress: accountAddress, + entrypoint: revision == typedData.TypedDataRevision.Active ? "execute_from_outside_v2" : "execute_from_outside", + calldata: CallData.compile({ ...outsideExecution, signature }), + }; +} 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/recovery.ts b/lib/recovery.ts new file mode 100644 index 0000000..6424df8 --- /dev/null +++ b/lib/recovery.ts @@ -0,0 +1,62 @@ +import { expect } from "chai"; +import { CairoCustomEnum, Contract, hash } from "starknet"; +import { RawSigner } from "."; + +export const ESCAPE_SECURITY_PERIOD = 7n * 24n * 60n * 60n; // 7 days +export const ESCAPE_EXPIRY_PERIOD = 2n * 7n * 24n * 60n * 60n; // 14 days +export const MAX_U64 = 2n ** 64n - 1n; + +export enum EscapeStatus { + None, + NotReady, + Ready, + Expired, +} + +export const ESCAPE_TYPE_NONE = new CairoCustomEnum({ + None: {}, + Guardian: undefined, + Owner: undefined, +}); + +export const ESCAPE_TYPE_GUARDIAN = new CairoCustomEnum({ + None: undefined, + Guardian: {}, + Owner: undefined, +}); + +export const ESCAPE_TYPE_OWNER = new CairoCustomEnum({ + None: undefined, + Guardian: undefined, + Owner: {}, +}); + +export const signChangeOwnerMessage = async ( + accountAddress: string, + currentOwnerGuid: bigint, + newOwner: RawSigner, + chainId: string, +) => { + const messageHash = await getChangeOwnerMessageHash(accountAddress, currentOwnerGuid, chainId); + return newOwner.signRaw(messageHash); +}; + +export const getChangeOwnerMessageHash = async (accountAddress: string, currentOwnerGuid: bigint, chainId: string) => { + const changeOwnerSelector = hash.getSelectorFromName("change_owner"); + return hash.computeHashOnElements([changeOwnerSelector, chainId, accountAddress, currentOwnerGuid]); +}; + +export async function hasOngoingEscape(accountContract: Contract): Promise { + const escape = await accountContract.get_escape(); + return escape.escape_type != 0n && escape.ready_at != 0n && escape.new_signer != 0n; +} + +export async function getEscapeStatus(accountContract: Contract): Promise { + // StarknetJs parsing is broken so we do it manually + const result = (await accountContract.call("get_escape_and_status", undefined, { parseResponse: false })) as string[]; + const result_len = result.length; + expect(result_len).to.be.oneOf([4, 6]); + const status = Number(result[result_len - 1]); + expect(status).to.be.lessThan(4, `Unknown status ${status}`); + return status; +} diff --git a/lib/signers/cairo0-sha256.patch b/lib/signers/cairo0-sha256.patch new file mode 100644 index 0000000..4dc4e56 --- /dev/null +++ b/lib/signers/cairo0-sha256.patch @@ -0,0 +1,31 @@ +commit d77431b967d84de3a902fbfea5cf8e1ac972f6de +Author: Yoav Gaziel +Date: Thu Nov 16 11:10:28 2023 +0200 + + add external entrypoint in main.cairo + +diff --git a/src/main.cairo b/src/main.cairo +new file mode 100644 +index 0000000..16c4e00 +--- /dev/null ++++ b/src/main.cairo +@@ -0,0 +1,19 @@ ++%lang starknet ++from starkware.cairo.common.alloc import alloc ++from starkware.cairo.common.math import split_int ++from starkware.cairo.common.memcpy import memcpy ++from starkware.cairo.common.cairo_builtins import BitwiseBuiltin, HashBuiltin ++from src.sha256 import sha256, finalize_sha256 ++ ++@view ++func sha256_cairo0{bitwise_ptr: BitwiseBuiltin*, pedersen_ptr: HashBuiltin*, range_check_ptr}( ++ data_len: felt, data: felt*, data_len_no_padding: felt ++) -> (result_len: felt, result: felt*) { ++ alloc_locals; ++ let (local sha256_ptr_start: felt*) = alloc(); ++ let sha256_ptr = sha256_ptr_start; ++ let sha256_ptr_end = sha256_ptr_start; ++ let (hash) = sha256{sha256_ptr=sha256_ptr}(data, data_len_no_padding); ++ finalize_sha256(sha256_ptr_start=sha256_ptr_start, sha256_ptr_end=sha256_ptr_end); ++ return (8, hash); ++} diff --git a/lib/signers/legacy.ts b/lib/signers/legacy.ts new file mode 100644 index 0000000..37a39b1 --- /dev/null +++ b/lib/signers/legacy.ts @@ -0,0 +1,87 @@ +import { ArraySignatureType, ec, encode } 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 class LegacyMultisigSigner extends RawSigner { + constructor(public keys: RawSigner[]) { + super(); + } + + async signRaw(messageHash: string): Promise { + const keys = []; + for (const key of this.keys) { + keys.push(await key.signRaw(messageHash)); + } + return keys.flat(); + } +} + +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 ? `${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 LegacyMultisigKeyPair extends LegacyKeyPair { + pk: string; + + constructor(pk?: string | bigint) { + super(); + this.pk = pk ? `${pk}` : `0x${encode.buf2hex(ec.starkCurve.utils.randomPrivateKey())}`; + } + + public get publicKey() { + return BigInt(ec.starkCurve.getStarkKey(this.pk)); + } + + public get privateKey(): string { + return this.pk; + } + + public async signRaw(messageHash: string): Promise { + const { r, s } = ec.starkCurve.sign(messageHash, this.pk); + return [this.publicKey.toString(), r.toString(), s.toString()]; + } +} + +export const randomLegacyMultisigKeyPairs = (length: number) => + Array.from({ length }, () => new LegacyMultisigKeyPair()).sort((n1, n2) => (n1.publicKey < n2.publicKey ? -1 : 1)); diff --git a/lib/signers/secp256.ts b/lib/signers/secp256.ts new file mode 100644 index 0000000..9c52dfc --- /dev/null +++ b/lib/signers/secp256.ts @@ -0,0 +1,229 @@ +import * as utils from "@noble/curves/abstract/utils"; +import { p256 as secp256r1 } from "@noble/curves/p256"; +import { secp256k1 } from "@noble/curves/secp256k1"; +import { Signature as EthersSignature, Wallet } from "ethers"; +import { CairoCustomEnum, CallData, hash, num, shortString, uint256 } from "starknet"; +import { KeyPair, SignerType, signerTypeToCustomEnum } from "../signers/signers"; + +export type NormalizedSecpSignature = { r: bigint; s: bigint; yParity: boolean }; + +export function normalizeSecpR1Signature(signature: { + r: bigint; + s: bigint; + recovery: number; +}): NormalizedSecpSignature { + return normalizeSecpSignature(secp256r1, signature); +} + +export function normalizeSecpK1Signature(signature: { + r: bigint; + s: bigint; + recovery: number; +}): NormalizedSecpSignature { + return normalizeSecpSignature(secp256k1, signature); +} + +export function normalizeSecpSignature( + curve: typeof secp256r1 | typeof secp256k1, + signature: { r: bigint; s: bigint; recovery: number }, +): NormalizedSecpSignature { + let s = signature.s; + let yParity = signature.recovery !== 0; + if (s > curve.CURVE.n / 2n) { + s = curve.CURVE.n - s; + yParity = !yParity; + } + return { r: signature.r, s, yParity }; +} + +export class EthKeyPair extends KeyPair { + pk: bigint; + allowLowS?: boolean; + + constructor(pk?: string | bigint, allowLowS?: boolean) { + super(); + + if (pk == undefined) { + pk = Wallet.createRandom().privateKey; + } + if (typeof pk === "string") { + pk = BigInt(pk); + } + this.pk = pk; + this.allowLowS = allowLowS; + } + + public get address(): bigint { + return BigInt(new Wallet("0x" + padTo32Bytes(num.toHex(this.pk))).address); + } + + public get guid(): bigint { + return BigInt(hash.computePoseidonHash(shortString.encodeShortString("Secp256k1 Signer"), this.address)); + } + + public get storedValue(): bigint { + throw new Error("Not implemented yet"); + } + + public get signer(): CairoCustomEnum { + return signerTypeToCustomEnum(SignerType.Secp256k1, { signer: this.address }); + } + + public async signRaw(messageHash: string): Promise { + const signature = normalizeSecpK1Signature( + secp256k1.sign(padTo32Bytes(messageHash), this.pk, { lowS: this.allowLowS }), + ); + + return CallData.compile([ + signerTypeToCustomEnum(SignerType.Secp256k1, { + pubkeyHash: this.address, + r: uint256.bnToUint256(signature.r), + s: uint256.bnToUint256(signature.s), + y_parity: signature.yParity, + }), + ]); + } +} + +export class Eip191KeyPair extends KeyPair { + pk: string; + + constructor(pk?: string | bigint) { + super(); + this.pk = pk ? "0x" + padTo32Bytes(num.toHex(pk)) : Wallet.createRandom().privateKey; + } + + public get address() { + return BigInt(new Wallet(this.pk).address); + } + + public get guid(): bigint { + return BigInt(hash.computePoseidonHash(shortString.encodeShortString("Eip191 Signer"), this.address)); + } + + public get storedValue(): bigint { + throw new Error("Not implemented yet"); + } + + public get signer(): CairoCustomEnum { + return signerTypeToCustomEnum(SignerType.Eip191, { signer: this.address }); + } + + public async signRaw(messageHash: string): Promise { + const ethSigner = new Wallet(this.pk); + messageHash = "0x" + padTo32Bytes(messageHash); + const ethersSignature = EthersSignature.from(ethSigner.signMessageSync(num.hexToBytes(messageHash))); + + const signature = normalizeSecpK1Signature({ + r: BigInt(ethersSignature.r), + s: BigInt(ethersSignature.s), + recovery: ethersSignature.yParity ? 1 : 0, + }); + + return CallData.compile([ + signerTypeToCustomEnum(SignerType.Eip191, { + ethAddress: this.address, + r: uint256.bnToUint256(signature.r), + s: uint256.bnToUint256(signature.s), + y_parity: signature.yParity, + }), + ]); + } +} + +export class EstimateEip191KeyPair extends KeyPair { + readonly address: bigint; + + constructor(address: bigint) { + super(); + this.address = address; + } + + public get privateKey(): string { + throw new Error("EstimateEip191KeyPair does not have a private key"); + } + + public get guid(): bigint { + throw new Error("Not implemented yet"); + } + + public get storedValue(): bigint { + throw new Error("Not implemented yet"); + } + + public get signer(): CairoCustomEnum { + return signerTypeToCustomEnum(SignerType.Eip191, { signer: this.address }); + } + + public async signRaw(messageHash: string): Promise { + return CallData.compile([ + signerTypeToCustomEnum(SignerType.Eip191, { + ethAddress: this.address, + r: uint256.bnToUint256("0x1556a70d76cc452ae54e83bb167a9041f0d062d000fa0dcb42593f77c544f647"), + s: uint256.bnToUint256("0x1643d14dbd6a6edc658f4b16699a585181a08dba4f6d16a9273e0e2cbed622da"), + y_parity: false, + }), + ]); + } +} + +export class Secp256r1KeyPair extends KeyPair { + pk: bigint; + private allowLowS?: boolean; + + constructor(pk?: string | bigint, allowLowS?: boolean) { + super(); + this.pk = BigInt(pk ? `${pk}` : Wallet.createRandom().privateKey); + this.allowLowS = allowLowS; + } + + public get publicKey() { + const publicKey = secp256r1.getPublicKey(this.pk).slice(1); + return uint256.bnToUint256("0x" + utils.bytesToHex(publicKey)); + } + + public get guid(): bigint { + return BigInt( + hash.computePoseidonHashOnElements([ + shortString.encodeShortString("Secp256r1 Signer"), + this.publicKey.low, + this.publicKey.high, + ]), + ); + } + + public get storedValue(): bigint { + throw new Error("Not implemented yet"); + } + + public get signer() { + return signerTypeToCustomEnum(SignerType.Secp256r1, { signer: this.publicKey }); + } + + public async signRaw(messageHash: string): Promise { + messageHash = padTo32Bytes(messageHash); + const signature = normalizeSecpR1Signature(secp256r1.sign(messageHash, this.pk, { lowS: this.allowLowS })); + return CallData.compile([ + signerTypeToCustomEnum(SignerType.Secp256r1, { + pubkey: this.publicKey, + r: uint256.bnToUint256(signature.r), + s: uint256.bnToUint256(signature.s), + y_parity: signature.yParity, + }), + ]); + } +} + +export function padTo32Bytes(hexString: string): string { + if (hexString.startsWith("0x")) { + hexString = hexString.slice(2); + } + if (hexString.length < 64) { + hexString = "0".repeat(64 - hexString.length) + hexString; + } + return hexString; +} + +export const randomEthKeyPair = () => new EthKeyPair(); +export const randomEip191KeyPair = () => new Eip191KeyPair(); +export const randomSecp256r1KeyPair = () => new Secp256r1KeyPair(); diff --git a/lib/signers/signers.ts b/lib/signers/signers.ts new file mode 100644 index 0000000..0f3c956 --- /dev/null +++ b/lib/signers/signers.ts @@ -0,0 +1,302 @@ +import { + CairoCustomEnum, + CairoOption, + CairoOptionVariant, + Call, + CallData, + Calldata, + DeclareSignerDetails, + DeployAccountSignerDetails, + InvocationsSignerDetails, + RPC, + Signature, + SignerInterface, + V2DeclareSignerDetails, + V2DeployAccountSignerDetails, + V2InvocationsSignerDetails, + V3DeclareSignerDetails, + V3DeployAccountSignerDetails, + V3InvocationsSignerDetails, + ec, + encode, + hash, + num, + shortString, + stark, + transaction, + typedData, +} from "starknet"; + +/** + * 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); + } +} + +export class MultisigSigner extends RawSigner { + constructor(public keys: KeyPair[]) { + super(); + } + + async signRaw(messageHash: string): Promise { + const keys = []; + for (const key of this.keys) { + keys.push(await key.signRaw(messageHash)); + } + return [keys.length.toString(), keys.flat()].flat(); + } +} + +export class ArgentSigner extends MultisigSigner { + constructor( + public owner: KeyPair = randomStarknetKeyPair(), + public guardian?: KeyPair, + ) { + const signers = [owner]; + if (guardian) { + signers.push(guardian); + } + super(signers); + } +} + +export abstract class KeyPair extends RawSigner { + abstract get signer(): CairoCustomEnum; + abstract get guid(): bigint; + abstract get storedValue(): bigint; + + public get compiledSigner(): Calldata { + return CallData.compile([this.signer]); + } + + public get signerAsOption() { + return new CairoOption(CairoOptionVariant.Some, { + signer: this.signer, + }); + } + public get compiledSignerAsOption() { + return CallData.compile([this.signerAsOption]); + } +} + +export class StarknetKeyPair extends KeyPair { + 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 get guid() { + return BigInt(hash.computePoseidonHash(shortString.encodeShortString("Starknet Signer"), this.publicKey)); + } + + public get storedValue() { + return this.publicKey; + } + + public get signer(): CairoCustomEnum { + return signerTypeToCustomEnum(SignerType.Starknet, { signer: this.publicKey }); + } + + public async signRaw(messageHash: string): Promise { + const { r, s } = ec.starkCurve.sign(messageHash, this.pk); + return starknetSignatureType(this.publicKey, r, s); + } +} + +export class EstimateStarknetKeyPair extends KeyPair { + readonly pubKey: bigint; + + constructor(pubKey: bigint) { + super(); + this.pubKey = pubKey; + } + + public get privateKey(): string { + throw new Error("EstimateStarknetKeyPair does not have a private key"); + } + + public get publicKey() { + return this.pubKey; + } + + public get guid() { + return BigInt(hash.computePoseidonHash(shortString.encodeShortString("Starknet Signer"), this.publicKey)); + } + + public get storedValue() { + return this.publicKey; + } + + public get signer(): CairoCustomEnum { + return signerTypeToCustomEnum(SignerType.Starknet, { signer: this.publicKey }); + } + + public async signRaw(messageHash: string): Promise { + const fakeR = "0x6cefb49a1f4eb406e8112db9b8cdf247965852ddc5ca4d74b09e42471689495"; + const fakeS = "0x25760910405a052b7f08ec533939c54948bc530c662c5d79e8ff416579087f7"; + return starknetSignatureType(this.publicKey, fakeR, fakeS); + } +} + +export function starknetSignatureType( + signer: bigint | number | string, + r: bigint | number | string, + s: bigint | number | string, +) { + return CallData.compile([signerTypeToCustomEnum(SignerType.Starknet, { signer, r, s })]); +} + +export function zeroStarknetSignatureType() { + return signerTypeToCustomEnum(SignerType.Starknet, { signer: 0 }); +} + +// reflects the signer type in signer_signature.cairo +// needs to be updated for the signer types +// used to convert signertype to guid +export enum SignerType { + Starknet, + Secp256k1, + Secp256r1, + Eip191, + Webauthn, +} + +export function signerTypeToCustomEnum(signerType: SignerType, value: any): CairoCustomEnum { + const contents = { + Starknet: undefined, + Secp256k1: undefined, + Secp256r1: undefined, + Eip191: undefined, + Webauthn: undefined, + }; + + if (signerType === SignerType.Starknet) { + contents.Starknet = value; + } else if (signerType === SignerType.Secp256k1) { + contents.Secp256k1 = value; + } else if (signerType === SignerType.Secp256r1) { + contents.Secp256r1 = value; + } else if (signerType === SignerType.Eip191) { + contents.Eip191 = value; + } else if (signerType === SignerType.Webauthn) { + contents.Webauthn = value; + } else { + throw new Error(`Unknown SignerType`); + } + + return new CairoCustomEnum(contents); +} + +export function sortByGuid(keys: KeyPair[]) { + return keys.sort((n1, n2) => (n1.guid < n2.guid ? -1 : 1)); +} + +export const randomStarknetKeyPair = () => new StarknetKeyPair(); +export const randomStarknetKeyPairs = (length: number) => Array.from({ length }, randomStarknetKeyPair); diff --git a/lib/signers/webauthn.ts b/lib/signers/webauthn.ts new file mode 100644 index 0000000..f38a1de --- /dev/null +++ b/lib/signers/webauthn.ts @@ -0,0 +1,160 @@ +import { concatBytes } from "@noble/curves/abstract/utils"; +import { p256 as secp256r1 } from "@noble/curves/p256"; +import { BinaryLike, createHash } from "crypto"; +import { + ArraySignatureType, + BigNumberish, + CairoCustomEnum, + CallData, + Uint256, + hash, + shortString, + uint256, +} from "starknet"; +import { KeyPair, SignerType, normalizeSecpR1Signature, signerTypeToCustomEnum } from ".."; + +const buf2hex = (buffer: ArrayBuffer, prefix = true) => + `${prefix ? "0x" : ""}${[...new Uint8Array(buffer)].map((x) => x.toString(16).padStart(2, "0")).join("")}`; + +const normalizeTransactionHash = (transactionHash: string) => transactionHash.replace(/^0x/, "").padStart(64, "0"); + +const buf2base64url = (buffer: ArrayBuffer) => + buf2base64(buffer).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, ""); + +const buf2base64 = (buffer: ArrayBuffer) => btoa(String.fromCharCode(...new Uint8Array(buffer))); + +const hex2buf = (hex: string) => + Uint8Array.from( + hex + .replace(/^0x/, "") + .match(/.{1,2}/g)! + .map((byte) => parseInt(byte, 16)), + ); + +const toCharArray = (value: string) => CallData.compile(value.split("").map(shortString.encodeShortString)); + +interface WebauthnSigner { + origin: BigNumberish[]; + rp_id_hash: Uint256; + pubkey: Uint256; +} + +interface WebauthnSignature { + cross_origin: boolean; + client_data_json_outro: BigNumberish[]; + flags: number; + sign_count: number; + ec_signature: { r: Uint256; s: Uint256; y_parity: boolean }; + sha256_implementation: CairoCustomEnum; +} + +export class WebauthnOwner extends KeyPair { + pk: Uint8Array; + rpIdHash: Uint256; + + constructor( + pk?: string, + public rpId = "localhost", + public origin = "http://localhost:5173", + ) { + super(); + this.pk = pk ? hex2buf(normalizeTransactionHash(pk)) : secp256r1.utils.randomPrivateKey(); + this.rpIdHash = uint256.bnToUint256(buf2hex(sha256(rpId))); + } + + public get publicKey() { + return secp256r1.getPublicKey(this.pk).slice(1); + } + + public get guid(): bigint { + const rpIdHashAsU256 = this.rpIdHash; + const publicKeyAsU256 = uint256.bnToUint256(buf2hex(this.publicKey)); + const originBytes = toCharArray(this.origin); + const elements = [ + shortString.encodeShortString("Webauthn Signer"), + originBytes.length, + ...originBytes, + rpIdHashAsU256.low, + rpIdHashAsU256.high, + publicKeyAsU256.low, + publicKeyAsU256.high, + ]; + return BigInt(hash.computePoseidonHashOnElements(elements)); + } + + public get storedValue(): bigint { + throw new Error("Not implemented yet"); + } + + public get signer(): CairoCustomEnum { + const signer: WebauthnSigner = { + origin: toCharArray(this.origin), + rp_id_hash: this.rpIdHash, + pubkey: uint256.bnToUint256(buf2hex(this.publicKey)), + }; + return signerTypeToCustomEnum(SignerType.Webauthn, signer); + } + + public async signRaw(messageHash: string): Promise { + const webauthnSigner = this.signer.variant.Webauthn; + const webauthnSignature = await this.signHash(messageHash); + return CallData.compile([signerTypeToCustomEnum(SignerType.Webauthn, { webauthnSigner, webauthnSignature })]); + } + + public async signHash(transactionHash: string): Promise { + const flags = "0b00000101"; // present and verified + const signCount = 0; + const authenticatorData = concatBytes(sha256(this.rpId), new Uint8Array([Number(flags), 0, 0, 0, signCount])); + + const sha256Impl = 0; + const challenge = buf2base64url(hex2buf(`${normalizeTransactionHash(transactionHash)}0${sha256Impl}`)); + const crossOrigin = false; + const extraJson = ""; // = `,"extraField":"random data"}`; + const clientData = JSON.stringify({ type: "webauthn.get", challenge, origin: this.origin, crossOrigin }); + const clientDataJson = extraJson ? clientData.replace(/}$/, extraJson) : clientData; + const clientDataHash = sha256(new TextEncoder().encode(clientDataJson)); + + const signedHash = sha256(concatBytes(authenticatorData, clientDataHash)); + + const signature = normalizeSecpR1Signature(secp256r1.sign(signedHash, this.pk)); + + // console.log(` + // let transaction_hash = ${transactionHash}; + // let pubkey = ${buf2hex(this.publicKey)}; + // let signer = new_webauthn_signer(:origin, :rp_id_hash, :pubkey); + // let signature = WebauthnSignature { + // cross_origin: ${crossOrigin}, + // client_data_json_outro: ${extraJson ? `${JSON.stringify(extraJson)}.into_bytes()` : "array![]"}.span(), + // flags: ${flags}, + // sign_count: ${signCount}, + // ec_signature: Signature { + // r: 0x${r.toString(16)}, + // s: 0x${s.toString(16)}, + // y_parity: ${recovery !== 0}, + // }, + // sha256_implementation: Sha256Implementation::Cairo${sha256Impl}, + // };`); + + return { + cross_origin: crossOrigin, + client_data_json_outro: CallData.compile(toCharArray(extraJson)), + flags: Number(flags), + sign_count: signCount, + ec_signature: { + r: uint256.bnToUint256(signature.r), + s: uint256.bnToUint256(signature.s), + y_parity: signature.yParity, + }, + sha256_implementation: new CairoCustomEnum({ + Cairo0: sha256Impl ? undefined : {}, + Cairo1: sha256Impl ? {} : undefined, + }), + }; + } +} + +function sha256(message: BinaryLike) { + return createHash("sha256").update(message).digest(); +} + +export const randomWebauthnOwner = () => new WebauthnOwner(); diff --git a/lib/tokens.ts b/lib/tokens.ts new file mode 100644 index 0000000..b9f4767 --- /dev/null +++ b/lib/tokens.ts @@ -0,0 +1,49 @@ +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(); + } + + 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 ethBalance(accountAddress: string): Promise { + const ethContract = await this.ethContract(); + return await ethContract.balanceOf(accountAddress); + } + + async strkBalance(accountAddress: string): Promise { + const strkContract = await this.strkContract(); + return await strkContract.balanceOf(accountAddress); + } +} diff --git a/lib/udc.ts b/lib/udc.ts new file mode 100644 index 0000000..fe8abe1 --- /dev/null +++ b/lib/udc.ts @@ -0,0 +1,19 @@ +import { CallData, RawCalldata } from "starknet"; +import { deployer, manager } from "."; + +export const udcAddress = "0x041a78e741e5af2fec34b695679bc6891742439f7afb8484ecd7766661ad02bf"; + +export async function deployContractUDC(classHash: string, salt: string, calldata: RawCalldata) { + const unique = 0n; //false + + const udcContract = await manager.loadContract(udcAddress); + + udcContract.connect(deployer); + + const deployCall = udcContract.populate("deployContract", CallData.compile([classHash, salt, unique, calldata])); + const { transaction_hash } = await udcContract.deployContract(deployCall.calldata); + + const transaction_response = await manager.waitForTransaction(transaction_hash); + + return transaction_response.events?.[0].from_address; +} diff --git a/lib/upgrade.ts b/lib/upgrade.ts new file mode 100644 index 0000000..d60be57 --- /dev/null +++ b/lib/upgrade.ts @@ -0,0 +1,12 @@ +import { Call, CallData } from "starknet"; +import { getOutsideCall } from "./outsideExecution"; + +export function getUpgradeData(calls: Call[]) { + const externalCalls = calls.map(getOutsideCall); + return CallData.compile({ externalCalls }); +} + +export function getUpgradeDataLegacy(calls: Call[]) { + const upgradeData = getUpgradeData(calls); + return CallData.compile({ upgradeData }); +} diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..bc23b5a --- /dev/null +++ b/package-lock.json @@ -0,0 +1,677 @@ +{ + "name": "scripts", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "scripts", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "starknet": "6.5.0" + }, + "devDependencies": { + "@types/node": "^20.11.30", + "dotenv": "^16.3.1", + "ts-node": "^10.9.1", + "typescript": "^5.4.3" + } + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@noble/curves": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.3.0.tgz", + "integrity": "sha512-t01iSXPuN+Eqzb4eBX0S5oubSqXbK/xXa1Ne18Hj8f9pStxztHCE2gfboSp/dZRLSqfuLpRK2nDXDK+W9puocA==", + "dependencies": { + "@noble/hashes": "1.3.3" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/hashes": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.3.tgz", + "integrity": "sha512-V7/fPHgl+jsVPXqqeOzT8egNj2iBIVt+ECeMMG8TdcnTikP3oaBtUVqpT/gYCR68aEBJSF+XbYUxStjbFMqIIA==", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/base": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.6.tgz", + "integrity": "sha512-ok9AWwhcgYuGG3Zfhyqg+zwl+Wn5uE+dwC0NV/2qQkx4dABbb/bx96vWu8NSj+BNjjSjno+JRYRjle1jV08k3g==", + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/starknet": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@scure/starknet/-/starknet-1.0.0.tgz", + "integrity": "sha512-o5J57zY0f+2IL/mq8+AYJJ4Xpc1fOtDhr+mFQKbHnYFmm3WQrC+8zj2HEgxak1a+x86mhmBC1Kq305KUpVf0wg==", + "dependencies": { + "@noble/curves": "~1.3.0", + "@noble/hashes": "~1.3.3" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", + "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", + "dev": true + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true + }, + "node_modules/@types/node": { + "version": "20.12.4", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.4.tgz", + "integrity": "sha512-E+Fa9z3wSQpzgYQdYmme5X3OTuejnnTx88A6p6vkkJosR3KBz+HpE3kqNm98VE6cfLFcISx7zW7MsJkH6KwbTw==", + "dev": true, + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/abi-wan-kanabi": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/abi-wan-kanabi/-/abi-wan-kanabi-2.2.2.tgz", + "integrity": "sha512-sTCv2HyNIj1x2WFUoc9oL8ZT9liosrL+GoqEGZJK1kDND096CfA7lwx06vLxLWMocQ41FQXO3oliwoh/UZHYdQ==", + "dependencies": { + "ansicolors": "^0.3.2", + "cardinal": "^2.1.1", + "fs-extra": "^10.0.0", + "yargs": "^17.7.2" + }, + "bin": { + "generate": "dist/generate.js" + } + }, + "node_modules/acorn": { + "version": "8.11.3", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", + "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.2.tgz", + "integrity": "sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/ansicolors": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/ansicolors/-/ansicolors-0.3.2.tgz", + "integrity": "sha512-QXu7BPrP29VllRxH8GwB7x5iX5qWKAAMLqKQGWTeLWVlNHNOpVMJ91dsxQAIWXpjuW5wqvxu3Jd/nRjrJ+0pqg==" + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true + }, + "node_modules/cardinal": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/cardinal/-/cardinal-2.1.1.tgz", + "integrity": "sha512-JSr5eOgoEymtYHBjNWyjrMqet9Am2miJhlfKNdqLp6zoeAh0KN5dRAcxlecj5mAJrmQomgiOBj35xHLrFjqBpw==", + "dependencies": { + "ansicolors": "~0.3.2", + "redeyed": "~2.1.0" + }, + "bin": { + "cdl": "bin/cdl.js" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "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" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true + }, + "node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true, + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/dotenv": { + "version": "16.4.5", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", + "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "node_modules/escalade": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", + "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", + "engines": { + "node": ">=6" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/fetch-cookie": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/fetch-cookie/-/fetch-cookie-3.0.1.tgz", + "integrity": "sha512-ZGXe8Y5Z/1FWqQ9q/CrJhkUD73DyBU9VF0hBQmEO/wPHe4A9PKTjplFDLeFX8aOsYypZUcX5Ji/eByn3VCVO3Q==", + "dependencies": { + "set-cookie-parser": "^2.4.8", + "tough-cookie": "^4.0.0" + } + }, + "node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "engines": { + "node": ">=8" + } + }, + "node_modules/isomorphic-fetch": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-3.0.0.tgz", + "integrity": "sha512-qvUtwJ3j6qwsF3jLxkZ72qCgjMysPzDfeV240JHiGZsANBYd+EEuu35v7dfrJ9Up0Ak07D7GGSkGhCHTqg/5wA==", + "dependencies": { + "node-fetch": "^2.6.1", + "whatwg-fetch": "^3.4.1" + } + }, + "node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/lossless-json": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lossless-json/-/lossless-json-4.0.1.tgz", + "integrity": "sha512-l0L+ppmgPDnb+JGxNLndPtJZGNf6+ZmVaQzoxQm3u6TXmhdnsA+YtdVR8DjzZd/em58686CQhOFDPewfJ4l7MA==" + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/pako": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz", + "integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==" + }, + "node_modules/psl": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", + "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==" + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "engines": { + "node": ">=6" + } + }, + "node_modules/querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==" + }, + "node_modules/redeyed": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/redeyed/-/redeyed-2.1.1.tgz", + "integrity": "sha512-FNpGGo1DycYAdnrKFxCMmKYgo/mILAqtRYbkdQD8Ep/Hk2PQ5+aEAEx+IU713RTDmuBaH0c8P5ZozurNu5ObRQ==", + "dependencies": { + "esprima": "~4.0.0" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==" + }, + "node_modules/set-cookie-parser": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.6.0.tgz", + "integrity": "sha512-RVnVQxTXuerk653XfuliOxBP81Sf0+qfQE73LIYKcyMYHG94AuH0kgrQpRDuTZnSmjpysHmzxJXKNfa6PjFhyQ==" + }, + "node_modules/starknet": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/starknet/-/starknet-6.5.0.tgz", + "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" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "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" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tough-cookie": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.3.tgz", + "integrity": "sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw==", + "dependencies": { + "psl": "^1.1.33", + "punycode": "^2.1.1", + "universalify": "^0.2.0", + "url-parse": "^1.5.3" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tough-cookie/node_modules/universalify": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", + "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, + "node_modules/ts-mixer": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/ts-mixer/-/ts-mixer-6.0.4.tgz", + "integrity": "sha512-ufKpbmrugz5Aou4wcr5Wc1UUFWOLhq+Fm6qa6P0w0K5Qw2yhaUoiWszhCVuNQyNwrlGiscHOmqYoAox1PtvgjA==" + }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "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" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/typescript": { + "version": "5.4.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.3.tgz", + "integrity": "sha512-KrPd3PKaCLr78MalgiwJnA25Nm8HAmdwN3mYUYZgG/wizIo9EainNVQI9/yDavtVFRN2h3k8uf3GLHuhDMgEHg==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "dev": true + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/url-join": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/url-join/-/url-join-4.0.1.tgz", + "integrity": "sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==" + }, + "node_modules/url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "dependencies": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "node_modules/whatwg-fetch": { + "version": "3.6.20", + "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.20.tgz", + "integrity": "sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "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" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "engines": { + "node": ">=12" + } + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "engines": { + "node": ">=6" + } + } + } +} 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/declare.js b/scripts/declare.js new file mode 100644 index 0000000..75453e7 --- /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/starknet_gifting_EscrowAccount.contract_class.json").toString("ascii"), +); +const compiledTestCasm = json.parse( + fs.readFileSync("./target/dev/starknet_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/double-transfer.js b/scripts/double-transfer.js new file mode 100644 index 0000000..d86d396 --- /dev/null +++ b/scripts/double-transfer.js @@ -0,0 +1,69 @@ +import "dotenv/config"; +import { Account, Contract, RpcProvider } from "starknet"; + +// connect provider +const provider = new RpcProvider({ nodeUrl: process.env.RPC_PROVIDER }); +const account = new Account(provider, process.env.ACCOUNT, process.env.PRIVATE_KEY); +const ethAddress = "0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7"; +const ethClassHash = "0x05ffbcfeb50d200a0677c48a129a11245a3fc519d1d98d76882d1c9a1b19c6ed"; +const receiver = "0x01a10f22c98BA5Fb02374089EAA9c62deaced318a909c264a5270B2844CCb37d"; +const amount = 50; +const maxFee = 1e15; + +// setInterval(getNonce, 3000); + +const ethContract = await loadContract(ethAddress, ethClassHash); + +let nonce = await getNonce(); + +doSendTx(nonce, amount, "0"); +doSendTx(nonce, amount + 1, "0b"); +doSendTx(nonce + 1, amount + 3, "+1"); +doSendTx(nonce + 1, amount + 4, "+1b"); +doSendTx(nonce + 2, amount + 5, "+2"); +doSendTx(nonce + 2, amount + 6, "+2b"); + +function doSendTx(nonce, amount, name) { + account + .execute(ethContract.populateTransaction.transfer(receiver, amount), undefined, { + skipValidate: true, + maxFee, + nonce, + }) + .then(async (tx) => handle(name, tx)); +} +async function handle(name, tx) { + console.log(`${getFormattedDate()} ${name}`); + getNonce(); + console.log(tx); + try { + const x = await provider.waitForTransaction(tx.transaction_hash); + console.log(`${getFormattedDate()} result ${name}`); + console.log(x); + getNonce(); + } catch (error) { + console.log(`${getFormattedDate()} error ${name}`); + console.log(error); + getNonce(); + } +} + +function getFormattedDate() { + return "[" + new Date().toLocaleTimeString() + "]"; +} + +async function getNonce() { + let nonce = await account.getNonce(); + console.log(`${getFormattedDate()} Nonce: ${nonce}`); + return Number(nonce); +} + +async function loadContract(contractAddress) { + const { abi } = await provider.getClassAt(contractAddress); + return new Contract( + abi, + contractAddress, + provider, + "0x05ffbcfeb50d200a0677c48a129a11245a3fc519d1d98d76882d1c9a1b19c6ed", + ); +} diff --git a/scripts/monitor-nonce.js b/scripts/monitor-nonce.js new file mode 100644 index 0000000..ff3a28a --- /dev/null +++ b/scripts/monitor-nonce.js @@ -0,0 +1,17 @@ +import "dotenv/config"; +import { Account, RpcProvider } from "starknet"; + +// connect provider +const provider = new RpcProvider({ nodeUrl: process.env.RPC_PROVIDER }); +const account = new Account(provider, process.env.ACCOUNT); + +async function monitorNonce() { + let nonce = await account.getNonce("latest"); + console.log(`${getFormattedDate()} Nonce: ${nonce}`); +} + +function getFormattedDate() { + return "[" + new Date().toLocaleTimeString() + "]"; +} + +setInterval(monitorNonce, 1000); diff --git a/scripts/profile.ts b/scripts/profile.ts new file mode 100644 index 0000000..1cff752 --- /dev/null +++ b/scripts/profile.ts @@ -0,0 +1,61 @@ +import { Account, RPC, num, uint256 } from "starknet"; +import { LegacyStarknetKeyPair, deployer, manager } 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 claimAccountClassHash = await manager.declareLocalContract("ClaimAccount"); +const factory = await manager.deployContract("GiftFactory", { + unique: true, + constructorCalldata: [claimAccountClassHash, deployer.address], +}); + +for (const useTxV3 of [false, true]) { + const signer = new LegacyStarknetKeyPair(); + const claimPubkey = signer.publicKey; + const amount = 1000000000000000n; + const maxFee = 50000000000000n; + const receiver = "0x42"; + + // Make a gift + const tokenContract = await manager.tokens.feeTokenContract(useTxV3); + tokenContract.connect(deployer); + factory.connect(deployer); + await tokenContract.approve(factory.address, amount + maxFee); + await profiler.profile( + `Deposit (txV3: ${useTxV3})`, + await factory.deposit(amount, maxFee, tokenContract.address, claimPubkey), + ); + + // Ensure there is a contract for the claim + const claimAddress = await factory.get_claim_address( + deployer.address, + amount, + maxFee, + tokenContract.address, + claimPubkey, + ); + + const claim = { + factory: factory.address, + class_hash: claimAccountClassHash, + sender: deployer.address, + amount: uint256.bnToUint256(amount), + max_fee: maxFee, + token: tokenContract.address, + claim_pubkey: claimPubkey, + }; + + const txVersion = useTxV3 ? RPC.ETransactionVersion.V3 : RPC.ETransactionVersion.V2; + const claimAccount = new Account(manager, num.toHex(claimAddress), signer, undefined, txVersion); + factory.connect(claimAccount); + await profiler.profile(`Claim (txV3: ${useTxV3})`, await factory.claim_internal(claim, receiver)); +} + +profiler.printSummary(); +profiler.updateOrCheckReport(); diff --git a/src/contracts/claim_account.cairo b/src/contracts/claim_account.cairo new file mode 100644 index 0000000..41f450c --- /dev/null +++ b/src/contracts/claim_account.cairo @@ -0,0 +1,102 @@ +#[starknet::contract(account)] +mod ClaimAccount { + use core::ecdsa::check_ecdsa_signature; + use core::num::traits::Zero; + use core::starknet::event::EventEmitter; + use core::traits::TryInto; + use openzeppelin::token::erc20::interface::{IERC20, IERC20DispatcherTrait, IERC20Dispatcher}; + use starknet::{ + ClassHash, account::Call, VALIDATED, call_contract_syscall, ContractAddress, get_contract_address, + get_caller_address, contract_address::contract_address_const, get_execution_info + }; + use starknet_gifting::contracts::claim_hash::{ClaimExternal, IOffChainMessageHashRev1}; + use starknet_gifting::contracts::claim_utils::calculate_claim_account_address; + use starknet_gifting::contracts::interface::{IAccount, IGiftAccount, ClaimData, AccountConstructorArguments}; + use starknet_gifting::contracts::utils::{ + full_deserialize, STRK_ADDRESS, ETH_ADDRESS, TX_V1_ESTIMATE, TX_V1, TX_V3, TX_V3_ESTIMATE, compute_max_fee_v3, + execute_multicall + }; + #[storage] + struct Storage {} + + #[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(), 'gift-acc/only-protocol'); + assert(calls.len() == 1, 'gift-acc/invalid-call-len'); + let Call { to, selector, calldata } = calls.at(0); + assert(*selector == selector!("claim_internal"), 'gift-acc/invalid-call-selector'); + let (claim, _): (ClaimData, ContractAddress) = full_deserialize(*calldata) + .expect('gift-acc/invalid-calldata'); + assert(*to == claim.factory, 'gift-acc/invalid-call-to'); + self.assert_valid_claim(claim); + + let tx_info = execution_info.tx_info.unbox(); + // Isn't it an issue if for some reason it fails during execution? + // Like if the gas is not enough? + // Nonce will be incremented and the account will be unusable + assert(tx_info.nonce == 0, 'gift-acc/invalid-claim-nonce'); + let execution_hash = tx_info.transaction_hash; + let signature = tx_info.signature; + assert(signature.len() == 2, 'gift-acc/invalid-signature-len'); + // Should we allow while in estimation? + assert( + check_ecdsa_signature(execution_hash, claim.claim_pubkey, *signature[0], *signature[1]), + 'invalid-signature' + ); + let tx_version = tx_info.version; + if claim.token == STRK_ADDRESS() { + assert(tx_version == TX_V3 || tx_version == TX_V3_ESTIMATE, 'gift-acc/invalid-tx3-version'); + let tx_fee = compute_max_fee_v3(tx_info.resource_bounds, tx_info.tip); + // TODO: should this error be max fee too high? + assert(tx_fee <= claim.max_fee, 'gift-acc/insufficient-v3-fee'); + } else if claim.token == ETH_ADDRESS() { + assert(tx_version == TX_V1 || tx_version == TX_V1_ESTIMATE, 'gift-acc/invalid-tx1-version'); + // TODO: should this error be max fee too high? + assert(tx_info.max_fee <= claim.max_fee, 'gift-acc/insufficient-v1-fee'); + } else { + core::panic_with_felt252('gift-acc/invalid-token'); + } + VALIDATED + } + + fn __execute__(ref self: ContractState, calls: Array) -> Array> { + let execution_info = get_execution_info().unbox(); + assert(execution_info.caller_address.is_zero(), 'gift-acc/only-protocol'); + let Call { to, selector, calldata }: @Call = calls[0]; + call_contract_syscall(*to, *selector, *calldata).expect('gift-acc/execute-failed'); + array![] + } + + fn is_valid_signature(self: @ContractState, hash: felt252, signature: Array) -> felt252 { + 0 + } + } + + #[abi(embed_v0)] + impl GiftAccountImpl of IGiftAccount { + fn execute_factory_calls( + ref self: ContractState, claim: ClaimData, mut calls: Array + ) -> Array> { + self.assert_valid_claim(claim); + assert(get_caller_address() == claim.factory, 'gift/only-factory'); + execute_multicall(calls.span()) + } + } + + #[generate_trait] + impl Private of PrivateTrait { + fn assert_valid_claim(self: @ContractState, claim: ClaimData) { + let calculated_address = calculate_claim_account_address(claim); + assert(calculated_address == get_contract_address(), 'gift-acc/invalid-claim-address'); + } + } +} diff --git a/src/contracts/claim_hash.cairo b/src/contracts/claim_hash.cairo new file mode 100644 index 0000000..3d9366a --- /dev/null +++ b/src/contracts/claim_hash.cairo @@ -0,0 +1,63 @@ +use core::poseidon::poseidon_hash_span; +use starknet::{ContractAddress, get_tx_info, get_contract_address}; +use starknet_gifting::contracts::interface::ClaimData; + +/// @notice Defines the function to generate the SNIP-12 revision 1 compliant message hash +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 +trait IStructHashRev1 { + fn get_struct_hash_rev_1(self: @T) -> felt252; +} + +/// @notice StarkNetDomain using SNIP 12 Revision 1 +#[derive(Drop, Copy)] +struct StarknetDomain { + name: felt252, + version: felt252, + chain_id: felt252, + revision: felt252, +} + +#[derive(Drop, Copy)] +struct ClaimExternal { + claim: ClaimData, + 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\")"); + +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')].span() + ) + } +} + +impl ClaimExternalHash of IOffChainMessageHashRev1 { + fn get_message_hash_rev_1(self: @ClaimExternal, account: ContractAddress) -> felt252 { + let chain_id = get_tx_info().unbox().chain_id; + let domain = StarknetDomain { name: 'GiftAccount.claim_external', version: '1', chain_id, revision: 1 }; + // We could hardcode mainnet && sepolia for better performance + poseidon_hash_span( + array!['StarkNet Message', domain.get_struct_hash_rev_1(), account.into(), self.get_struct_hash_rev_1()] + .span() + ) + } +} diff --git a/src/contracts/claim_utils.cairo b/src/contracts/claim_utils.cairo new file mode 100644 index 0000000..32d68ae --- /dev/null +++ b/src/contracts/claim_utils.cairo @@ -0,0 +1,20 @@ +use openzeppelin::utils::deployments::calculate_contract_address_from_deploy_syscall; +use starknet::{ContractAddress, contract_address_const}; +use starknet_gifting::contracts::interface::{ClaimData, AccountConstructorArguments}; +use starknet_gifting::contracts::utils::serialize; + +fn calculate_claim_account_address(claim: ClaimData) -> ContractAddress { + let constructor_arguments = AccountConstructorArguments { + sender: claim.sender, + amount: claim.amount, + max_fee: claim.max_fee, + token: claim.token, + claim_pubkey: claim.claim_pubkey + }; + return calculate_contract_address_from_deploy_syscall( + 0, // salt + claim.class_hash, // class_hash + serialize(@constructor_arguments).span(), // constructor_data + claim.factory + ); +} diff --git a/src/contracts/gift_factory.cairo b/src/contracts/gift_factory.cairo new file mode 100644 index 0000000..3a6fd76 --- /dev/null +++ b/src/contracts/gift_factory.cairo @@ -0,0 +1,242 @@ +#[starknet::contract] +mod GiftFactory { + use core::ecdsa::check_ecdsa_signature; + use openzeppelin::access::ownable::OwnableComponent; + use openzeppelin::security::PausableComponent; + use openzeppelin::token::erc20::interface::{IERC20, IERC20DispatcherTrait, IERC20Dispatcher}; + use openzeppelin::utils::deployments::calculate_contract_address_from_deploy_syscall; + use starknet::{ + ClassHash, ContractAddress, deploy_syscall, get_caller_address, get_contract_address, + contract_address::contract_address_const, account::Call + }; + use starknet_gifting::contracts::claim_hash::{ClaimExternal, IOffChainMessageHashRev1}; + use starknet_gifting::contracts::claim_utils::{calculate_claim_account_address}; + + use starknet_gifting::contracts::interface::{ + IGiftAccount, IGiftAccountDispatcherTrait, IGiftFactory, ClaimData, AccountConstructorArguments, + IGiftAccountDispatcher + }; + use starknet_gifting::contracts::utils::{STRK_ADDRESS, ETH_ADDRESS, serialize, full_deserialize}; + + // Ownable + component!(path: OwnableComponent, storage: ownable, event: OwnableEvent); + #[abi(embed_v0)] + impl OwnableImpl = OwnableComponent::OwnableImpl; + impl InternalImpl = OwnableComponent::InternalImpl; + + // Pausable + component!(path: PausableComponent, storage: pausable, event: PausableEvent); + // Pausable + #[abi(embed_v0)] + impl PausableImpl = PausableComponent::PausableImpl; + impl PausableInternalImpl = PausableComponent::InternalImpl; + + #[storage] + struct Storage { + #[substorage(v0)] + ownable: OwnableComponent::Storage, + #[substorage(v0)] + pausable: PausableComponent::Storage, + claim_class_hash: ClassHash, + } + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + #[flat] + OwnableEvent: OwnableComponent::Event, + #[flat] + PausableEvent: PausableComponent::Event, + GiftCreated: GiftCreated, + GiftClaimed: GiftClaimed, + GiftCanceled: GiftCanceled, + } + + #[derive(Drop, starknet::Event)] + struct GiftCreated { + #[key] + claim_pubkey: felt252, + #[key] // Find back all gifts from a specific sender + sender: ContractAddress, + #[key] // If you have the ContractAddress you can find back the claim + gift_address: ContractAddress, + class_hash: ClassHash, + factory: ContractAddress, + amount: u256, + max_fee: u128, + token: ContractAddress, + } + + // TODO Do we need a different event for external claims? + #[derive(Drop, starknet::Event)] + struct GiftClaimed { + #[key] + receiver: ContractAddress + } + + #[derive(Drop, starknet::Event)] + struct GiftCanceled {} + + // TODO replace all fields with NonZero + // TODO Upgrade? or will we just deploy a new factory with another claim_class_hash and logic attached + + #[constructor] + fn constructor(ref self: ContractState, claim_class_hash: ClassHash, owner: ContractAddress) { + self.claim_class_hash.write(claim_class_hash); + self.ownable.initializer(owner); + } + + #[abi(embed_v0)] + impl GiftFactoryImpl of IGiftFactory { + fn deposit( + ref self: ContractState, amount: u256, max_fee: u128, token: ContractAddress, claim_pubkey: felt252 + ) { + self.pausable.assert_not_paused(); + assert(token == STRK_ADDRESS() || token == ETH_ADDRESS(), 'gift-fac/invalid-token'); + // TODO: Assert max_fee is not zero? + assert(max_fee.into() < amount, 'gift-fac/fee-too-high'); + + let sender = get_caller_address(); + let factory = get_contract_address(); + // TODO We could manually serialize for better performance + // TODO pubkey can be zero? + let constructor_arguments = AccountConstructorArguments { sender, amount, max_fee, token, claim_pubkey }; + let (claim_contract, _) = deploy_syscall( + self.claim_class_hash.read(), // class_hash + 0, // salt + serialize(@constructor_arguments).span(), // constructor data + false // deploy_from_zero + ) + .expect('gift-fac/deploy-failed'); + self + .emit( + GiftCreated { + claim_pubkey, + factory, + gift_address: claim_contract, + class_hash: self.claim_class_hash.read(), + sender, + amount, + max_fee, + token, + } + ); + let transfer_status = IERC20Dispatcher { contract_address: token } + .transfer_from(get_caller_address(), claim_contract, amount + max_fee.into()); + assert(transfer_status, 'gift-fac/transfer-failed'); + } + + fn claim_internal(ref self: ContractState, claim: ClaimData, receiver: ContractAddress) { + let claim_address = self.check_factory_and_get_account_address(claim); + assert(get_caller_address() == claim_address, 'gift/only-claim-account'); + let balance = IERC20Dispatcher { contract_address: claim.token }.balance_of(claim_address); + assert(balance > claim.amount, 'gift-acc/gift-canceled'); + self.transfer_from_account(claim, claim_address, claim.token, claim.amount, receiver); + self.emit(GiftClaimed { receiver }); + } + + fn claim_external( + ref self: ContractState, claim: ClaimData, receiver: ContractAddress, signature: Array + ) { + let claim_address = self.check_factory_and_get_account_address(claim); + let claim_external_hash = ClaimExternal { claim, receiver }.get_message_hash_rev_1(claim_address); + assert( + check_ecdsa_signature(claim_external_hash, claim.claim_pubkey, *signature[0], *signature[1]), + 'gift-acc/invalid-ext-signature' + ); + + let balance = IERC20Dispatcher { contract_address: claim.token }.balance_of(claim_address); + self.transfer_from_account(claim, claim_address, claim.token, balance, receiver); + self.emit(GiftClaimed { receiver }); + } + + fn cancel(ref self: ContractState, claim: ClaimData) { + let claim_address = self.check_factory_and_get_account_address(claim); + assert(get_caller_address() == claim.sender, 'gift-acc/wrong-sender'); + + let balance = IERC20Dispatcher { contract_address: claim.token }.balance_of(claim_address); + // Won't that lead to the sender also being able to get the extra dust? + // assert(balance > claim.max_fee, 'already claimed'); + assert(balance > 0, 'gift-acc/already-claimed'); + self.transfer_from_account(claim, claim_address, claim.token, balance, claim.sender); + self.emit(GiftCanceled {}); + } + + + fn get_dust(ref self: ContractState, claim: ClaimData, receiver: ContractAddress) { + self.ownable.assert_only_owner(); + let claim_address = self.check_factory_and_get_account_address(claim); + + let balance = IERC20Dispatcher { contract_address: claim.token }.balance_of(claim_address); + assert(balance < claim.max_fee.into(), 'gift/not-yet-claimed'); + self.transfer_from_account(claim, claim_address, claim.token, balance, receiver); + } + + fn get_claim_class_hash(ref self: ContractState) -> ClassHash { + self.claim_class_hash.read() + } + + fn get_claim_address( + self: @ContractState, + sender: ContractAddress, + amount: u256, + max_fee: u128, + token: ContractAddress, + claim_pubkey: felt252 + ) -> ContractAddress { + calculate_claim_account_address( + ClaimData { + factory: get_contract_address(), + class_hash: self.claim_class_hash.read(), + sender, + amount, + max_fee, + token, + claim_pubkey, + } + ) + } + } + + #[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(); + } + + + #[generate_trait] + impl Private of PrivateTrait { + fn check_factory_and_get_account_address(self: @ContractState, claim: ClaimData) -> ContractAddress { + assert(claim.factory == get_contract_address(), 'gift/invalid-factory-address'); + calculate_claim_account_address(claim) + } + + fn transfer_from_account( + self: @ContractState, + claim: ClaimData, + claim_address: ContractAddress, + token: ContractAddress, + amount: u256, + receiver: ContractAddress, + ) { + let results = IGiftAccountDispatcher { contract_address: claim_address } + .execute_factory_calls( + claim, + array![ + Call { + to: token, selector: selector!("transfer"), calldata: serialize(@(receiver, amount)).span() + }, + ] + ); + let transfer_status = full_deserialize::(*results.at(0)).expect('gift/invalid-result-calldata'); + assert(transfer_status, 'gift-acc/transfer-failed'); + } + } +} diff --git a/src/contracts/interface.cairo b/src/contracts/interface.cairo new file mode 100644 index 0000000..5c261fd --- /dev/null +++ b/src/contracts/interface.cairo @@ -0,0 +1,57 @@ +use starknet::{ContractAddress, ClassHash, account::Call}; + +#[starknet::interface] +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; +} + +#[starknet::interface] +trait IGiftFactory { + fn deposit(ref self: TContractState, amount: u256, max_fee: u128, token: ContractAddress, claim_pubkey: felt252); + fn get_claim_address( + self: @TContractState, + sender: ContractAddress, + amount: u256, + max_fee: u128, + token: ContractAddress, + claim_pubkey: felt252 + ) -> ContractAddress; + fn get_claim_class_hash(ref self: TContractState) -> ClassHash; + + fn claim_internal(ref self: TContractState, claim: ClaimData, receiver: ContractAddress); + + fn claim_external(ref self: TContractState, claim: ClaimData, receiver: ContractAddress, signature: Array); + + fn cancel(ref self: TContractState, claim: ClaimData); + + fn get_dust(ref self: TContractState, claim: ClaimData, receiver: ContractAddress); +} + +// TODO Align => Rename ClaimData to Claim OR claim to claim_data +// Or even rename to GIFT? so that the user will see gifts in the interface +#[starknet::interface] +trait IGiftAccount { + fn execute_factory_calls(ref self: TContractState, claim: ClaimData, calls: Array) -> Array>; +} + +#[derive(Serde, Drop, Copy)] +struct ClaimData { + factory: ContractAddress, + class_hash: ClassHash, + sender: ContractAddress, + amount: u256, + max_fee: u128, + token: ContractAddress, + claim_pubkey: felt252 +} + +#[derive(Serde, Drop, Copy)] +struct AccountConstructorArguments { + sender: ContractAddress, + amount: u256, + max_fee: u128, + token: ContractAddress, + claim_pubkey: felt252 +} diff --git a/src/contracts/utils.cairo b/src/contracts/utils.cairo new file mode 100644 index 0000000..93131e0 --- /dev/null +++ b/src/contracts/utils.cairo @@ -0,0 +1,82 @@ +// TODO Just temp atm, plz split this file +use core::hash::{HashStateTrait, HashStateExTrait, Hash}; +use core::poseidon::{PoseidonTrait, HashState}; +use openzeppelin::token::erc20::interface::IERC20Dispatcher; +use starknet::{ + ContractAddress, account::Call, contract_address::contract_address_const, info::v2::ResourceBounds, + call_contract_syscall +}; + +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>() +} + +// Tries to deserialize the given data into. +// The data must only contain the returned value and nothing else +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 + } +} + +fn serialize>(value: @E) -> Array { + let mut output = array![]; + ESerde::serialize(value, ref output); + output +} + + +fn compute_max_fee_v3(mut resource_bounds: Span, tip: u128) -> u128 { + 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 +} + +fn execute_multicall(mut calls: Span) -> Array> { + let mut result = array![]; + let mut index = 0; + while let Option::Some(call) = calls + .pop_front() { + match call_contract_syscall(*call.to, *call.selector, *call.calldata) { + Result::Ok(retdata) => { + result.append(retdata); + index += 1; + }, + Result::Err(revert_reason) => { + let mut data = array!['argent/multicall-failed', index]; + data.append_all(revert_reason.span()); + panic(data); + }, + } + }; + result +} + +#[generate_trait] +impl ArrayExt, +Copy> of ArrayExtTrait { + fn append_all(ref self: Array, mut value: Span) { + while let Option::Some(item) = value.pop_front() { + self.append(*item); + }; + } +} diff --git a/src/lib.cairo b/src/lib.cairo new file mode 100644 index 0000000..5292af5 --- /dev/null +++ b/src/lib.cairo @@ -0,0 +1,12 @@ +mod contracts { + mod claim_account; + mod claim_hash; + mod claim_utils; + mod gift_factory; + mod interface; + mod utils; +} + +mod mocks { + mod erc20; +} diff --git a/src/mocks/erc20.cairo b/src/mocks/erc20.cairo new file mode 100644 index 0000000..e9d5dfa --- /dev/null +++ b/src/mocks/erc20.cairo @@ -0,0 +1,120 @@ +#[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); + } +} + + +#[starknet::contract] +mod BrokenERC20 { + use openzeppelin::token::erc20::interface::IERC20; + use openzeppelin::token::erc20::{ERC20Component, ERC20HooksEmptyImpl}; + use starknet::{info::{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/tests-integration/account.test.ts b/tests-integration/account.test.ts new file mode 100644 index 0000000..d108b26 --- /dev/null +++ b/tests-integration/account.test.ts @@ -0,0 +1,210 @@ +import { expect } from "chai"; +import { Account, CallData, RPC, hash, num, uint256 } from "starknet"; +import { LegacyStarknetKeyPair, deployer, expectRevertWithErrorMessage, manager } from "../lib"; + +describe("Gifting", function () { + const signer = new LegacyStarknetKeyPair(); + const claimPubkey = signer.publicKey; + const amount = 1000000000000000n; + const maxFee = 50000000000000n; + const receiver = "0x42"; + + for (const useTxV3 of [false, true]) { + it(`Testing simple claim flow using txV3: ${useTxV3}`, async function () { + await manager.restartDevnetAndClearClassCache(); + // Deploy factory + const claimAccountClassHash = await manager.declareLocalContract("ClaimAccount"); + const factory = await manager.deployContract("GiftFactory", { + unique: true, + constructorCalldata: [claimAccountClassHash, deployer.address], + }); + + // Make a gift + const tokenContract = await manager.tokens.feeTokenContract(useTxV3); + tokenContract.connect(deployer); + factory.connect(deployer); + await tokenContract.approve(factory.address, amount + maxFee); + await factory.deposit(amount, maxFee, tokenContract.address, claimPubkey); + + // Ensure there is a contract for the claim + const claimAddress = await factory.get_claim_address( + deployer.address, + amount, + maxFee, + tokenContract.address, + claimPubkey, + ); + + const constructorArgs = { + sender: deployer.address, + amount: uint256.bnToUint256(amount), + max_fee: maxFee, + token: tokenContract.address, + claim_pubkey: claimPubkey, + }; + const claim = { + factory: factory.address, + class_hash: claimAccountClassHash, + ...constructorArgs, + }; + + const correctAddress = hash.calculateContractAddressFromHash( + 0, + claimAccountClassHash, + CallData.compile(constructorArgs), + factory.address, + ); + expect(claimAddress).to.be.equal(num.toBigInt(correctAddress)); + + // Check balance of the claim contract is correct + await tokenContract.balance_of(claimAddress).should.eventually.equal(amount + maxFee); + // Check balance receiver address == 0 + await tokenContract.balance_of(receiver).should.eventually.equal(0n); + + const txVersion = useTxV3 ? RPC.ETransactionVersion.V3 : RPC.ETransactionVersion.V2; + const claimAccount = new Account(manager, num.toHex(claimAddress), signer, undefined, txVersion); + factory.connect(claimAccount); + if (useTxV3) { + const estimate = await factory.estimateFee.claim_internal(claim, receiver); + const newResourceBounds = { + ...estimate.resourceBounds, + l2_gas: { + ...estimate.resourceBounds.l2_gas, + max_amount: maxFee + 1n, + max_price_per_unit: num.toHexString(4), + }, + }; + await expectRevertWithErrorMessage("gift-acc/insufficient-v3-fee", () => + claimAccount.execute( + [ + { + contractAddress: factory.address, + calldata: [claim, receiver], + entrypoint: "claim_internal", + }, + ], + undefined, + { resourceBounds: newResourceBounds, tip: 1 }, + ), + ); + } else { + await expectRevertWithErrorMessage("gift-acc/insufficient-v1-fee", () => + factory.claim_internal(claim, receiver, { maxFee: maxFee + 1n }), + ); + } + await factory.claim_internal(claim, receiver); + + // Final check + const finalBalance = await tokenContract.balance_of(claimAddress); + expect(finalBalance < maxFee).to.be.true; + await tokenContract.balance_of(receiver).should.eventually.equal(amount); + }); + } + + it(`Test basic validation asserts`, async function () { + await manager.restartDevnetAndClearClassCache(); + // Deploy factory + const claimAccountClassHash = await manager.declareLocalContract("ClaimAccount"); + const factory = await manager.deployContract("GiftFactory", { + unique: true, + constructorCalldata: [claimAccountClassHash, deployer.address], + }); + + const fakeFactory = await manager.deployContract("GiftFactory", { + unique: true, + constructorCalldata: [claimAccountClassHash, deployer.address], + }); + + // Make a gift + const tokenContract = await manager.tokens.feeTokenContract(false); + tokenContract.connect(deployer); + factory.connect(deployer); + await tokenContract.approve(factory.address, amount + maxFee); + await factory.deposit(amount, maxFee, tokenContract.address, claimPubkey); + + // Ensure there is a contract for the claim + const claimAddress = await factory.get_claim_address( + deployer.address, + amount, + maxFee, + tokenContract.address, + claimPubkey, + ); + + const claim = { + factory: factory.address, + class_hash: claimAccountClassHash, + sender: deployer.address, + amount: uint256.bnToUint256(amount), + max_fee: maxFee, + token: tokenContract.address, + claim_pubkey: claimPubkey, + }; + + const constructorCalldata = CallData.compile(claim); + const correctAddress = hash.calculateContractAddressFromHash(0, claimAccountClassHash, constructorCalldata, 0); + expect(claimAddress).to.be.equal(num.toBigInt(correctAddress)); + + // Check balance of the claim contract is correct + await tokenContract.balance_of(claimAddress).should.eventually.equal(amount + maxFee); + // Check balance receiver address == 0 + await tokenContract.balance_of(receiver).should.eventually.equal(0n); + + const claimContract = await manager.loadContract(num.toHex(claimAddress)); + const claimAccount = new Account(manager, claimContract.address, signer, undefined, RPC.ETransactionVersion.V2); + // only protocol + claimContract.connect(claimAccount); + await expectRevertWithErrorMessage("gift-acc/only-protocol", () => claimContract.__validate__([])); + + // cant call another contract + fakeFactory.connect(claimAccount); + await expectRevertWithErrorMessage("gift-acc/invalid-call-to", () => + fakeFactory.claim_internal(claim, receiver, { maxFee: 400000000000000n }), + ); + + // wrong selector + factory.connect(claimAccount); + await expectRevertWithErrorMessage("gift-acc/invalid-call-selector", () => factory.get_claim_class_hash()); + + // multicall + await expectRevertWithErrorMessage("gift-acc/invalid-call-len", () => + claimAccount.execute([ + { + contractAddress: factory.address, + calldata: [claim, receiver], + entrypoint: "claim_internal", + }, + { + contractAddress: factory.address, + calldata: [claim, receiver], + entrypoint: "claim_internal", + }, + ]), + ); + + // double claim + await factory.claim_internal(claim, receiver); + await expectRevertWithErrorMessage("gift-acc/invalid-claim-nonce", () => + claimAccount.execute( + [ + { + contractAddress: factory.address, + calldata: [claim, receiver], + entrypoint: "claim_internal", + }, + ], + undefined, + { skipValidate: false }, + ), + ); + }); + + // TODO Tests: + // - claim_external + // - check with wrong claim data + // - claim without enough fee to full-fill execution + // - cancel + // - get_dust + // - All validate branches + // - What if ERC20 reverts? (check every fn with that) +}); diff --git a/tests-integration/claim_external.test.ts b/tests-integration/claim_external.test.ts new file mode 100644 index 0000000..c00d171 --- /dev/null +++ b/tests-integration/claim_external.test.ts @@ -0,0 +1,50 @@ +import { uint256 } from "starknet"; +import { LegacyStarknetKeyPair, deployer, getClaimExternalData, manager } from "../lib"; + +describe("claim_external", function () { + const useTxV3 = true; + it(`Testing claim_external flow using txV3: ${useTxV3}`, async function () { + await manager.restartDevnetAndClearClassCache(); + // Deploy factory + const claimAccountClassHash = await manager.declareLocalContract("ClaimAccount"); + const factory = await manager.deployContract("GiftFactory", { + unique: true, + constructorCalldata: [claimAccountClassHash, deployer.address], + }); + const signer = new LegacyStarknetKeyPair(); + const claimPubkey = signer.publicKey; + const amount = 1000000000000000n; + const maxFee = 50000000000000n; + const receiver = "0x42"; + + // Make a gift + const tokenContract = await manager.tokens.feeTokenContract(useTxV3); + tokenContract.connect(deployer); + factory.connect(deployer); + await tokenContract.approve(factory.address, amount + maxFee); + await factory.deposit(amount, maxFee, tokenContract.address, claimPubkey); + + const claimAddress = await factory.get_claim_address( + deployer.address, + amount, + maxFee, + tokenContract.address, + claimPubkey, + ); + + const claim = { + factory: factory.address, + class_hash: claimAccountClassHash, + sender: deployer.address, + amount: uint256.bnToUint256(amount), + max_fee: maxFee, + token: tokenContract.address, + claim_pubkey: claimPubkey, + }; + + const claimExternalData = await getClaimExternalData({ receiver }); + const signature = await signer.signMessage(claimExternalData, claimAddress); + + await factory.claim_external(claim, receiver, signature); + }); +}); diff --git a/tests-integration/factory.test.ts b/tests-integration/factory.test.ts new file mode 100644 index 0000000..7b61436 --- /dev/null +++ b/tests-integration/factory.test.ts @@ -0,0 +1,202 @@ +import { expect } from "chai"; +import { Account, RPC, num, uint256 } from "starknet"; +import { LegacyStarknetKeyPair, deployer, expectRevertWithErrorMessage, genericAccount, manager } from "../lib"; + +describe("Factory", function () { + for (const useTxV3 of [false, true]) { + it(`get_dust: ${useTxV3}`, async function () { + await manager.restartDevnetAndClearClassCache(); + // Deploy factory + const claimAccountClassHash = await manager.declareLocalContract("ClaimAccount"); + const factory = await manager.deployContract("GiftFactory", { + unique: true, + constructorCalldata: [claimAccountClassHash, deployer.address], + }); + const signer = new LegacyStarknetKeyPair(); + const claimPubkey = signer.publicKey; + const amount = 1000000000000000n; + const maxFee = 50000000000000n; + const receiver = "0x42"; + const receiverDust = "0x43"; + + // Make a gift + const tokenContract = await manager.tokens.feeTokenContract(useTxV3); + tokenContract.connect(deployer); + factory.connect(deployer); + await tokenContract.approve(factory.address, amount + maxFee); + await factory.deposit(amount, maxFee, tokenContract.address, claimPubkey); + + // Ensure there is a contract for the claim + const claimAddress = await factory.get_claim_address( + deployer.address, + amount, + maxFee, + tokenContract.address, + claimPubkey, + ); + + const claim = { + factory: factory.address, + class_hash: claimAccountClassHash, + sender: deployer.address, + amount: uint256.bnToUint256(amount), + max_fee: maxFee, + token: tokenContract.address, + claim_pubkey: claimPubkey, + }; + + // Check balance of the claim contract is correct + await tokenContract.balance_of(claimAddress).should.eventually.equal(amount + maxFee); + // Check balance receiver address == 0 + await tokenContract.balance_of(receiver).should.eventually.equal(0n); + + const claimContract = await manager.loadContract(num.toHex(claimAddress)); + const txVersion = useTxV3 ? RPC.ETransactionVersion.V3 : RPC.ETransactionVersion.V2; + const claimAccount = new Account(manager, claimContract.address, signer, undefined, txVersion); + factory.connect(claimAccount); + await factory.claim_internal(claim, receiver); + + // Final check + const dustBalance = await tokenContract.balance_of(claimAddress); + expect(dustBalance < maxFee).to.be.true; + await tokenContract.balance_of(receiver).should.eventually.equal(amount); + + // Test dust + await tokenContract.balance_of(receiverDust).should.eventually.equal(0n); + + factory.connect(deployer); + await factory.get_dust(claim, receiverDust); + await tokenContract.balance_of(claimAccount.address).should.eventually.equal(0n); + await tokenContract.balance_of(receiverDust).should.eventually.equal(dustBalance); + }); + } + + it(`Test Cancel Claim`, async function () { + await manager.restartDevnetAndClearClassCache(); + // Deploy factory + const claimAccountClassHash = await manager.declareLocalContract("ClaimAccount"); + const factory = await manager.deployContract("GiftFactory", { + unique: true, + constructorCalldata: [claimAccountClassHash, deployer.address], + }); + const signer = new LegacyStarknetKeyPair(); + const claimPubkey = signer.publicKey; + const amount = 1000000000000000n; + const maxFee = 50000000000000n; + const receiver = "0x42"; + + // Make a gift + const tokenContract = await manager.tokens.feeTokenContract(false); + tokenContract.connect(deployer); + factory.connect(deployer); + await tokenContract.approve(factory.address, amount + maxFee); + await factory.deposit(amount, maxFee, tokenContract.address, claimPubkey); + + // Ensure there is a contract for the claim + const claimAddress = await factory.get_claim_address( + deployer.address, + amount, + maxFee, + tokenContract.address, + claimPubkey, + ); + + const claim = { + factory: factory.address, + class_hash: claimAccountClassHash, + sender: deployer.address, + amount: uint256.bnToUint256(amount), + max_fee: maxFee, + token: tokenContract.address, + claim_pubkey: claimPubkey, + }; + + // Check balance of the claim contract is correct + await tokenContract.balance_of(claimAddress).should.eventually.equal(amount + maxFee); + // Check balance receiver address == 0 + await tokenContract.balance_of(receiver).should.eventually.equal(0n); + + const claimContract = await manager.loadContract(num.toHex(claimAddress)); + const claimAccount = new Account(manager, claimContract.address, signer, undefined, RPC.ETransactionVersion.V2); + + const balanceSenderBefore = await tokenContract.balance_of(deployer.address); + const { transaction_hash } = await factory.cancel(claim); + const txFee = BigInt((await manager.getTransactionReceipt(transaction_hash)).actual_fee.amount); + // Check balance of the sender is correct + await tokenContract + .balance_of(deployer.address) + .should.eventually.equal(balanceSenderBefore + amount + maxFee - txFee); + // Check balance claim address address == 0 + await tokenContract.balance_of(claimAddress).should.eventually.equal(0n); + + factory.connect(claimAccount); + await expectRevertWithErrorMessage("gift-acc/gift-canceled", () => factory.claim_internal(claim, receiver)); + }); + + it(`Test pausable`, async function () { + await manager.restartDevnetAndClearClassCache(); + // Deploy factory + const claimAccountClassHash = await manager.declareLocalContract("ClaimAccount"); + const factory = await manager.deployContract("GiftFactory", { + unique: true, + constructorCalldata: [claimAccountClassHash, deployer.address], + }); + const signer = new LegacyStarknetKeyPair(); + const claimPubkey = signer.publicKey; + const amount = 1000000000000000n; + const maxFee = 50000000000000n; + const receiver = "0x42"; + + // Make a gift + const tokenContract = await manager.tokens.feeTokenContract(false); + tokenContract.connect(deployer); + factory.connect(deployer); + await tokenContract.approve(factory.address, amount + maxFee); + + factory.connect(genericAccount); + await expectRevertWithErrorMessage("Caller is not the owner", () => factory.pause()); + factory.connect(deployer); + await factory.pause(); + await expectRevertWithErrorMessage("Pausable: paused", () => + factory.deposit(amount, maxFee, tokenContract.address, claimPubkey), + ); + + await factory.unpause(); + await factory.deposit(amount, maxFee, tokenContract.address, claimPubkey); + + // Ensure there is a contract for the claim + const claimAddress = await factory.get_claim_address( + deployer.address, + amount, + maxFee, + tokenContract.address, + claimPubkey, + ); + + const claim = { + factory: factory.address, + class_hash: claimAccountClassHash, + sender: deployer.address, + amount: uint256.bnToUint256(amount), + max_fee: maxFee, + token: tokenContract.address, + claim_pubkey: claimPubkey, + }; + + const claimContract = await manager.loadContract(num.toHex(claimAddress)); + const claimAccount = new Account(manager, claimContract.address, signer, undefined, RPC.ETransactionVersion.V2); + + // Check balance of the claim contract is correct + await tokenContract.balance_of(claimAddress).should.eventually.equal(amount + maxFee); + // Check balance receiver address == 0 + await tokenContract.balance_of(receiver).should.eventually.equal(0n); + + factory.connect(claimAccount); + await factory.claim_internal(claim, receiver); + + // Final check + const dustBalance = await tokenContract.balance_of(claimAddress); + expect(dustBalance < maxFee).to.be.true; + await tokenContract.balance_of(receiver).should.eventually.equal(amount); + }); +}); diff --git a/tests/constants.cairo b/tests/constants.cairo new file mode 100644 index 0000000..5f46fda --- /dev/null +++ b/tests/constants.cairo @@ -0,0 +1,25 @@ +use snforge_std::signature::{ + KeyPair, KeyPairTrait, stark_curve::{StarkCurveKeyPairImpl, StarkCurveSignerImpl, StarkCurveVerifierImpl}, +}; +use starknet::ContractAddress; + +fn OWNER() -> ContractAddress { + 'OWNER'.try_into().unwrap() +} + +fn DEPOSITOR() -> ContractAddress { + 'DEPOSITOR'.try_into().unwrap() +} + +fn CLAIMER() -> ContractAddress { + 'CLAIMER'.try_into().unwrap() +} + +fn CLAIM_PUB_KEY() -> felt252 { + let new_owner = KeyPairTrait::from_secret_key('CLAIM'); + new_owner.public_key +} + +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..e969657 --- /dev/null +++ b/tests/lib.cairo @@ -0,0 +1,3 @@ +mod constants; +mod setup; +mod test_gift_factory; diff --git a/tests/setup.cairo b/tests/setup.cairo new file mode 100644 index 0000000..66f253c --- /dev/null +++ b/tests/setup.cairo @@ -0,0 +1,105 @@ +use openzeppelin::token::erc20::interface::{IERC20, IERC20Dispatcher, IERC20DispatcherTrait}; +use openzeppelin::utils::serde::SerializedAppend; + +use snforge_std::{declare, ContractClassTrait, ContractClass, start_cheat_caller_address, stop_cheat_caller_address}; +use starknet::ClassHash; + +use starknet_gifting::contracts::interface::{IGiftFactory, IGiftFactoryDispatcher, IGiftFactoryDispatcherTrait}; +use starknet_gifting::contracts::utils::{STRK_ADDRESS, ETH_ADDRESS}; + +use super::constants::{OWNER, DEPOSITOR, CLAIMER}; + +struct GiftingSetup { + mock_eth: IERC20Dispatcher, + mock_strk: IERC20Dispatcher, + gift_factory: IGiftFactoryDispatcher, + claim_class_hash: ClassHash, +} + +fn deploy_gifting_broken_erc20() -> GiftingSetup { + 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, ETH_ADDRESS()) + .expect('Failed to deploy broken ERC20'); + let broken_erc20 = IERC20Dispatcher { contract_address: broken_erc20_address }; + + // claim contract + let claim_contract = declare("ClaimAccount").expect('Failed to declare claim'); + + // gift factory + let factory_contract = declare("GiftFactory").expect('Failed to declare factory'); + let mut factory_calldata: Array = array![ + claim_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_claim_class_hash() == claim_contract.class_hash, 'Incorrect factory setup'); + + GiftingSetup { + mock_eth: broken_erc20, mock_strk: broken_erc20, gift_factory, claim_class_hash: claim_contract.class_hash + } +} + +fn deploy_gifting_normal() -> GiftingSetup { + let erc20_supply = 1_000_000_000_000_000_000_u256; + + let mock_erc20 = declare("MockERC20").expect('Failed to declare ERC20'); + // mock ETH contract + let mut mock_eth_calldata: Array = array![]; + let name: ByteArray = "ETHER"; + let symbol: ByteArray = "ETH"; + 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, ETH_ADDRESS()).expect('Failed to deploy ETH'); + let mock_eth = IERC20Dispatcher { contract_address: mock_eth_address }; + assert(mock_eth.balance_of(OWNER()) == erc20_supply, 'Failed to mint ETH'); + + // mock STRK contract + let mut mock_eth_calldata: Array = array![]; + let name: ByteArray = "STARK"; + let symbol: ByteArray = "STRK"; + 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_strk_address, _) = mock_erc20 + .deploy_at(@mock_eth_calldata, STRK_ADDRESS()) + .expect('Failed to deploy STRK'); + let mock_strk = IERC20Dispatcher { contract_address: mock_strk_address }; + assert(mock_strk.balance_of(OWNER()) == erc20_supply, 'Failed to mint STRK'); + + // claim contract + let claim_contract = declare("ClaimAccount").expect('Failed to declare claim'); + + // gift factory + let factory_contract = declare("GiftFactory").expect('Failed to declare factory'); + let mut factory_calldata: Array = array![ + claim_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_claim_class_hash() == claim_contract.class_hash, 'Incorrect factory setup'); + + start_cheat_caller_address(mock_eth_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_address, DEPOSITOR()); + start_cheat_caller_address(mock_strk_address, DEPOSITOR()); + mock_eth.approve(factory_contract_address, 1000); + mock_strk.approve(factory_contract_address, 1000); + stop_cheat_caller_address(mock_eth_address); + stop_cheat_caller_address(mock_strk_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, claim_class_hash: claim_contract.class_hash } +} diff --git a/tests/test_gift_factory.cairo b/tests/test_gift_factory.cairo new file mode 100644 index 0000000..8ad8836 --- /dev/null +++ b/tests/test_gift_factory.cairo @@ -0,0 +1,108 @@ +use openzeppelin::security::interface::{IPausable, IPausableDispatcher, IPausableDispatcherTrait}; +use openzeppelin::token::erc20::interface::{IERC20, IERC20Dispatcher, IERC20DispatcherTrait}; +use openzeppelin::utils::deployments::calculate_contract_address_from_deploy_syscall; +use openzeppelin::utils::serde::SerializedAppend; +use snforge_std::{start_cheat_caller_address, stop_cheat_caller_address, get_class_hash}; +use starknet_gifting::contracts::claim_utils::calculate_claim_account_address; +use starknet_gifting::contracts::interface::{ + IGiftFactory, IGiftFactoryDispatcher, IGiftFactoryDispatcherTrait, ClaimData +}; +use super::constants::{DEPOSITOR, CLAIMER, UNAUTHORIZED_ERC20, CLAIM_PUB_KEY}; +use super::setup::{deploy_gifting_normal, deploy_gifting_broken_erc20, GiftingSetup}; + + +#[test] +#[should_panic(expected: ('gift-fac/invalid-token',))] +fn test_deposit_correct_token() { + let GiftingSetup { mock_eth, mock_strk, gift_factory, .. } = deploy_gifting_normal(); + + start_cheat_caller_address(gift_factory.contract_address, DEPOSITOR()); + gift_factory.deposit(10, 5, mock_eth.contract_address, CLAIM_PUB_KEY()); + gift_factory.deposit(20, 10, mock_strk.contract_address, CLAIM_PUB_KEY()); + gift_factory.deposit(10, 5, UNAUTHORIZED_ERC20(), CLAIM_PUB_KEY()); + + assert(mock_eth.balance_of(gift_factory.contract_address) == 10, 'ETH deposit failed'); + assert(mock_strk.balance_of(gift_factory.contract_address) == 20, 'STRK deposit failed'); +} + +#[test] +#[should_panic(expected: ('gift-fac/transfer-failed',))] +fn test_transfer_from_fail() { + let GiftingSetup { mock_eth, gift_factory, .. } = deploy_gifting_broken_erc20(); + + start_cheat_caller_address(gift_factory.contract_address, DEPOSITOR()); + gift_factory.deposit(10, 5, mock_eth.contract_address, CLAIM_PUB_KEY()); +} + +#[test] +#[should_panic(expected: ('gift-fac/fee-too-high',))] +fn test_deposit_max_fee_same_as_amount() { + let GiftingSetup { mock_eth, gift_factory, .. } = deploy_gifting_normal(); + start_cheat_caller_address(gift_factory.contract_address, DEPOSITOR()); + gift_factory.deposit(10, 10, mock_eth.contract_address, CLAIM_PUB_KEY()); +} + +#[test] +#[should_panic(expected: ('gift-fac/fee-too-high',))] +fn test_deposit_max_fee_too_high() { + let GiftingSetup { mock_eth, gift_factory, .. } = deploy_gifting_normal(); + start_cheat_caller_address(gift_factory.contract_address, DEPOSITOR()); + gift_factory.deposit(10, 12, mock_eth.contract_address, CLAIM_PUB_KEY()); +} + +#[test] +fn test_claim_account_deployed() { + let GiftingSetup { mock_eth, gift_factory, claim_class_hash, .. } = deploy_gifting_normal(); + let amount = 10; + let max_fee = 5; + + let claim_data = ClaimData { + factory: gift_factory.contract_address, + class_hash: claim_class_hash, + sender: DEPOSITOR(), + amount, + max_fee, + token: mock_eth.contract_address, + claim_pubkey: CLAIM_PUB_KEY(), + }; + + let calculated_claim_address = calculate_claim_account_address(claim_data); + + start_cheat_caller_address(gift_factory.contract_address, DEPOSITOR()); + gift_factory.deposit(amount, max_fee, mock_eth.contract_address, CLAIM_PUB_KEY()); + + // Check that the claim account was deployed by getting class hash at that address + // un-deployed claim account should return 0 + let fetched_claim_class_hash = get_class_hash(calculated_claim_address); + assert(claim_class_hash == fetched_claim_class_hash, 'Claim account not deployed'); + assert(claim_class_hash == gift_factory.get_claim_class_hash(), 'Incorrect claim class hash'); + + // Check that factory calculates claim address correctly + let get_claim_address = gift_factory + .get_claim_address( + claim_data.sender, claim_data.amount, claim_data.max_fee, claim_data.token, claim_data.claim_pubkey + ); + assert!(calculated_claim_address == get_claim_address, "Claim address not calculated correctly"); +} + +#[test] +#[should_panic(expected: ('Caller is not the owner',))] +fn test_get_dust_only_owner() { + let GiftingSetup { mock_eth, gift_factory, claim_class_hash, .. } = deploy_gifting_normal(); + let amount = 10; + let max_fee = 5; + start_cheat_caller_address(gift_factory.contract_address, DEPOSITOR()); + gift_factory.deposit(10, 5, mock_eth.contract_address, CLAIM_PUB_KEY()); + + let claim_data = ClaimData { + factory: gift_factory.contract_address, + class_hash: claim_class_hash, + sender: DEPOSITOR(), + amount, + max_fee, + token: mock_eth.contract_address, + claim_pubkey: CLAIM_PUB_KEY(), + }; + gift_factory.get_dust(claim_data, CLAIMER()); +} + 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== From 363fc0388dd7598bc0df9fb8d89f39560be150a9 Mon Sep 17 00:00:00 2001 From: Leonard Paturel Date: Wed, 5 Jun 2024 16:14:32 +0100 Subject: [PATCH 02/29] refactor --- tests-integration/account.test.ts | 148 ++++++------------------------ tests-integration/setupClaim.ts | 84 +++++++++++++++++ 2 files changed, 112 insertions(+), 120 deletions(-) create mode 100644 tests-integration/setupClaim.ts diff --git a/tests-integration/account.test.ts b/tests-integration/account.test.ts index d108b26..312406f 100644 --- a/tests-integration/account.test.ts +++ b/tests-integration/account.test.ts @@ -1,76 +1,26 @@ import { expect } from "chai"; -import { Account, CallData, RPC, hash, num, uint256 } from "starknet"; -import { LegacyStarknetKeyPair, deployer, expectRevertWithErrorMessage, manager } from "../lib"; +import { num } from "starknet"; +import { deployer, expectRevertWithErrorMessage, manager } from "../lib"; +import { GIFT_AMOUNT, GIFT_MAX_FEE, GIFT_RECEIVER, setupClaim } from "./setupClaim"; describe("Gifting", function () { - const signer = new LegacyStarknetKeyPair(); - const claimPubkey = signer.publicKey; - const amount = 1000000000000000n; - const maxFee = 50000000000000n; - const receiver = "0x42"; + let claimAccountClassHash: string; + before(async () => { + // declare claim account + claimAccountClassHash = await manager.declareLocalContract("ClaimAccount"); + }); for (const useTxV3 of [false, true]) { it(`Testing simple claim flow using txV3: ${useTxV3}`, async function () { - await manager.restartDevnetAndClearClassCache(); + const { factory, claimAccount, claim, tokenContract } = await setupClaim(useTxV3); // Deploy factory - const claimAccountClassHash = await manager.declareLocalContract("ClaimAccount"); - const factory = await manager.deployContract("GiftFactory", { - unique: true, - constructorCalldata: [claimAccountClassHash, deployer.address], - }); - - // Make a gift - const tokenContract = await manager.tokens.feeTokenContract(useTxV3); - tokenContract.connect(deployer); - factory.connect(deployer); - await tokenContract.approve(factory.address, amount + maxFee); - await factory.deposit(amount, maxFee, tokenContract.address, claimPubkey); - - // Ensure there is a contract for the claim - const claimAddress = await factory.get_claim_address( - deployer.address, - amount, - maxFee, - tokenContract.address, - claimPubkey, - ); - - const constructorArgs = { - sender: deployer.address, - amount: uint256.bnToUint256(amount), - max_fee: maxFee, - token: tokenContract.address, - claim_pubkey: claimPubkey, - }; - const claim = { - factory: factory.address, - class_hash: claimAccountClassHash, - ...constructorArgs, - }; - - const correctAddress = hash.calculateContractAddressFromHash( - 0, - claimAccountClassHash, - CallData.compile(constructorArgs), - factory.address, - ); - expect(claimAddress).to.be.equal(num.toBigInt(correctAddress)); - - // Check balance of the claim contract is correct - await tokenContract.balance_of(claimAddress).should.eventually.equal(amount + maxFee); - // Check balance receiver address == 0 - await tokenContract.balance_of(receiver).should.eventually.equal(0n); - - const txVersion = useTxV3 ? RPC.ETransactionVersion.V3 : RPC.ETransactionVersion.V2; - const claimAccount = new Account(manager, num.toHex(claimAddress), signer, undefined, txVersion); - factory.connect(claimAccount); if (useTxV3) { - const estimate = await factory.estimateFee.claim_internal(claim, receiver); + const estimate = await factory.estimateFee.claim_internal(claim, GIFT_RECEIVER); const newResourceBounds = { ...estimate.resourceBounds, l2_gas: { ...estimate.resourceBounds.l2_gas, - max_amount: maxFee + 1n, + max_amount: GIFT_MAX_FEE + 1n, max_price_per_unit: num.toHexString(4), }, }; @@ -79,7 +29,7 @@ describe("Gifting", function () { [ { contractAddress: factory.address, - calldata: [claim, receiver], + calldata: [claim, GIFT_RECEIVER], entrypoint: "claim_internal", }, ], @@ -89,77 +39,35 @@ describe("Gifting", function () { ); } else { await expectRevertWithErrorMessage("gift-acc/insufficient-v1-fee", () => - factory.claim_internal(claim, receiver, { maxFee: maxFee + 1n }), + factory.claim_internal(claim, GIFT_RECEIVER, { maxFee: GIFT_MAX_FEE + 1n }), ); } - await factory.claim_internal(claim, receiver); + await factory.claim_internal(claim, GIFT_RECEIVER); // Final check - const finalBalance = await tokenContract.balance_of(claimAddress); - expect(finalBalance < maxFee).to.be.true; - await tokenContract.balance_of(receiver).should.eventually.equal(amount); + const finalBalance = await tokenContract.balance_of(claimAccount.address); + expect(finalBalance < GIFT_MAX_FEE).to.be.true; + await tokenContract.balance_of(GIFT_RECEIVER).should.eventually.equal(GIFT_AMOUNT); }); } it(`Test basic validation asserts`, async function () { - await manager.restartDevnetAndClearClassCache(); - // Deploy factory - const claimAccountClassHash = await manager.declareLocalContract("ClaimAccount"); - const factory = await manager.deployContract("GiftFactory", { - unique: true, - constructorCalldata: [claimAccountClassHash, deployer.address], - }); - - const fakeFactory = await manager.deployContract("GiftFactory", { - unique: true, - constructorCalldata: [claimAccountClassHash, deployer.address], - }); - - // Make a gift - const tokenContract = await manager.tokens.feeTokenContract(false); - tokenContract.connect(deployer); - factory.connect(deployer); - await tokenContract.approve(factory.address, amount + maxFee); - await factory.deposit(amount, maxFee, tokenContract.address, claimPubkey); - - // Ensure there is a contract for the claim - const claimAddress = await factory.get_claim_address( - deployer.address, - amount, - maxFee, - tokenContract.address, - claimPubkey, - ); + const { factory, claimAccount, claim } = await setupClaim(); - const claim = { - factory: factory.address, - class_hash: claimAccountClassHash, - sender: deployer.address, - amount: uint256.bnToUint256(amount), - max_fee: maxFee, - token: tokenContract.address, - claim_pubkey: claimPubkey, - }; + const claimContract = await manager.loadContract(num.toHex(claimAccount.address)); - const constructorCalldata = CallData.compile(claim); - const correctAddress = hash.calculateContractAddressFromHash(0, claimAccountClassHash, constructorCalldata, 0); - expect(claimAddress).to.be.equal(num.toBigInt(correctAddress)); - - // Check balance of the claim contract is correct - await tokenContract.balance_of(claimAddress).should.eventually.equal(amount + maxFee); - // Check balance receiver address == 0 - await tokenContract.balance_of(receiver).should.eventually.equal(0n); - - const claimContract = await manager.loadContract(num.toHex(claimAddress)); - const claimAccount = new Account(manager, claimContract.address, signer, undefined, RPC.ETransactionVersion.V2); // only protocol claimContract.connect(claimAccount); await expectRevertWithErrorMessage("gift-acc/only-protocol", () => claimContract.__validate__([])); // cant call another contract + const fakeFactory = await manager.deployContract("GiftFactory", { + unique: true, + constructorCalldata: [claimAccountClassHash, deployer.address], + }); fakeFactory.connect(claimAccount); await expectRevertWithErrorMessage("gift-acc/invalid-call-to", () => - fakeFactory.claim_internal(claim, receiver, { maxFee: 400000000000000n }), + fakeFactory.claim_internal(claim, GIFT_RECEIVER, { maxFee: 400000000000000n }), ); // wrong selector @@ -171,25 +79,25 @@ describe("Gifting", function () { claimAccount.execute([ { contractAddress: factory.address, - calldata: [claim, receiver], + calldata: [claim, GIFT_RECEIVER], entrypoint: "claim_internal", }, { contractAddress: factory.address, - calldata: [claim, receiver], + calldata: [claim, GIFT_RECEIVER], entrypoint: "claim_internal", }, ]), ); // double claim - await factory.claim_internal(claim, receiver); + await factory.claim_internal(claim, GIFT_RECEIVER); await expectRevertWithErrorMessage("gift-acc/invalid-claim-nonce", () => claimAccount.execute( [ { contractAddress: factory.address, - calldata: [claim, receiver], + calldata: [claim, GIFT_RECEIVER], entrypoint: "claim_internal", }, ], diff --git a/tests-integration/setupClaim.ts b/tests-integration/setupClaim.ts new file mode 100644 index 0000000..b0baa61 --- /dev/null +++ b/tests-integration/setupClaim.ts @@ -0,0 +1,84 @@ +import { expect } from "chai"; +import { Account, CallData, Contract, RPC, Uint256, hash, num, uint256 } from "starknet"; +import { LegacyStarknetKeyPair, deployer, manager } from "../lib"; + +export const GIFT_SIGNER = new LegacyStarknetKeyPair(); +export const CLAIM_PUB_KEY = GIFT_SIGNER.publicKey; +export const GIFT_AMOUNT = 1000000000000000n; +export const GIFT_MAX_FEE = 50000000000000n; +export const GIFT_RECEIVER = "0x42"; + +interface AccountConstructorArguments { + sender: string; + amount: Uint256; + max_fee: bigint; + token: string; + claim_pubkey: bigint; +} + +interface Claim extends AccountConstructorArguments { + factory: string; + class_hash: string; +} + +export async function setupClaim(useTxV3 = false): Promise<{ + factory: Contract; + claimAccount: Account; + claim: Claim; + tokenContract: Contract; +}> { + // claim account class hash is read from cache + const claimAccountClassHash = await manager.declareLocalContract("ClaimAccount"); + const factory = await manager.deployContract("GiftFactory", { + unique: true, + constructorCalldata: [claimAccountClassHash, deployer.address], + }); + + // Make a gift + const tokenContract = await manager.tokens.feeTokenContract(useTxV3); + tokenContract.connect(deployer); + factory.connect(deployer); + await tokenContract.approve(factory.address, GIFT_AMOUNT + GIFT_MAX_FEE); + await factory.deposit(GIFT_AMOUNT, GIFT_MAX_FEE, tokenContract.address, CLAIM_PUB_KEY); + + // Ensure there is a contract for the claim + const claimAddress = await factory.get_claim_address( + deployer.address, + GIFT_AMOUNT, + GIFT_MAX_FEE, + tokenContract.address, + CLAIM_PUB_KEY, + ); + + const constructorArgs: AccountConstructorArguments = { + sender: deployer.address, + amount: uint256.bnToUint256(GIFT_AMOUNT), + max_fee: GIFT_MAX_FEE, + token: tokenContract.address, + claim_pubkey: CLAIM_PUB_KEY, + }; + + const claim: Claim = { + factory: factory.address, + class_hash: claimAccountClassHash, + ...constructorArgs, + }; + + const correctAddress = hash.calculateContractAddressFromHash( + 0, + claimAccountClassHash, + CallData.compile({ constructorArgs }), + factory.address, + ); + expect(claimAddress).to.be.equal(num.toBigInt(correctAddress)); + + // Check balance of the claim contract is correct + await tokenContract.balance_of(claimAddress).should.eventually.equal(GIFT_AMOUNT + GIFT_MAX_FEE); + // Check balance receiver address == 0 + await tokenContract.balance_of(GIFT_RECEIVER).should.eventually.equal(0n); + + const txVersion = useTxV3 ? RPC.ETransactionVersion.V3 : RPC.ETransactionVersion.V2; + const claimAccount = new Account(manager, num.toHex(claimAddress), GIFT_SIGNER, undefined, txVersion); + factory.connect(claimAccount); + return { factory, claimAccount, claim, tokenContract }; +} From d313bd915d0ec7230d870f0aa5939158f745fcfe Mon Sep 17 00:00:00 2001 From: Leonard Paturel Date: Wed, 5 Jun 2024 16:44:21 +0100 Subject: [PATCH 03/29] refactor tests --- tests-integration/account.test.ts | 39 ++++--- tests-integration/claim_external.test.ts | 58 +++------- tests-integration/factory.test.ts | 140 +++++------------------ tests-integration/setupClaim.ts | 18 ++- 4 files changed, 74 insertions(+), 181 deletions(-) diff --git a/tests-integration/account.test.ts b/tests-integration/account.test.ts index 312406f..aab82f1 100644 --- a/tests-integration/account.test.ts +++ b/tests-integration/account.test.ts @@ -1,7 +1,7 @@ import { expect } from "chai"; import { num } from "starknet"; import { deployer, expectRevertWithErrorMessage, manager } from "../lib"; -import { GIFT_AMOUNT, GIFT_MAX_FEE, GIFT_RECEIVER, setupClaim } from "./setupClaim"; +import { GIFT_AMOUNT, GIFT_MAX_FEE, setupClaim } from "./setupClaim"; describe("Gifting", function () { let claimAccountClassHash: string; @@ -12,10 +12,19 @@ describe("Gifting", function () { for (const useTxV3 of [false, true]) { it(`Testing simple claim flow using txV3: ${useTxV3}`, async function () { - const { factory, claimAccount, claim, tokenContract } = await setupClaim(useTxV3); - // Deploy factory + const { factory, claimAccount, claim, tokenContract, receiver } = await setupClaim(useTxV3); + await factory.claim_internal(claim, receiver); + + // Final check + const finalBalance = await tokenContract.balance_of(claimAccount.address); + expect(finalBalance < GIFT_MAX_FEE).to.be.true; + await tokenContract.balance_of(receiver).should.eventually.equal(GIFT_AMOUNT); + }); + + it(`Test max fee too high`, async function () { + const { factory, claimAccount, claim, receiver } = await setupClaim(useTxV3); if (useTxV3) { - const estimate = await factory.estimateFee.claim_internal(claim, GIFT_RECEIVER); + const estimate = await factory.estimateFee.claim_internal(claim, receiver); const newResourceBounds = { ...estimate.resourceBounds, l2_gas: { @@ -29,7 +38,7 @@ describe("Gifting", function () { [ { contractAddress: factory.address, - calldata: [claim, GIFT_RECEIVER], + calldata: [claim, receiver], entrypoint: "claim_internal", }, ], @@ -39,20 +48,14 @@ describe("Gifting", function () { ); } else { await expectRevertWithErrorMessage("gift-acc/insufficient-v1-fee", () => - factory.claim_internal(claim, GIFT_RECEIVER, { maxFee: GIFT_MAX_FEE + 1n }), + factory.claim_internal(claim, receiver, { maxFee: GIFT_MAX_FEE + 1n }), ); } - await factory.claim_internal(claim, GIFT_RECEIVER); - - // Final check - const finalBalance = await tokenContract.balance_of(claimAccount.address); - expect(finalBalance < GIFT_MAX_FEE).to.be.true; - await tokenContract.balance_of(GIFT_RECEIVER).should.eventually.equal(GIFT_AMOUNT); }); } it(`Test basic validation asserts`, async function () { - const { factory, claimAccount, claim } = await setupClaim(); + const { factory, claimAccount, claim, receiver } = await setupClaim(); const claimContract = await manager.loadContract(num.toHex(claimAccount.address)); @@ -67,7 +70,7 @@ describe("Gifting", function () { }); fakeFactory.connect(claimAccount); await expectRevertWithErrorMessage("gift-acc/invalid-call-to", () => - fakeFactory.claim_internal(claim, GIFT_RECEIVER, { maxFee: 400000000000000n }), + fakeFactory.claim_internal(claim, receiver, { maxFee: 400000000000000n }), ); // wrong selector @@ -79,25 +82,25 @@ describe("Gifting", function () { claimAccount.execute([ { contractAddress: factory.address, - calldata: [claim, GIFT_RECEIVER], + calldata: [claim, receiver], entrypoint: "claim_internal", }, { contractAddress: factory.address, - calldata: [claim, GIFT_RECEIVER], + calldata: [claim, receiver], entrypoint: "claim_internal", }, ]), ); // double claim - await factory.claim_internal(claim, GIFT_RECEIVER); + await factory.claim_internal(claim, receiver); await expectRevertWithErrorMessage("gift-acc/invalid-claim-nonce", () => claimAccount.execute( [ { contractAddress: factory.address, - calldata: [claim, GIFT_RECEIVER], + calldata: [claim, receiver], entrypoint: "claim_internal", }, ], diff --git a/tests-integration/claim_external.test.ts b/tests-integration/claim_external.test.ts index c00d171..2d504bf 100644 --- a/tests-integration/claim_external.test.ts +++ b/tests-integration/claim_external.test.ts @@ -1,50 +1,20 @@ -import { uint256 } from "starknet"; -import { LegacyStarknetKeyPair, deployer, getClaimExternalData, manager } from "../lib"; +import { deployer, getClaimExternalData, manager } from "../lib"; +import { GIFT_SIGNER, setupClaim } from "./setupClaim"; describe("claim_external", function () { - const useTxV3 = true; - it(`Testing claim_external flow using txV3: ${useTxV3}`, async function () { - await manager.restartDevnetAndClearClassCache(); - // Deploy factory - const claimAccountClassHash = await manager.declareLocalContract("ClaimAccount"); - const factory = await manager.deployContract("GiftFactory", { - unique: true, - constructorCalldata: [claimAccountClassHash, deployer.address], - }); - const signer = new LegacyStarknetKeyPair(); - const claimPubkey = signer.publicKey; - const amount = 1000000000000000n; - const maxFee = 50000000000000n; - const receiver = "0x42"; - - // Make a gift - const tokenContract = await manager.tokens.feeTokenContract(useTxV3); - tokenContract.connect(deployer); - factory.connect(deployer); - await tokenContract.approve(factory.address, amount + maxFee); - await factory.deposit(amount, maxFee, tokenContract.address, claimPubkey); - - const claimAddress = await factory.get_claim_address( - deployer.address, - amount, - maxFee, - tokenContract.address, - claimPubkey, - ); + before(async () => { + await manager.declareLocalContract("ClaimAccount"); + }); - const claim = { - factory: factory.address, - class_hash: claimAccountClassHash, - sender: deployer.address, - amount: uint256.bnToUint256(amount), - max_fee: maxFee, - token: tokenContract.address, - claim_pubkey: claimPubkey, - }; + for (const useTxV3 of [false, true]) { + it(`Testing claim_external flow using txV3: ${useTxV3}`, async function () { + const { factory, claimAccount, claim, receiver } = await setupClaim(useTxV3); - const claimExternalData = await getClaimExternalData({ receiver }); - const signature = await signer.signMessage(claimExternalData, claimAddress); + const claimExternalData = await getClaimExternalData({ receiver }); + const signature = await GIFT_SIGNER.signMessage(claimExternalData, claimAccount.address); - await factory.claim_external(claim, receiver, signature); - }); + factory.connect(deployer); + await factory.claim_external(claim, receiver, signature); + }); + } }); diff --git a/tests-integration/factory.test.ts b/tests-integration/factory.test.ts index 7b61436..1a9d894 100644 --- a/tests-integration/factory.test.ts +++ b/tests-integration/factory.test.ts @@ -1,65 +1,26 @@ import { expect } from "chai"; import { Account, RPC, num, uint256 } from "starknet"; import { LegacyStarknetKeyPair, deployer, expectRevertWithErrorMessage, genericAccount, manager } from "../lib"; +import { GIFT_AMOUNT, GIFT_MAX_FEE, setupClaim } from "./setupClaim"; describe("Factory", function () { + let claimAccountClassHash: string; + before(async () => { + claimAccountClassHash = await manager.declareLocalContract("ClaimAccount"); + }); + for (const useTxV3 of [false, true]) { it(`get_dust: ${useTxV3}`, async function () { - await manager.restartDevnetAndClearClassCache(); - // Deploy factory - const claimAccountClassHash = await manager.declareLocalContract("ClaimAccount"); - const factory = await manager.deployContract("GiftFactory", { - unique: true, - constructorCalldata: [claimAccountClassHash, deployer.address], - }); - const signer = new LegacyStarknetKeyPair(); - const claimPubkey = signer.publicKey; - const amount = 1000000000000000n; - const maxFee = 50000000000000n; - const receiver = "0x42"; + const { factory, tokenContract, claimAccount, claim, receiver } = await setupClaim(useTxV3); + const receiverDust = "0x43"; - // Make a gift - const tokenContract = await manager.tokens.feeTokenContract(useTxV3); - tokenContract.connect(deployer); - factory.connect(deployer); - await tokenContract.approve(factory.address, amount + maxFee); - await factory.deposit(amount, maxFee, tokenContract.address, claimPubkey); - - // Ensure there is a contract for the claim - const claimAddress = await factory.get_claim_address( - deployer.address, - amount, - maxFee, - tokenContract.address, - claimPubkey, - ); - - const claim = { - factory: factory.address, - class_hash: claimAccountClassHash, - sender: deployer.address, - amount: uint256.bnToUint256(amount), - max_fee: maxFee, - token: tokenContract.address, - claim_pubkey: claimPubkey, - }; - - // Check balance of the claim contract is correct - await tokenContract.balance_of(claimAddress).should.eventually.equal(amount + maxFee); - // Check balance receiver address == 0 - await tokenContract.balance_of(receiver).should.eventually.equal(0n); - - const claimContract = await manager.loadContract(num.toHex(claimAddress)); - const txVersion = useTxV3 ? RPC.ETransactionVersion.V3 : RPC.ETransactionVersion.V2; - const claimAccount = new Account(manager, claimContract.address, signer, undefined, txVersion); - factory.connect(claimAccount); await factory.claim_internal(claim, receiver); // Final check - const dustBalance = await tokenContract.balance_of(claimAddress); - expect(dustBalance < maxFee).to.be.true; - await tokenContract.balance_of(receiver).should.eventually.equal(amount); + const dustBalance = await tokenContract.balance_of(claimAccount.address); + expect(dustBalance < GIFT_MAX_FEE).to.be.true; + await tokenContract.balance_of(receiver).should.eventually.equal(GIFT_AMOUNT); // Test dust await tokenContract.balance_of(receiverDust).should.eventually.equal(0n); @@ -72,52 +33,7 @@ describe("Factory", function () { } it(`Test Cancel Claim`, async function () { - await manager.restartDevnetAndClearClassCache(); - // Deploy factory - const claimAccountClassHash = await manager.declareLocalContract("ClaimAccount"); - const factory = await manager.deployContract("GiftFactory", { - unique: true, - constructorCalldata: [claimAccountClassHash, deployer.address], - }); - const signer = new LegacyStarknetKeyPair(); - const claimPubkey = signer.publicKey; - const amount = 1000000000000000n; - const maxFee = 50000000000000n; - const receiver = "0x42"; - - // Make a gift - const tokenContract = await manager.tokens.feeTokenContract(false); - tokenContract.connect(deployer); - factory.connect(deployer); - await tokenContract.approve(factory.address, amount + maxFee); - await factory.deposit(amount, maxFee, tokenContract.address, claimPubkey); - - // Ensure there is a contract for the claim - const claimAddress = await factory.get_claim_address( - deployer.address, - amount, - maxFee, - tokenContract.address, - claimPubkey, - ); - - const claim = { - factory: factory.address, - class_hash: claimAccountClassHash, - sender: deployer.address, - amount: uint256.bnToUint256(amount), - max_fee: maxFee, - token: tokenContract.address, - claim_pubkey: claimPubkey, - }; - - // Check balance of the claim contract is correct - await tokenContract.balance_of(claimAddress).should.eventually.equal(amount + maxFee); - // Check balance receiver address == 0 - await tokenContract.balance_of(receiver).should.eventually.equal(0n); - - const claimContract = await manager.loadContract(num.toHex(claimAddress)); - const claimAccount = new Account(manager, claimContract.address, signer, undefined, RPC.ETransactionVersion.V2); + const { factory, tokenContract, claimAccount, claim, receiver } = await setupClaim(); const balanceSenderBefore = await tokenContract.balance_of(deployer.address); const { transaction_hash } = await factory.cancel(claim); @@ -125,50 +41,48 @@ describe("Factory", function () { // Check balance of the sender is correct await tokenContract .balance_of(deployer.address) - .should.eventually.equal(balanceSenderBefore + amount + maxFee - txFee); + .should.eventually.equal(balanceSenderBefore + GIFT_AMOUNT + GIFT_MAX_FEE - txFee); // Check balance claim address address == 0 - await tokenContract.balance_of(claimAddress).should.eventually.equal(0n); + await tokenContract.balance_of(claimAccount.address).should.eventually.equal(0n); factory.connect(claimAccount); await expectRevertWithErrorMessage("gift-acc/gift-canceled", () => factory.claim_internal(claim, receiver)); }); it(`Test pausable`, async function () { - await manager.restartDevnetAndClearClassCache(); // Deploy factory - const claimAccountClassHash = await manager.declareLocalContract("ClaimAccount"); const factory = await manager.deployContract("GiftFactory", { unique: true, constructorCalldata: [claimAccountClassHash, deployer.address], }); const signer = new LegacyStarknetKeyPair(); const claimPubkey = signer.publicKey; - const amount = 1000000000000000n; - const maxFee = 50000000000000n; - const receiver = "0x42"; + const GIFT_AMOUNT = 1000000000000000n; + const GIFT_MAX_FEE = 50000000000000n; + const receiver = "0x45"; // Make a gift const tokenContract = await manager.tokens.feeTokenContract(false); tokenContract.connect(deployer); factory.connect(deployer); - await tokenContract.approve(factory.address, amount + maxFee); + await tokenContract.approve(factory.address, GIFT_AMOUNT + GIFT_MAX_FEE); factory.connect(genericAccount); await expectRevertWithErrorMessage("Caller is not the owner", () => factory.pause()); factory.connect(deployer); await factory.pause(); await expectRevertWithErrorMessage("Pausable: paused", () => - factory.deposit(amount, maxFee, tokenContract.address, claimPubkey), + factory.deposit(GIFT_AMOUNT, GIFT_MAX_FEE, tokenContract.address, claimPubkey), ); await factory.unpause(); - await factory.deposit(amount, maxFee, tokenContract.address, claimPubkey); + await factory.deposit(GIFT_AMOUNT, GIFT_MAX_FEE, tokenContract.address, claimPubkey); // Ensure there is a contract for the claim const claimAddress = await factory.get_claim_address( deployer.address, - amount, - maxFee, + GIFT_AMOUNT, + GIFT_MAX_FEE, tokenContract.address, claimPubkey, ); @@ -177,8 +91,8 @@ describe("Factory", function () { factory: factory.address, class_hash: claimAccountClassHash, sender: deployer.address, - amount: uint256.bnToUint256(amount), - max_fee: maxFee, + GIFT_AMOUNT: uint256.bnToUint256(GIFT_AMOUNT), + max_fee: GIFT_MAX_FEE, token: tokenContract.address, claim_pubkey: claimPubkey, }; @@ -187,7 +101,7 @@ describe("Factory", function () { const claimAccount = new Account(manager, claimContract.address, signer, undefined, RPC.ETransactionVersion.V2); // Check balance of the claim contract is correct - await tokenContract.balance_of(claimAddress).should.eventually.equal(amount + maxFee); + await tokenContract.balance_of(claimAddress).should.eventually.equal(GIFT_AMOUNT + GIFT_MAX_FEE); // Check balance receiver address == 0 await tokenContract.balance_of(receiver).should.eventually.equal(0n); @@ -196,7 +110,7 @@ describe("Factory", function () { // Final check const dustBalance = await tokenContract.balance_of(claimAddress); - expect(dustBalance < maxFee).to.be.true; - await tokenContract.balance_of(receiver).should.eventually.equal(amount); + expect(dustBalance < GIFT_MAX_FEE).to.be.true; + await tokenContract.balance_of(receiver).should.eventually.equal(GIFT_AMOUNT); }); }); diff --git a/tests-integration/setupClaim.ts b/tests-integration/setupClaim.ts index b0baa61..ebeff7e 100644 --- a/tests-integration/setupClaim.ts +++ b/tests-integration/setupClaim.ts @@ -1,12 +1,11 @@ import { expect } from "chai"; -import { Account, CallData, Contract, RPC, Uint256, hash, num, uint256 } from "starknet"; +import { Account, CallData, Contract, RPC, Uint256, ec, encode, hash, num, uint256 } from "starknet"; import { LegacyStarknetKeyPair, deployer, manager } from "../lib"; export const GIFT_SIGNER = new LegacyStarknetKeyPair(); export const CLAIM_PUB_KEY = GIFT_SIGNER.publicKey; export const GIFT_AMOUNT = 1000000000000000n; export const GIFT_MAX_FEE = 50000000000000n; -export const GIFT_RECEIVER = "0x42"; interface AccountConstructorArguments { sender: string; @@ -21,12 +20,19 @@ interface Claim extends AccountConstructorArguments { class_hash: string; } -export async function setupClaim(useTxV3 = false): Promise<{ +export async function setupClaim( + useTxV3 = false, + useRandomReceiver = true, +): Promise<{ factory: Contract; claimAccount: Account; claim: Claim; tokenContract: Contract; + receiver: string; }> { + // static receiver for gas profiling + const receiver = useRandomReceiver ? `0x${encode.buf2hex(ec.starkCurve.utils.randomPrivateKey())}` : "0x42"; + // claim account class hash is read from cache const claimAccountClassHash = await manager.declareLocalContract("ClaimAccount"); const factory = await manager.deployContract("GiftFactory", { @@ -59,9 +65,9 @@ export async function setupClaim(useTxV3 = false): Promise<{ }; const claim: Claim = { + ...constructorArgs, factory: factory.address, class_hash: claimAccountClassHash, - ...constructorArgs, }; const correctAddress = hash.calculateContractAddressFromHash( @@ -75,10 +81,10 @@ export async function setupClaim(useTxV3 = false): Promise<{ // Check balance of the claim contract is correct await tokenContract.balance_of(claimAddress).should.eventually.equal(GIFT_AMOUNT + GIFT_MAX_FEE); // Check balance receiver address == 0 - await tokenContract.balance_of(GIFT_RECEIVER).should.eventually.equal(0n); + await tokenContract.balance_of(receiver).should.eventually.equal(0n); const txVersion = useTxV3 ? RPC.ETransactionVersion.V3 : RPC.ETransactionVersion.V2; const claimAccount = new Account(manager, num.toHex(claimAddress), GIFT_SIGNER, undefined, txVersion); factory.connect(claimAccount); - return { factory, claimAccount, claim, tokenContract }; + return { factory, claimAccount, claim, tokenContract, receiver }; } From 60e09cc5a8426c7df79f5752be7ce7bff5ae4a8e Mon Sep 17 00:00:00 2001 From: Leonard Paturel Date: Wed, 5 Jun 2024 17:23:23 +0100 Subject: [PATCH 04/29] fix tests --- tests-integration/claim_external.test.ts | 6 +++--- tests-integration/factory.test.ts | 6 +++--- tests-integration/setupClaim.ts | 21 +++++++++++---------- 3 files changed, 17 insertions(+), 16 deletions(-) diff --git a/tests-integration/claim_external.test.ts b/tests-integration/claim_external.test.ts index 2d504bf..ff1abff 100644 --- a/tests-integration/claim_external.test.ts +++ b/tests-integration/claim_external.test.ts @@ -1,5 +1,5 @@ import { deployer, getClaimExternalData, manager } from "../lib"; -import { GIFT_SIGNER, setupClaim } from "./setupClaim"; +import { setupClaim } from "./setupClaim"; describe("claim_external", function () { before(async () => { @@ -8,10 +8,10 @@ describe("claim_external", function () { for (const useTxV3 of [false, true]) { it(`Testing claim_external flow using txV3: ${useTxV3}`, async function () { - const { factory, claimAccount, claim, receiver } = await setupClaim(useTxV3); + const { factory, claimAccount, claim, receiver, giftSigner } = await setupClaim(useTxV3); const claimExternalData = await getClaimExternalData({ receiver }); - const signature = await GIFT_SIGNER.signMessage(claimExternalData, claimAccount.address); + const signature = await giftSigner.signMessage(claimExternalData, claimAccount.address); factory.connect(deployer); await factory.claim_external(claim, receiver, signature); diff --git a/tests-integration/factory.test.ts b/tests-integration/factory.test.ts index 1a9d894..9221438 100644 --- a/tests-integration/factory.test.ts +++ b/tests-integration/factory.test.ts @@ -12,8 +12,7 @@ describe("Factory", function () { for (const useTxV3 of [false, true]) { it(`get_dust: ${useTxV3}`, async function () { const { factory, tokenContract, claimAccount, claim, receiver } = await setupClaim(useTxV3); - - const receiverDust = "0x43"; + const receiverDust = `0x${Math.floor(Math.random() * 10)}`; await factory.claim_internal(claim, receiver); @@ -36,6 +35,7 @@ describe("Factory", function () { const { factory, tokenContract, claimAccount, claim, receiver } = await setupClaim(); const balanceSenderBefore = await tokenContract.balance_of(deployer.address); + factory.connect(deployer); const { transaction_hash } = await factory.cancel(claim); const txFee = BigInt((await manager.getTransactionReceipt(transaction_hash)).actual_fee.amount); // Check balance of the sender is correct @@ -91,7 +91,7 @@ describe("Factory", function () { factory: factory.address, class_hash: claimAccountClassHash, sender: deployer.address, - GIFT_AMOUNT: uint256.bnToUint256(GIFT_AMOUNT), + amount: uint256.bnToUint256(GIFT_AMOUNT), max_fee: GIFT_MAX_FEE, token: tokenContract.address, claim_pubkey: claimPubkey, diff --git a/tests-integration/setupClaim.ts b/tests-integration/setupClaim.ts index ebeff7e..deb50e1 100644 --- a/tests-integration/setupClaim.ts +++ b/tests-integration/setupClaim.ts @@ -2,8 +2,6 @@ import { expect } from "chai"; import { Account, CallData, Contract, RPC, Uint256, ec, encode, hash, num, uint256 } from "starknet"; import { LegacyStarknetKeyPair, deployer, manager } from "../lib"; -export const GIFT_SIGNER = new LegacyStarknetKeyPair(); -export const CLAIM_PUB_KEY = GIFT_SIGNER.publicKey; export const GIFT_AMOUNT = 1000000000000000n; export const GIFT_MAX_FEE = 50000000000000n; @@ -22,16 +20,19 @@ interface Claim extends AccountConstructorArguments { export async function setupClaim( useTxV3 = false, - useRandomReceiver = true, + useRandom = true, ): Promise<{ factory: Contract; claimAccount: Account; claim: Claim; tokenContract: Contract; receiver: string; + giftSigner: LegacyStarknetKeyPair; }> { + const giftSigner = new LegacyStarknetKeyPair(); + const claimPubKey = giftSigner.publicKey; // static receiver for gas profiling - const receiver = useRandomReceiver ? `0x${encode.buf2hex(ec.starkCurve.utils.randomPrivateKey())}` : "0x42"; + const receiver = useRandom ? `0x${encode.buf2hex(ec.starkCurve.utils.randomPrivateKey())}` : "0x42"; // claim account class hash is read from cache const claimAccountClassHash = await manager.declareLocalContract("ClaimAccount"); @@ -45,7 +46,7 @@ export async function setupClaim( tokenContract.connect(deployer); factory.connect(deployer); await tokenContract.approve(factory.address, GIFT_AMOUNT + GIFT_MAX_FEE); - await factory.deposit(GIFT_AMOUNT, GIFT_MAX_FEE, tokenContract.address, CLAIM_PUB_KEY); + await factory.deposit(GIFT_AMOUNT, GIFT_MAX_FEE, tokenContract.address, claimPubKey); // Ensure there is a contract for the claim const claimAddress = await factory.get_claim_address( @@ -53,7 +54,7 @@ export async function setupClaim( GIFT_AMOUNT, GIFT_MAX_FEE, tokenContract.address, - CLAIM_PUB_KEY, + claimPubKey, ); const constructorArgs: AccountConstructorArguments = { @@ -61,13 +62,13 @@ export async function setupClaim( amount: uint256.bnToUint256(GIFT_AMOUNT), max_fee: GIFT_MAX_FEE, token: tokenContract.address, - claim_pubkey: CLAIM_PUB_KEY, + claim_pubkey: claimPubKey, }; const claim: Claim = { - ...constructorArgs, factory: factory.address, class_hash: claimAccountClassHash, + ...constructorArgs, }; const correctAddress = hash.calculateContractAddressFromHash( @@ -84,7 +85,7 @@ export async function setupClaim( await tokenContract.balance_of(receiver).should.eventually.equal(0n); const txVersion = useTxV3 ? RPC.ETransactionVersion.V3 : RPC.ETransactionVersion.V2; - const claimAccount = new Account(manager, num.toHex(claimAddress), GIFT_SIGNER, undefined, txVersion); + const claimAccount = new Account(manager, num.toHex(claimAddress), giftSigner, undefined, txVersion); factory.connect(claimAccount); - return { factory, claimAccount, claim, tokenContract, receiver }; + return { factory, claimAccount, claim, tokenContract, receiver, giftSigner }; } From 55d3cc98dea01dd63387e1694bcc943ed8e62bc1 Mon Sep 17 00:00:00 2001 From: Leonard Paturel Date: Wed, 5 Jun 2024 18:35:53 +0100 Subject: [PATCH 05/29] random values --- tests-integration/setupClaim.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests-integration/setupClaim.ts b/tests-integration/setupClaim.ts index deb50e1..d1bb6c8 100644 --- a/tests-integration/setupClaim.ts +++ b/tests-integration/setupClaim.ts @@ -29,9 +29,9 @@ export async function setupClaim( receiver: string; giftSigner: LegacyStarknetKeyPair; }> { - const giftSigner = new LegacyStarknetKeyPair(); + // static receiver / signer for gas profiling + const giftSigner = new LegacyStarknetKeyPair(useRandom ? undefined : "0x42"); const claimPubKey = giftSigner.publicKey; - // static receiver for gas profiling const receiver = useRandom ? `0x${encode.buf2hex(ec.starkCurve.utils.randomPrivateKey())}` : "0x42"; // claim account class hash is read from cache From 222139d4d98299c9759cc5bc8b3169301b86fb50 Mon Sep 17 00:00:00 2001 From: Leonard Paturel Date: Thu, 6 Jun 2024 09:30:51 +0100 Subject: [PATCH 06/29] cleanup lib --- lib/accounts.ts | 279 +------------------------------- lib/index.ts | 6 - lib/multisig.ts | 126 --------------- lib/outsideExecution.ts | 155 ------------------ lib/recovery.ts | 62 ------- lib/signers/cairo0-sha256.patch | 31 ---- lib/signers/secp256.ts | 229 -------------------------- lib/signers/webauthn.ts | 160 ------------------ lib/udc.ts | 19 --- 9 files changed, 1 insertion(+), 1066 deletions(-) delete mode 100644 lib/multisig.ts delete mode 100644 lib/outsideExecution.ts delete mode 100644 lib/recovery.ts delete mode 100644 lib/signers/cairo0-sha256.patch delete mode 100644 lib/signers/secp256.ts delete mode 100644 lib/signers/webauthn.ts delete mode 100644 lib/udc.ts diff --git a/lib/accounts.ts b/lib/accounts.ts index e9858d6..ae14e88 100644 --- a/lib/accounts.ts +++ b/lib/accounts.ts @@ -2,38 +2,21 @@ import { Abi, Account, AllowArray, - ArraySignatureType, - CairoOption, - CairoOptionVariant, Call, CallData, Contract, DeployAccountContractPayload, DeployContractResponse, - GetTransactionReceiptResponse, - InvocationsSignerDetails, InvokeFunctionResponse, RPC, - RawCalldata, - Signature, UniversalDetails, - V2InvocationsSignerDetails, - V3InvocationsSignerDetails, - hash, num, - shortString, - stark, - transaction, uint256, } from "starknet"; import { manager } from "./manager"; -import { ensureSuccess } from "./receipts"; -import { LegacyArgentSigner, LegacyKeyPair, LegacyMultisigSigner, LegacyStarknetKeyPair } from "./signers/legacy"; -import { ArgentSigner, KeyPair, RawSigner, randomStarknetKeyPair } from "./signers/signers"; +import { KeyPair } from "./signers/signers"; import { ethAddress, strkAddress } from "./tokens"; -export const VALID = BigInt(shortString.encodeShortString("VALID")); - export class ArgentAccount extends Account { // Increase the gas limit by 30% to avoid failures due to gas estimation being too low with tx v3 and transactions the use escaping override async deployAccount( @@ -79,21 +62,6 @@ export interface ArgentWallet { owner: KeyPair; } -export interface ArgentWalletWithGuardian extends ArgentWallet { - guardian: KeyPair; -} - -export interface LegacyArgentWallet { - account: ArgentAccount; - accountContract: Contract; - owner: LegacyKeyPair; - guardian: LegacyKeyPair; -} - -export interface ArgentWalletWithGuardianAndBackup extends ArgentWalletWithGuardian { - guardianBackup: KeyPair; -} - export const deployer = (() => { if (manager.isDevnet) { const devnetAddress = "0x64b48806902a367c8598f4f95c305e8c1a1acba5f082d294a43793113115691"; @@ -133,251 +101,6 @@ export function setDefaultTransactionVersionV3(account: ArgentAccount): ArgentAc console.log("Deployer:", deployer.address); -export async function deployOldAccount( - owner = new LegacyStarknetKeyPair(), - guardian = new LegacyStarknetKeyPair(), - salt = num.toHex(randomStarknetKeyPair().privateKey), -): Promise { - const proxyClassHash = await manager.declareFixtureContract("Proxy"); - const oldArgentAccountClassHash = await manager.declareFixtureContract("OldArgentAccount"); - - const constructorCalldata = CallData.compile({ - implementation: oldArgentAccountClassHash, - selector: hash.getSelectorFromName("initialize"), - calldata: CallData.compile({ owner: owner.publicKey, guardian: guardian.publicKey }), - }); - - const contractAddress = hash.calculateContractAddressFromHash(salt, proxyClassHash, constructorCalldata, 0); - - const account = new Account(manager, contractAddress, owner); - account.signer = new LegacyMultisigSigner([owner, guardian]); - - await fundAccount(account.address, 1e16, "ETH"); // 0.01 ETH - - const { transaction_hash } = await account.deployAccount({ - classHash: proxyClassHash, - constructorCalldata, - contractAddress, - addressSalt: salt, - }); - await manager.waitForTransaction(transaction_hash); - const accountContract = await manager.loadContract(account.address); - accountContract.connect(account); - return { account, accountContract, owner, guardian }; -} - -async function deployAccountInner(params: DeployAccountParams): Promise< - DeployAccountParams & { - account: Account; - classHash: string; - owner: KeyPair; - guardian?: KeyPair; - salt: string; - transactionHash: string; - } -> { - const finalParams = { - ...params, - classHash: params.classHash ?? (await manager.declareLocalContract("ArgentAccount")), - salt: params.salt ?? num.toHex(randomStarknetKeyPair().privateKey), - owner: params.owner ?? randomStarknetKeyPair(), - useTxV3: params.useTxV3 ?? false, - selfDeploy: params.selfDeploy ?? false, - }; - const guardian = finalParams.guardian - ? finalParams.guardian.signerAsOption - : new CairoOption(CairoOptionVariant.None); - const constructorCalldata = CallData.compile({ owner: finalParams.owner.signer, guardian }); - - const { classHash, salt } = finalParams; - const contractAddress = hash.calculateContractAddressFromHash(salt, classHash, constructorCalldata, 0); - const fundingCall = finalParams.useTxV3 - ? await fundAccountCall(contractAddress, finalParams.fundingAmount ?? 1e16, "STRK") // 0.01 STRK - : await fundAccountCall(contractAddress, finalParams.fundingAmount ?? 1e18, "ETH"); // 1 ETH - const calls = fundingCall ? [fundingCall] : []; - - const transactionVersion = finalParams.useTxV3 ? RPC.ETransactionVersion.V3 : RPC.ETransactionVersion.V2; - const signer = new ArgentSigner(finalParams.owner, finalParams.guardian); - const account = new ArgentAccount(manager, contractAddress, signer, "1", transactionVersion); - - let transactionHash; - if (finalParams.selfDeploy) { - const response = await deployer.execute(calls); - await manager.waitForTransaction(response.transaction_hash); - const { transaction_hash } = await account.deploySelf({ classHash, constructorCalldata, addressSalt: salt }); - transactionHash = transaction_hash; - } else { - const udcCalls = deployer.buildUDCContractPayload({ classHash, salt, constructorCalldata, unique: false }); - const { transaction_hash } = await deployer.execute([...calls, ...udcCalls]); - transactionHash = transaction_hash; - } - - await manager.waitForTransaction(transactionHash); - return { ...finalParams, account, transactionHash }; -} - -export type DeployAccountParams = { - useTxV3?: boolean; - classHash?: string; - owner?: KeyPair; - guardian?: KeyPair; - salt?: string; - fundingAmount?: number | bigint; - selfDeploy?: boolean; -}; - -export async function deployAccount( - params: DeployAccountParams = {}, -): Promise { - params.guardian ||= randomStarknetKeyPair(); - const { account, owner, transactionHash } = await deployAccountInner(params); - const accountContract = await manager.loadContract(account.address); - accountContract.connect(account); - return { account, accountContract, owner, guardian: params.guardian, transactionHash }; -} - -export async function deployAccountWithoutGuardian( - params: Omit = {}, -): Promise { - const { account, owner, transactionHash } = await deployAccountInner(params); - const accountContract = await manager.loadContract(account.address); - accountContract.connect(account); - return { account, accountContract, owner, transactionHash }; -} - -export async function deployAccountWithGuardianBackup( - params: DeployAccountParams & { guardianBackup?: KeyPair } = {}, -): Promise { - const guardianBackup = params.guardianBackup ?? randomStarknetKeyPair(); - - const wallet = (await deployAccount(params)) as ArgentWalletWithGuardianAndBackup & { transactionHash: string }; - await wallet.accountContract.change_guardian_backup(guardianBackup.compiledSignerAsOption); - - wallet.account.signer = new ArgentSigner(wallet.owner, guardianBackup); - wallet.guardianBackup = guardianBackup; - wallet.accountContract.connect(wallet.account); - return wallet; -} - -export async function deployLegacyAccount(classHash: string) { - const owner = new LegacyStarknetKeyPair(); - const guardian = new LegacyStarknetKeyPair(); - const salt = num.toHex(owner.privateKey); - const constructorCalldata = CallData.compile({ owner: owner.publicKey, guardian: guardian.publicKey }); - const contractAddress = hash.calculateContractAddressFromHash(salt, classHash, constructorCalldata, 0); - await fundAccount(contractAddress, 1e15, "ETH"); // 0.001 ETH - const account = new Account(manager, contractAddress, owner, "1"); - account.signer = new LegacyArgentSigner(owner, guardian); - - const { transaction_hash } = await account.deploySelf({ - classHash, - constructorCalldata, - addressSalt: salt, - }); - await manager.waitForTransaction(transaction_hash); - - const accountContract = await manager.loadContract(account.address); - accountContract.connect(account); - return { account, accountContract, owner, guardian }; -} - -export async function upgradeAccount( - accountToUpgrade: Account, - newClassHash: string, - calldata: RawCalldata = [], -): Promise { - const { transaction_hash } = await accountToUpgrade.execute( - { - contractAddress: accountToUpgrade.address, - entrypoint: "upgrade", - calldata: CallData.compile({ implementation: newClassHash, calldata }), - }, - undefined, - { maxFee: 1e14 }, - ); - return await ensureSuccess(await manager.waitForTransaction(transaction_hash)); -} - -export async function executeWithCustomSig( - account: ArgentAccount, - transactions: AllowArray, - signature: ArraySignatureType, - transactionsDetail: UniversalDetails = {}, -): Promise { - const signer = new (class extends RawSigner { - public async signRaw(messageHash: string): Promise { - return signature; - } - })(); - const newAccount = new ArgentAccount( - manager, - account.address, - signer, - account.cairoVersion, - account.transactionVersion, - ); - - return await newAccount.execute(transactions, undefined, transactionsDetail); -} - -export async function getSignerDetails(account: ArgentAccount, calls: Call[]): Promise { - const newAccount = new ArgentAccount( - manager, - account.address, - account.signer, - account.cairoVersion, - account.transactionVersion, - ); - const customSigner = new (class extends RawSigner { - public signerDetails?: InvocationsSignerDetails; - public async signTransaction(calls: Call[], signerDetails: InvocationsSignerDetails): Promise { - this.signerDetails = signerDetails; - throw Error("Should not execute"); - } - public async signRaw(messageHash: string): Promise { - throw Error("Not implemented"); - } - })(); - newAccount.signer = customSigner; - try { - await newAccount.execute(calls, undefined); - throw Error("Should not execute"); - } catch (customError) { - return customSigner.signerDetails!; - } -} - -export function calculateTransactionHash(transactionDetail: InvocationsSignerDetails, calls: Call[]): string { - const compiledCalldata = transaction.getExecuteCalldata(calls, transactionDetail.cairoVersion); - let transactionHash; - if (Object.values(RPC.ETransactionVersion2).includes(transactionDetail.version as any)) { - const transactionDetailV2 = transactionDetail as V2InvocationsSignerDetails; - transactionHash = hash.calculateInvokeTransactionHash({ - ...transactionDetailV2, - senderAddress: transactionDetailV2.walletAddress, - compiledCalldata, - }); - } else if (Object.values(RPC.ETransactionVersion3).includes(transactionDetail.version as any)) { - const transactionDetailV3 = transactionDetail as V3InvocationsSignerDetails; - transactionHash = hash.calculateInvokeTransactionHash({ - ...transactionDetailV3, - senderAddress: transactionDetailV3.walletAddress, - compiledCalldata, - nonceDataAvailabilityMode: stark.intDAM(transactionDetailV3.nonceDataAvailabilityMode), - feeDataAvailabilityMode: stark.intDAM(transactionDetailV3.feeDataAvailabilityMode), - }); - } else { - throw Error("unsupported transaction version"); - } - return transactionHash; -} - -export async function fundAccount(recipient: string, amount: number | bigint, token: "ETH" | "STRK") { - const call = await fundAccountCall(recipient, amount, token); - const response = await deployer.execute(call ? [call] : []); - await manager.waitForTransaction(response.transaction_hash); -} - export async function fundAccountCall( recipient: string, amount: number | bigint, diff --git a/lib/index.ts b/lib/index.ts index 3116f82..79e104b 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -10,17 +10,11 @@ export * from "./contracts"; export * from "./devnet"; export * from "./expectations"; export * from "./manager"; -export * from "./multisig"; export * from "./openZeppelinAccount"; -export * from "./outsideExecution"; export * from "./receipts"; -export * from "./recovery"; export * from "./signers/legacy"; -export * from "./signers/secp256"; export * from "./signers/signers"; -export * from "./signers/webauthn"; export * from "./tokens"; -export * from "./udc"; export * from "./upgrade"; export type Constructor = new (...args: any[]) => T; diff --git a/lib/multisig.ts b/lib/multisig.ts deleted file mode 100644 index 5855bdc..0000000 --- a/lib/multisig.ts +++ /dev/null @@ -1,126 +0,0 @@ -import { Account, CallData, Contract, GetTransactionReceiptResponse, RPC, hash, num } from "starknet"; -import { - ArgentAccount, - KeyPair, - LegacyMultisigSigner, - MultisigSigner, - deployer, - fundAccount, - fundAccountCall, - manager, - randomLegacyMultisigKeyPairs, - randomStarknetKeyPair, - randomStarknetKeyPairs, - sortByGuid, -} from "."; - -export interface MultisigWallet { - account: Account; - accountContract: Contract; - keys: KeyPair[]; - threshold: bigint; - receipt: GetTransactionReceiptResponse; -} - -export type DeployMultisigParams = { - threshold: number; - signersLength?: number; - keys?: KeyPair[]; - useTxV3?: boolean; - classHash?: string; - salt?: string; - fundingAmount?: number | bigint; - selfDeploy?: boolean; - selfDeploymentIndexes?: number[]; -}; - -export async function deployMultisig(params: DeployMultisigParams): Promise { - const finalParams = { - ...params, - classHash: params.classHash ?? (await manager.declareLocalContract("ArgentMultisigAccount")), - salt: params.salt ?? num.toHex(randomStarknetKeyPair().privateKey), - useTxV3: params.useTxV3 ?? false, - selfDeploy: params.selfDeploy ?? false, - selfDeploymentIndexes: params.selfDeploymentIndexes ?? [0], - }; - - if (params.selfDeploymentIndexes && !finalParams.selfDeploy) { - throw new Error("selfDeploymentIndexes can only be used with selfDeploy"); - } - - if (!params.keys && !finalParams.signersLength) { - throw new Error("Fill in one of 'keys' or 'signersLength'"); - } - const keys = params.keys ?? sortedKeyPairs(finalParams.signersLength!); - const signers = keysToSigners(keys); - const constructorCalldata = CallData.compile({ threshold: finalParams.threshold, signers }); - - const { classHash, salt, selfDeploymentIndexes } = finalParams; - const accountAddress = hash.calculateContractAddressFromHash(salt, classHash, constructorCalldata, 0); - - const fundingCall = finalParams.useTxV3 - ? await fundAccountCall(accountAddress, finalParams.fundingAmount ?? 1e16, "STRK") // 0.01 STRK - : await fundAccountCall(accountAddress, finalParams.fundingAmount ?? 1e15, "ETH"); // 0.001 ETH - const calls = fundingCall ? [fundingCall] : []; - - const transactionVersion = finalParams.useTxV3 ? RPC.ETransactionVersion.V3 : RPC.ETransactionVersion.V2; - - let transactionHash; - if (finalParams.selfDeploy) { - const response = await deployer.execute(calls); - await manager.waitForTransaction(response.transaction_hash); - - const selfDeploymentSigner = new MultisigSigner(keys.filter((_, i) => selfDeploymentIndexes.includes(i))); - const account = new Account(manager, accountAddress, selfDeploymentSigner, "1", transactionVersion); - - const { transaction_hash } = await account.deploySelf({ classHash, constructorCalldata, addressSalt: salt }); - transactionHash = transaction_hash; - } else { - const udcCalls = deployer.buildUDCContractPayload({ classHash, salt, constructorCalldata, unique: false }); - const { transaction_hash } = await deployer.execute([...calls, ...udcCalls]); - transactionHash = transaction_hash; - } - - const receipt = await manager.waitForTransaction(transactionHash); - const signer = new MultisigSigner(keys.slice(0, finalParams.threshold)); - const account = new ArgentAccount(manager, accountAddress, signer, "1", transactionVersion); - const accountContract = await manager.loadContract(account.address); - accountContract.connect(account); - return { account, accountContract, keys, receipt, threshold: BigInt(finalParams.threshold) }; -} - -export async function deployMultisig1_3( - params: Omit = {}, -): Promise { - return deployMultisig({ ...params, threshold: 1, signersLength: 3 }); -} - -export async function deployMultisig1_1( - params: Omit = {}, -): Promise { - return deployMultisig({ ...params, threshold: 1, signersLength: 1 }); -} - -const sortedKeyPairs = (length: number) => sortByGuid(randomStarknetKeyPairs(length)); - -const keysToSigners = (keys: KeyPair[]) => keys.map(({ signer }) => signer); - -export async function deployLegacyMultisig(classHash: string, threshold = 1) { - const keys = randomLegacyMultisigKeyPairs(threshold); - const signersPublicKeys = keys.map((key) => key.publicKey); - const salt = num.toHex(randomStarknetKeyPair().privateKey); - const constructorCalldata = CallData.compile({ threshold, signers: signersPublicKeys }); - const contractAddress = hash.calculateContractAddressFromHash(salt, classHash, constructorCalldata, 0); - await fundAccount(contractAddress, 1e15, "ETH"); // 0.001 ETH - const deploySigner = new LegacyMultisigSigner([keys[0]]); - const account = new Account(manager, contractAddress, deploySigner, "1"); - - const { transaction_hash } = await account.deploySelf({ classHash, constructorCalldata, addressSalt: salt }); - await manager.waitForTransaction(transaction_hash); - - const signers = new LegacyMultisigSigner(keys); - account.signer = signers; - const accountContract = await manager.loadContract(account.address); - accountContract.connect(account); - return { account, accountContract, deploySigner, signers }; -} diff --git a/lib/outsideExecution.ts b/lib/outsideExecution.ts deleted file mode 100644 index 348c66a..0000000 --- a/lib/outsideExecution.ts +++ /dev/null @@ -1,155 +0,0 @@ -import { Call, CallData, hash, num, RawArgs, SignerInterface, typedData } from "starknet"; -import { manager } from "./"; - -const typesRev0 = { - StarkNetDomain: [ - { name: "name", type: "felt" }, - { name: "version", type: "felt" }, - { name: "chainId", type: "felt" }, - ], - OutsideExecution: [ - { name: "caller", type: "felt" }, - { name: "nonce", type: "felt" }, - { name: "execute_after", type: "felt" }, - { name: "execute_before", type: "felt" }, - { name: "calls_len", type: "felt" }, - { name: "calls", type: "OutsideCall*" }, - ], - OutsideCall: [ - { name: "to", type: "felt" }, - { name: "selector", type: "felt" }, - { name: "calldata_len", type: "felt" }, - { name: "calldata", type: "felt*" }, - ], -}; - -const typesRev1 = { - StarknetDomain: [ - { name: "name", type: "shortstring" }, - { name: "version", type: "shortstring" }, - { name: "chainId", type: "shortstring" }, - { name: "revision", type: "shortstring" }, - ], - OutsideExecution: [ - { name: "Caller", type: "ContractAddress" }, - { name: "Nonce", type: "felt" }, - { name: "Execute After", type: "u128" }, - { name: "Execute Before", type: "u128" }, - { name: "Calls", type: "Call*" }, - ], - Call: [ - { name: "To", type: "ContractAddress" }, - { name: "Selector", type: "selector" }, - { name: "Calldata", type: "felt*" }, - ], -}; - -function getDomain(chainId: string, revision: typedData.TypedDataRevision) { - if (revision == typedData.TypedDataRevision.Active) { - // WARNING! Version and revision are encoded as numbers 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: "Account.execute_from_outside", - version: "2", - chainId: chainId, - revision: "1", - }; - } - return { - name: "Account.execute_from_outside", - version: "1", - chainId: chainId, - }; -} - -export interface OutsideExecution { - caller: string; - nonce: num.BigNumberish; - execute_after: num.BigNumberish; - execute_before: num.BigNumberish; - calls: OutsideCall[]; -} - -export interface OutsideCall { - to: string; - selector: num.BigNumberish; - calldata: RawArgs; -} - -export function getOutsideCall(call: Call): OutsideCall { - return { - to: call.contractAddress, - selector: hash.getSelectorFromName(call.entrypoint), - calldata: call.calldata ?? [], - }; -} - -export function getTypedDataHash( - outsideExecution: OutsideExecution, - accountAddress: num.BigNumberish, - chainId: string, - revision: typedData.TypedDataRevision, -): string { - return typedData.getMessageHash(getTypedData(outsideExecution, chainId, revision), accountAddress); -} - -export function getTypedData( - outsideExecution: OutsideExecution, - chainId: string, - revision: typedData.TypedDataRevision, -) { - if (revision == typedData.TypedDataRevision.Active) { - return { - types: typesRev1, - primaryType: "OutsideExecution", - domain: getDomain(chainId, revision), - message: { - Caller: outsideExecution.caller, - Nonce: outsideExecution.nonce, - "Execute After": outsideExecution.execute_after, - "Execute Before": outsideExecution.execute_before, - Calls: outsideExecution.calls.map((call) => { - return { - To: call.to, - Selector: call.selector, - Calldata: call.calldata, - }; - }), - }, - }; - } - - return { - types: typesRev0, - primaryType: "OutsideExecution", - domain: getDomain(chainId, revision), - message: { - ...outsideExecution, - calls_len: outsideExecution.calls.length, - calls: outsideExecution.calls.map((call) => { - return { - ...call, - calldata_len: call.calldata.length, - calldata: call.calldata, - }; - }), - }, - }; -} - -export async function getOutsideExecutionCall( - outsideExecution: OutsideExecution, - accountAddress: string, - signer: SignerInterface, - revision: typedData.TypedDataRevision, - chainId?: string, -): Promise { - chainId = chainId ?? (await manager.getChainId()); - const currentTypedData = getTypedData(outsideExecution, chainId, revision); - const signature = await signer.signMessage(currentTypedData, accountAddress); - return { - contractAddress: accountAddress, - entrypoint: revision == typedData.TypedDataRevision.Active ? "execute_from_outside_v2" : "execute_from_outside", - calldata: CallData.compile({ ...outsideExecution, signature }), - }; -} diff --git a/lib/recovery.ts b/lib/recovery.ts deleted file mode 100644 index 6424df8..0000000 --- a/lib/recovery.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { expect } from "chai"; -import { CairoCustomEnum, Contract, hash } from "starknet"; -import { RawSigner } from "."; - -export const ESCAPE_SECURITY_PERIOD = 7n * 24n * 60n * 60n; // 7 days -export const ESCAPE_EXPIRY_PERIOD = 2n * 7n * 24n * 60n * 60n; // 14 days -export const MAX_U64 = 2n ** 64n - 1n; - -export enum EscapeStatus { - None, - NotReady, - Ready, - Expired, -} - -export const ESCAPE_TYPE_NONE = new CairoCustomEnum({ - None: {}, - Guardian: undefined, - Owner: undefined, -}); - -export const ESCAPE_TYPE_GUARDIAN = new CairoCustomEnum({ - None: undefined, - Guardian: {}, - Owner: undefined, -}); - -export const ESCAPE_TYPE_OWNER = new CairoCustomEnum({ - None: undefined, - Guardian: undefined, - Owner: {}, -}); - -export const signChangeOwnerMessage = async ( - accountAddress: string, - currentOwnerGuid: bigint, - newOwner: RawSigner, - chainId: string, -) => { - const messageHash = await getChangeOwnerMessageHash(accountAddress, currentOwnerGuid, chainId); - return newOwner.signRaw(messageHash); -}; - -export const getChangeOwnerMessageHash = async (accountAddress: string, currentOwnerGuid: bigint, chainId: string) => { - const changeOwnerSelector = hash.getSelectorFromName("change_owner"); - return hash.computeHashOnElements([changeOwnerSelector, chainId, accountAddress, currentOwnerGuid]); -}; - -export async function hasOngoingEscape(accountContract: Contract): Promise { - const escape = await accountContract.get_escape(); - return escape.escape_type != 0n && escape.ready_at != 0n && escape.new_signer != 0n; -} - -export async function getEscapeStatus(accountContract: Contract): Promise { - // StarknetJs parsing is broken so we do it manually - const result = (await accountContract.call("get_escape_and_status", undefined, { parseResponse: false })) as string[]; - const result_len = result.length; - expect(result_len).to.be.oneOf([4, 6]); - const status = Number(result[result_len - 1]); - expect(status).to.be.lessThan(4, `Unknown status ${status}`); - return status; -} diff --git a/lib/signers/cairo0-sha256.patch b/lib/signers/cairo0-sha256.patch deleted file mode 100644 index 4dc4e56..0000000 --- a/lib/signers/cairo0-sha256.patch +++ /dev/null @@ -1,31 +0,0 @@ -commit d77431b967d84de3a902fbfea5cf8e1ac972f6de -Author: Yoav Gaziel -Date: Thu Nov 16 11:10:28 2023 +0200 - - add external entrypoint in main.cairo - -diff --git a/src/main.cairo b/src/main.cairo -new file mode 100644 -index 0000000..16c4e00 ---- /dev/null -+++ b/src/main.cairo -@@ -0,0 +1,19 @@ -+%lang starknet -+from starkware.cairo.common.alloc import alloc -+from starkware.cairo.common.math import split_int -+from starkware.cairo.common.memcpy import memcpy -+from starkware.cairo.common.cairo_builtins import BitwiseBuiltin, HashBuiltin -+from src.sha256 import sha256, finalize_sha256 -+ -+@view -+func sha256_cairo0{bitwise_ptr: BitwiseBuiltin*, pedersen_ptr: HashBuiltin*, range_check_ptr}( -+ data_len: felt, data: felt*, data_len_no_padding: felt -+) -> (result_len: felt, result: felt*) { -+ alloc_locals; -+ let (local sha256_ptr_start: felt*) = alloc(); -+ let sha256_ptr = sha256_ptr_start; -+ let sha256_ptr_end = sha256_ptr_start; -+ let (hash) = sha256{sha256_ptr=sha256_ptr}(data, data_len_no_padding); -+ finalize_sha256(sha256_ptr_start=sha256_ptr_start, sha256_ptr_end=sha256_ptr_end); -+ return (8, hash); -+} diff --git a/lib/signers/secp256.ts b/lib/signers/secp256.ts deleted file mode 100644 index 9c52dfc..0000000 --- a/lib/signers/secp256.ts +++ /dev/null @@ -1,229 +0,0 @@ -import * as utils from "@noble/curves/abstract/utils"; -import { p256 as secp256r1 } from "@noble/curves/p256"; -import { secp256k1 } from "@noble/curves/secp256k1"; -import { Signature as EthersSignature, Wallet } from "ethers"; -import { CairoCustomEnum, CallData, hash, num, shortString, uint256 } from "starknet"; -import { KeyPair, SignerType, signerTypeToCustomEnum } from "../signers/signers"; - -export type NormalizedSecpSignature = { r: bigint; s: bigint; yParity: boolean }; - -export function normalizeSecpR1Signature(signature: { - r: bigint; - s: bigint; - recovery: number; -}): NormalizedSecpSignature { - return normalizeSecpSignature(secp256r1, signature); -} - -export function normalizeSecpK1Signature(signature: { - r: bigint; - s: bigint; - recovery: number; -}): NormalizedSecpSignature { - return normalizeSecpSignature(secp256k1, signature); -} - -export function normalizeSecpSignature( - curve: typeof secp256r1 | typeof secp256k1, - signature: { r: bigint; s: bigint; recovery: number }, -): NormalizedSecpSignature { - let s = signature.s; - let yParity = signature.recovery !== 0; - if (s > curve.CURVE.n / 2n) { - s = curve.CURVE.n - s; - yParity = !yParity; - } - return { r: signature.r, s, yParity }; -} - -export class EthKeyPair extends KeyPair { - pk: bigint; - allowLowS?: boolean; - - constructor(pk?: string | bigint, allowLowS?: boolean) { - super(); - - if (pk == undefined) { - pk = Wallet.createRandom().privateKey; - } - if (typeof pk === "string") { - pk = BigInt(pk); - } - this.pk = pk; - this.allowLowS = allowLowS; - } - - public get address(): bigint { - return BigInt(new Wallet("0x" + padTo32Bytes(num.toHex(this.pk))).address); - } - - public get guid(): bigint { - return BigInt(hash.computePoseidonHash(shortString.encodeShortString("Secp256k1 Signer"), this.address)); - } - - public get storedValue(): bigint { - throw new Error("Not implemented yet"); - } - - public get signer(): CairoCustomEnum { - return signerTypeToCustomEnum(SignerType.Secp256k1, { signer: this.address }); - } - - public async signRaw(messageHash: string): Promise { - const signature = normalizeSecpK1Signature( - secp256k1.sign(padTo32Bytes(messageHash), this.pk, { lowS: this.allowLowS }), - ); - - return CallData.compile([ - signerTypeToCustomEnum(SignerType.Secp256k1, { - pubkeyHash: this.address, - r: uint256.bnToUint256(signature.r), - s: uint256.bnToUint256(signature.s), - y_parity: signature.yParity, - }), - ]); - } -} - -export class Eip191KeyPair extends KeyPair { - pk: string; - - constructor(pk?: string | bigint) { - super(); - this.pk = pk ? "0x" + padTo32Bytes(num.toHex(pk)) : Wallet.createRandom().privateKey; - } - - public get address() { - return BigInt(new Wallet(this.pk).address); - } - - public get guid(): bigint { - return BigInt(hash.computePoseidonHash(shortString.encodeShortString("Eip191 Signer"), this.address)); - } - - public get storedValue(): bigint { - throw new Error("Not implemented yet"); - } - - public get signer(): CairoCustomEnum { - return signerTypeToCustomEnum(SignerType.Eip191, { signer: this.address }); - } - - public async signRaw(messageHash: string): Promise { - const ethSigner = new Wallet(this.pk); - messageHash = "0x" + padTo32Bytes(messageHash); - const ethersSignature = EthersSignature.from(ethSigner.signMessageSync(num.hexToBytes(messageHash))); - - const signature = normalizeSecpK1Signature({ - r: BigInt(ethersSignature.r), - s: BigInt(ethersSignature.s), - recovery: ethersSignature.yParity ? 1 : 0, - }); - - return CallData.compile([ - signerTypeToCustomEnum(SignerType.Eip191, { - ethAddress: this.address, - r: uint256.bnToUint256(signature.r), - s: uint256.bnToUint256(signature.s), - y_parity: signature.yParity, - }), - ]); - } -} - -export class EstimateEip191KeyPair extends KeyPair { - readonly address: bigint; - - constructor(address: bigint) { - super(); - this.address = address; - } - - public get privateKey(): string { - throw new Error("EstimateEip191KeyPair does not have a private key"); - } - - public get guid(): bigint { - throw new Error("Not implemented yet"); - } - - public get storedValue(): bigint { - throw new Error("Not implemented yet"); - } - - public get signer(): CairoCustomEnum { - return signerTypeToCustomEnum(SignerType.Eip191, { signer: this.address }); - } - - public async signRaw(messageHash: string): Promise { - return CallData.compile([ - signerTypeToCustomEnum(SignerType.Eip191, { - ethAddress: this.address, - r: uint256.bnToUint256("0x1556a70d76cc452ae54e83bb167a9041f0d062d000fa0dcb42593f77c544f647"), - s: uint256.bnToUint256("0x1643d14dbd6a6edc658f4b16699a585181a08dba4f6d16a9273e0e2cbed622da"), - y_parity: false, - }), - ]); - } -} - -export class Secp256r1KeyPair extends KeyPair { - pk: bigint; - private allowLowS?: boolean; - - constructor(pk?: string | bigint, allowLowS?: boolean) { - super(); - this.pk = BigInt(pk ? `${pk}` : Wallet.createRandom().privateKey); - this.allowLowS = allowLowS; - } - - public get publicKey() { - const publicKey = secp256r1.getPublicKey(this.pk).slice(1); - return uint256.bnToUint256("0x" + utils.bytesToHex(publicKey)); - } - - public get guid(): bigint { - return BigInt( - hash.computePoseidonHashOnElements([ - shortString.encodeShortString("Secp256r1 Signer"), - this.publicKey.low, - this.publicKey.high, - ]), - ); - } - - public get storedValue(): bigint { - throw new Error("Not implemented yet"); - } - - public get signer() { - return signerTypeToCustomEnum(SignerType.Secp256r1, { signer: this.publicKey }); - } - - public async signRaw(messageHash: string): Promise { - messageHash = padTo32Bytes(messageHash); - const signature = normalizeSecpR1Signature(secp256r1.sign(messageHash, this.pk, { lowS: this.allowLowS })); - return CallData.compile([ - signerTypeToCustomEnum(SignerType.Secp256r1, { - pubkey: this.publicKey, - r: uint256.bnToUint256(signature.r), - s: uint256.bnToUint256(signature.s), - y_parity: signature.yParity, - }), - ]); - } -} - -export function padTo32Bytes(hexString: string): string { - if (hexString.startsWith("0x")) { - hexString = hexString.slice(2); - } - if (hexString.length < 64) { - hexString = "0".repeat(64 - hexString.length) + hexString; - } - return hexString; -} - -export const randomEthKeyPair = () => new EthKeyPair(); -export const randomEip191KeyPair = () => new Eip191KeyPair(); -export const randomSecp256r1KeyPair = () => new Secp256r1KeyPair(); diff --git a/lib/signers/webauthn.ts b/lib/signers/webauthn.ts deleted file mode 100644 index f38a1de..0000000 --- a/lib/signers/webauthn.ts +++ /dev/null @@ -1,160 +0,0 @@ -import { concatBytes } from "@noble/curves/abstract/utils"; -import { p256 as secp256r1 } from "@noble/curves/p256"; -import { BinaryLike, createHash } from "crypto"; -import { - ArraySignatureType, - BigNumberish, - CairoCustomEnum, - CallData, - Uint256, - hash, - shortString, - uint256, -} from "starknet"; -import { KeyPair, SignerType, normalizeSecpR1Signature, signerTypeToCustomEnum } from ".."; - -const buf2hex = (buffer: ArrayBuffer, prefix = true) => - `${prefix ? "0x" : ""}${[...new Uint8Array(buffer)].map((x) => x.toString(16).padStart(2, "0")).join("")}`; - -const normalizeTransactionHash = (transactionHash: string) => transactionHash.replace(/^0x/, "").padStart(64, "0"); - -const buf2base64url = (buffer: ArrayBuffer) => - buf2base64(buffer).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, ""); - -const buf2base64 = (buffer: ArrayBuffer) => btoa(String.fromCharCode(...new Uint8Array(buffer))); - -const hex2buf = (hex: string) => - Uint8Array.from( - hex - .replace(/^0x/, "") - .match(/.{1,2}/g)! - .map((byte) => parseInt(byte, 16)), - ); - -const toCharArray = (value: string) => CallData.compile(value.split("").map(shortString.encodeShortString)); - -interface WebauthnSigner { - origin: BigNumberish[]; - rp_id_hash: Uint256; - pubkey: Uint256; -} - -interface WebauthnSignature { - cross_origin: boolean; - client_data_json_outro: BigNumberish[]; - flags: number; - sign_count: number; - ec_signature: { r: Uint256; s: Uint256; y_parity: boolean }; - sha256_implementation: CairoCustomEnum; -} - -export class WebauthnOwner extends KeyPair { - pk: Uint8Array; - rpIdHash: Uint256; - - constructor( - pk?: string, - public rpId = "localhost", - public origin = "http://localhost:5173", - ) { - super(); - this.pk = pk ? hex2buf(normalizeTransactionHash(pk)) : secp256r1.utils.randomPrivateKey(); - this.rpIdHash = uint256.bnToUint256(buf2hex(sha256(rpId))); - } - - public get publicKey() { - return secp256r1.getPublicKey(this.pk).slice(1); - } - - public get guid(): bigint { - const rpIdHashAsU256 = this.rpIdHash; - const publicKeyAsU256 = uint256.bnToUint256(buf2hex(this.publicKey)); - const originBytes = toCharArray(this.origin); - const elements = [ - shortString.encodeShortString("Webauthn Signer"), - originBytes.length, - ...originBytes, - rpIdHashAsU256.low, - rpIdHashAsU256.high, - publicKeyAsU256.low, - publicKeyAsU256.high, - ]; - return BigInt(hash.computePoseidonHashOnElements(elements)); - } - - public get storedValue(): bigint { - throw new Error("Not implemented yet"); - } - - public get signer(): CairoCustomEnum { - const signer: WebauthnSigner = { - origin: toCharArray(this.origin), - rp_id_hash: this.rpIdHash, - pubkey: uint256.bnToUint256(buf2hex(this.publicKey)), - }; - return signerTypeToCustomEnum(SignerType.Webauthn, signer); - } - - public async signRaw(messageHash: string): Promise { - const webauthnSigner = this.signer.variant.Webauthn; - const webauthnSignature = await this.signHash(messageHash); - return CallData.compile([signerTypeToCustomEnum(SignerType.Webauthn, { webauthnSigner, webauthnSignature })]); - } - - public async signHash(transactionHash: string): Promise { - const flags = "0b00000101"; // present and verified - const signCount = 0; - const authenticatorData = concatBytes(sha256(this.rpId), new Uint8Array([Number(flags), 0, 0, 0, signCount])); - - const sha256Impl = 0; - const challenge = buf2base64url(hex2buf(`${normalizeTransactionHash(transactionHash)}0${sha256Impl}`)); - const crossOrigin = false; - const extraJson = ""; // = `,"extraField":"random data"}`; - const clientData = JSON.stringify({ type: "webauthn.get", challenge, origin: this.origin, crossOrigin }); - const clientDataJson = extraJson ? clientData.replace(/}$/, extraJson) : clientData; - const clientDataHash = sha256(new TextEncoder().encode(clientDataJson)); - - const signedHash = sha256(concatBytes(authenticatorData, clientDataHash)); - - const signature = normalizeSecpR1Signature(secp256r1.sign(signedHash, this.pk)); - - // console.log(` - // let transaction_hash = ${transactionHash}; - // let pubkey = ${buf2hex(this.publicKey)}; - // let signer = new_webauthn_signer(:origin, :rp_id_hash, :pubkey); - // let signature = WebauthnSignature { - // cross_origin: ${crossOrigin}, - // client_data_json_outro: ${extraJson ? `${JSON.stringify(extraJson)}.into_bytes()` : "array![]"}.span(), - // flags: ${flags}, - // sign_count: ${signCount}, - // ec_signature: Signature { - // r: 0x${r.toString(16)}, - // s: 0x${s.toString(16)}, - // y_parity: ${recovery !== 0}, - // }, - // sha256_implementation: Sha256Implementation::Cairo${sha256Impl}, - // };`); - - return { - cross_origin: crossOrigin, - client_data_json_outro: CallData.compile(toCharArray(extraJson)), - flags: Number(flags), - sign_count: signCount, - ec_signature: { - r: uint256.bnToUint256(signature.r), - s: uint256.bnToUint256(signature.s), - y_parity: signature.yParity, - }, - sha256_implementation: new CairoCustomEnum({ - Cairo0: sha256Impl ? undefined : {}, - Cairo1: sha256Impl ? {} : undefined, - }), - }; - } -} - -function sha256(message: BinaryLike) { - return createHash("sha256").update(message).digest(); -} - -export const randomWebauthnOwner = () => new WebauthnOwner(); diff --git a/lib/udc.ts b/lib/udc.ts deleted file mode 100644 index fe8abe1..0000000 --- a/lib/udc.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { CallData, RawCalldata } from "starknet"; -import { deployer, manager } from "."; - -export const udcAddress = "0x041a78e741e5af2fec34b695679bc6891742439f7afb8484ecd7766661ad02bf"; - -export async function deployContractUDC(classHash: string, salt: string, calldata: RawCalldata) { - const unique = 0n; //false - - const udcContract = await manager.loadContract(udcAddress); - - udcContract.connect(deployer); - - const deployCall = udcContract.populate("deployContract", CallData.compile([classHash, salt, unique, calldata])); - const { transaction_hash } = await udcContract.deployContract(deployCall.calldata); - - const transaction_response = await manager.waitForTransaction(transaction_hash); - - return transaction_response.events?.[0].from_address; -} From 126d51eb5d9ff179875a62b464844d88f32f22c8 Mon Sep 17 00:00:00 2001 From: Leonard Paturel Date: Thu, 6 Jun 2024 09:34:59 +0100 Subject: [PATCH 07/29] remove more uneeded code --- lib/index.ts | 1 - lib/upgrade.ts | 12 ------------ 2 files changed, 13 deletions(-) delete mode 100644 lib/upgrade.ts diff --git a/lib/index.ts b/lib/index.ts index 79e104b..5ba5b06 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -15,6 +15,5 @@ export * from "./receipts"; export * from "./signers/legacy"; export * from "./signers/signers"; export * from "./tokens"; -export * from "./upgrade"; export type Constructor = new (...args: any[]) => T; diff --git a/lib/upgrade.ts b/lib/upgrade.ts deleted file mode 100644 index d60be57..0000000 --- a/lib/upgrade.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { Call, CallData } from "starknet"; -import { getOutsideCall } from "./outsideExecution"; - -export function getUpgradeData(calls: Call[]) { - const externalCalls = calls.map(getOutsideCall); - return CallData.compile({ externalCalls }); -} - -export function getUpgradeDataLegacy(calls: Call[]) { - const upgradeData = getUpgradeData(calls); - return CallData.compile({ upgradeData }); -} From 633aa2ccdad777cabafc88f03535ac1f0c93840b Mon Sep 17 00:00:00 2001 From: Leonard Paturel Date: Thu, 6 Jun 2024 10:01:37 +0100 Subject: [PATCH 08/29] split up setup and added caching --- tests-integration/account.test.ts | 25 +++++++++++-------- tests-integration/claim_external.test.ts | 5 ++-- tests-integration/factory.test.ts | 21 +++++++++------- tests-integration/setupClaim.ts | 31 +++++++++++++++++------- 4 files changed, 52 insertions(+), 30 deletions(-) diff --git a/tests-integration/account.test.ts b/tests-integration/account.test.ts index aab82f1..d026bf2 100644 --- a/tests-integration/account.test.ts +++ b/tests-integration/account.test.ts @@ -1,18 +1,17 @@ import { expect } from "chai"; import { num } from "starknet"; import { deployer, expectRevertWithErrorMessage, manager } from "../lib"; -import { GIFT_AMOUNT, GIFT_MAX_FEE, setupClaim } from "./setupClaim"; +import { GIFT_AMOUNT, GIFT_MAX_FEE, setupClaim, setupGiftProtocol } from "./setupClaim"; describe("Gifting", function () { - let claimAccountClassHash: string; - before(async () => { - // declare claim account - claimAccountClassHash = await manager.declareLocalContract("ClaimAccount"); - }); - for (const useTxV3 of [false, true]) { it(`Testing simple claim flow using txV3: ${useTxV3}`, async function () { - const { factory, claimAccount, claim, tokenContract, receiver } = await setupClaim(useTxV3); + const { factory, claimAccountClassHash } = await setupGiftProtocol(); + const { claimAccount, claim, tokenContract, receiver } = await setupClaim( + factory, + claimAccountClassHash, + useTxV3, + ); await factory.claim_internal(claim, receiver); // Final check @@ -22,7 +21,12 @@ describe("Gifting", function () { }); it(`Test max fee too high`, async function () { - const { factory, claimAccount, claim, receiver } = await setupClaim(useTxV3); + const { factory, claimAccountClassHash } = await setupGiftProtocol(); + const { claimAccount, claim, tokenContract, receiver } = await setupClaim( + factory, + claimAccountClassHash, + useTxV3, + ); if (useTxV3) { const estimate = await factory.estimateFee.claim_internal(claim, receiver); const newResourceBounds = { @@ -55,7 +59,8 @@ describe("Gifting", function () { } it(`Test basic validation asserts`, async function () { - const { factory, claimAccount, claim, receiver } = await setupClaim(); + const { factory, claimAccountClassHash } = await setupGiftProtocol(); + const { claimAccount, claim, receiver } = await setupClaim(factory, claimAccountClassHash); const claimContract = await manager.loadContract(num.toHex(claimAccount.address)); diff --git a/tests-integration/claim_external.test.ts b/tests-integration/claim_external.test.ts index ff1abff..0cca862 100644 --- a/tests-integration/claim_external.test.ts +++ b/tests-integration/claim_external.test.ts @@ -1,5 +1,5 @@ import { deployer, getClaimExternalData, manager } from "../lib"; -import { setupClaim } from "./setupClaim"; +import { setupClaim, setupGiftProtocol } from "./setupClaim"; describe("claim_external", function () { before(async () => { @@ -8,7 +8,8 @@ describe("claim_external", function () { for (const useTxV3 of [false, true]) { it(`Testing claim_external flow using txV3: ${useTxV3}`, async function () { - const { factory, claimAccount, claim, receiver, giftSigner } = await setupClaim(useTxV3); + const { factory, claimAccountClassHash } = await setupGiftProtocol(); + const { claimAccount, claim, receiver, giftSigner } = await setupClaim(factory, claimAccountClassHash, useTxV3); const claimExternalData = await getClaimExternalData({ receiver }); const signature = await giftSigner.signMessage(claimExternalData, claimAccount.address); diff --git a/tests-integration/factory.test.ts b/tests-integration/factory.test.ts index 9221438..b0f4efd 100644 --- a/tests-integration/factory.test.ts +++ b/tests-integration/factory.test.ts @@ -1,7 +1,7 @@ import { expect } from "chai"; import { Account, RPC, num, uint256 } from "starknet"; import { LegacyStarknetKeyPair, deployer, expectRevertWithErrorMessage, genericAccount, manager } from "../lib"; -import { GIFT_AMOUNT, GIFT_MAX_FEE, setupClaim } from "./setupClaim"; +import { GIFT_AMOUNT, GIFT_MAX_FEE, setupClaim, setupGiftProtocol } from "./setupClaim"; describe("Factory", function () { let claimAccountClassHash: string; @@ -11,8 +11,13 @@ describe("Factory", function () { for (const useTxV3 of [false, true]) { it(`get_dust: ${useTxV3}`, async function () { - const { factory, tokenContract, claimAccount, claim, receiver } = await setupClaim(useTxV3); - const receiverDust = `0x${Math.floor(Math.random() * 10)}`; + const { factory, claimAccountClassHash } = await setupGiftProtocol(); + const { claimAccount, claim, tokenContract, receiver } = await setupClaim( + factory, + claimAccountClassHash, + useTxV3, + ); + const receiverDust = `0x2${Math.floor(Math.random() * 1000)}`; await factory.claim_internal(claim, receiver); @@ -32,7 +37,8 @@ describe("Factory", function () { } it(`Test Cancel Claim`, async function () { - const { factory, tokenContract, claimAccount, claim, receiver } = await setupClaim(); + const { factory, claimAccountClassHash } = await setupGiftProtocol(); + const { claimAccount, claim, tokenContract, receiver } = await setupClaim(factory, claimAccountClassHash); const balanceSenderBefore = await tokenContract.balance_of(deployer.address); factory.connect(deployer); @@ -51,15 +57,12 @@ describe("Factory", function () { it(`Test pausable`, async function () { // Deploy factory - const factory = await manager.deployContract("GiftFactory", { - unique: true, - constructorCalldata: [claimAccountClassHash, deployer.address], - }); + const { factory, claimAccountClassHash } = await setupGiftProtocol(); const signer = new LegacyStarknetKeyPair(); const claimPubkey = signer.publicKey; const GIFT_AMOUNT = 1000000000000000n; const GIFT_MAX_FEE = 50000000000000n; - const receiver = "0x45"; + const receiver = `0x5${Math.floor(Math.random() * 1000)}`; // Make a gift const tokenContract = await manager.tokens.feeTokenContract(false); diff --git a/tests-integration/setupClaim.ts b/tests-integration/setupClaim.ts index d1bb6c8..dec6dcf 100644 --- a/tests-integration/setupClaim.ts +++ b/tests-integration/setupClaim.ts @@ -18,11 +18,31 @@ interface Claim extends AccountConstructorArguments { class_hash: string; } +const cache: Record = {}; + +export async function setupGiftProtocol(): Promise<{ + factory: Contract; + claimAccountClassHash: string; +}> { + const claimAccountClassHash = await manager.declareLocalContract("ClaimAccount"); + const cachedFactory = cache["GiftFactory"]; + if (cachedFactory) { + return { factory: cachedFactory, claimAccountClassHash }; + } + const factory = await manager.deployContract("GiftFactory", { + unique: true, + constructorCalldata: [claimAccountClassHash, deployer.address], + }); + cache["GiftFactory"] = factory; + return { factory, claimAccountClassHash }; +} + export async function setupClaim( + factory: Contract, + claimAccountClassHash: string, useTxV3 = false, useRandom = true, ): Promise<{ - factory: Contract; claimAccount: Account; claim: Claim; tokenContract: Contract; @@ -34,13 +54,6 @@ export async function setupClaim( const claimPubKey = giftSigner.publicKey; const receiver = useRandom ? `0x${encode.buf2hex(ec.starkCurve.utils.randomPrivateKey())}` : "0x42"; - // claim account class hash is read from cache - const claimAccountClassHash = await manager.declareLocalContract("ClaimAccount"); - const factory = await manager.deployContract("GiftFactory", { - unique: true, - constructorCalldata: [claimAccountClassHash, deployer.address], - }); - // Make a gift const tokenContract = await manager.tokens.feeTokenContract(useTxV3); tokenContract.connect(deployer); @@ -87,5 +100,5 @@ export async function setupClaim( const txVersion = useTxV3 ? RPC.ETransactionVersion.V3 : RPC.ETransactionVersion.V2; const claimAccount = new Account(manager, num.toHex(claimAddress), giftSigner, undefined, txVersion); factory.connect(claimAccount); - return { factory, claimAccount, claim, tokenContract, receiver, giftSigner }; + return { claimAccount, claim, tokenContract, receiver, giftSigner }; } From f1bb825062b992c750b1f8b74d6164932298ea07 Mon Sep 17 00:00:00 2001 From: Leonard Paturel Date: Thu, 6 Jun 2024 10:59:28 +0100 Subject: [PATCH 09/29] remove before function --- tests-integration/claim_external.test.ts | 6 +----- tests-integration/factory.test.ts | 5 ----- 2 files changed, 1 insertion(+), 10 deletions(-) diff --git a/tests-integration/claim_external.test.ts b/tests-integration/claim_external.test.ts index 5840321..801f5fe 100644 --- a/tests-integration/claim_external.test.ts +++ b/tests-integration/claim_external.test.ts @@ -1,11 +1,7 @@ -import { deployer, getClaimExternalData, manager } from "../lib"; +import { deployer, getClaimExternalData } from "../lib"; import { setupGift, setupGiftProtocol } from "./setupGift"; describe("claim_external", function () { - before(async () => { - await manager.declareLocalContract("ClaimAccount"); - }); - for (const useTxV3 of [false, true]) { it(`Testing claim_external flow using txV3: ${useTxV3}`, async function () { const { factory, claimAccountClassHash } = await setupGiftProtocol(); diff --git a/tests-integration/factory.test.ts b/tests-integration/factory.test.ts index 25283c6..bbb32bc 100644 --- a/tests-integration/factory.test.ts +++ b/tests-integration/factory.test.ts @@ -4,11 +4,6 @@ import { LegacyStarknetKeyPair, deployer, expectRevertWithErrorMessage, genericA import { GIFT_AMOUNT, GIFT_MAX_FEE, setupGift, setupGiftProtocol } from "./setupGift"; describe("Factory", function () { - let claimAccountClassHash: string; - before(async () => { - claimAccountClassHash = await manager.declareLocalContract("ClaimAccount"); - }); - for (const useTxV3 of [false, true]) { it(`get_dust: ${useTxV3}`, async function () { const { factory, claimAccountClassHash } = await setupGiftProtocol(); From 28ed1638e5153fc2bf1a012c88b988e2d8619b77 Mon Sep 17 00:00:00 2001 From: Leonard Paturel Date: Thu, 6 Jun 2024 11:06:32 +0100 Subject: [PATCH 10/29] remove more lib functions --- lib/accounts.ts | 68 +------------- lib/index.ts | 1 - lib/openZeppelinAccount.ts | 60 ------------- lib/signers/legacy.ts | 39 -------- lib/signers/signers.ts | 179 ------------------------------------- 5 files changed, 4 insertions(+), 343 deletions(-) delete mode 100644 lib/openZeppelinAccount.ts diff --git a/lib/accounts.ts b/lib/accounts.ts index ae14e88..417183a 100644 --- a/lib/accounts.ts +++ b/lib/accounts.ts @@ -1,67 +1,7 @@ -import { - Abi, - Account, - AllowArray, - Call, - CallData, - Contract, - DeployAccountContractPayload, - DeployContractResponse, - InvokeFunctionResponse, - RPC, - UniversalDetails, - num, - uint256, -} from "starknet"; +import { Account, Call, CallData, RPC, uint256 } from "starknet"; import { manager } from "./manager"; -import { KeyPair } from "./signers/signers"; import { ethAddress, strkAddress } from "./tokens"; -export class ArgentAccount extends Account { - // Increase the gas limit by 30% to avoid failures due to gas estimation being too low with tx v3 and transactions the use escaping - override async deployAccount( - payload: DeployAccountContractPayload, - details?: UniversalDetails, - ): Promise { - details ||= {}; - if (!details.skipValidate) { - details.skipValidate = false; - } - return super.deployAccount(payload, details); - } - - override async execute( - calls: AllowArray, - abis?: Abi[], - details: UniversalDetails = {}, - ): Promise { - details ||= {}; - if (!details.skipValidate) { - details.skipValidate = false; - } - if (details.resourceBounds) { - return super.execute(calls, abis, details); - } - const estimate = await this.estimateFee(calls, details); - return super.execute(calls, abis, { - ...details, - resourceBounds: { - ...estimate.resourceBounds, - l1_gas: { - ...estimate.resourceBounds.l1_gas, - max_amount: num.toHexString(num.addPercent(estimate.resourceBounds.l1_gas.max_amount, 30)), - }, - }, - }); - } -} - -export interface ArgentWallet { - account: ArgentAccount; - accountContract: Contract; - owner: KeyPair; -} - export const deployer = (() => { if (manager.isDevnet) { const devnetAddress = "0x64b48806902a367c8598f4f95c305e8c1a1acba5f082d294a43793113115691"; @@ -87,15 +27,15 @@ export const genericAccount = (() => { export const deployerV3 = setDefaultTransactionVersionV3(deployer); -export function setDefaultTransactionVersion(account: ArgentAccount, newVersion: boolean): Account { +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 ArgentAccount(account, account.address, account.signer, account.cairoVersion, newDefaultVersion); + return new Account(account, account.address, account.signer, account.cairoVersion, newDefaultVersion); } -export function setDefaultTransactionVersionV3(account: ArgentAccount): ArgentAccount { +export function setDefaultTransactionVersionV3(account: Account): Account { return setDefaultTransactionVersion(account, true); } diff --git a/lib/index.ts b/lib/index.ts index 5ba5b06..07452b0 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -10,7 +10,6 @@ export * from "./contracts"; export * from "./devnet"; export * from "./expectations"; export * from "./manager"; -export * from "./openZeppelinAccount"; export * from "./receipts"; export * from "./signers/legacy"; export * from "./signers/signers"; diff --git a/lib/openZeppelinAccount.ts b/lib/openZeppelinAccount.ts deleted file mode 100644 index b47df9c..0000000 --- a/lib/openZeppelinAccount.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { Account, CallData, RPC, hash, num } from "starknet"; -import { deployer, fundAccountCall } from "./accounts"; -import { ContractWithClass } from "./contracts"; -import { manager } from "./manager"; -import { LegacyMultisigSigner, LegacyStarknetKeyPair } from "./signers/legacy"; -import { randomStarknetKeyPair } from "./signers/signers"; - -export type DeployOzAccountParams = { - useTxV3?: boolean; - owner?: LegacyStarknetKeyPair; - salt?: string; - fundingAmount?: number | bigint; -}; - -export type DeployOzAccountResult = { - account: Account; - accountContract: ContractWithClass; - deployTxHash: string; - useTxV3: boolean; - owner: LegacyStarknetKeyPair; - salt: string; -}; - -export async function deployOpenZeppelinAccount(params: DeployOzAccountParams): Promise { - const classHash = await manager.declareLocalContract("AccountUpgradeable"); - const finalParams = { - ...params, - salt: params.salt ?? num.toHex(randomStarknetKeyPair().privateKey), - owner: params.owner ?? new LegacyStarknetKeyPair(), - useTxV3: params.useTxV3 ?? false, - }; - - const constructorCalldata = CallData.compile({ - owner: finalParams.owner.publicKey, - }); - - const contractAddress = hash.calculateContractAddressFromHash(finalParams.salt, classHash, constructorCalldata, 0); - - const fundingCall = finalParams.useTxV3 - ? await fundAccountCall(contractAddress, finalParams.fundingAmount ?? 1e18, "STRK") - : await fundAccountCall(contractAddress, finalParams.fundingAmount ?? 1e16, "ETH"); - const response = await deployer.execute([fundingCall!]); - await manager.waitForTransaction(response.transaction_hash); - - const defaultTxVersion = finalParams.useTxV3 ? RPC.ETransactionVersion.V3 : RPC.ETransactionVersion.V2; - const signer = new LegacyMultisigSigner([finalParams.owner]); - const account = new Account(manager, contractAddress, signer, "1", defaultTxVersion); - - const { transaction_hash: deployTxHash } = await account.deploySelf({ - classHash, - constructorCalldata, - addressSalt: finalParams.salt, - }); - - await manager.waitForTransaction(deployTxHash); - const accountContract = await manager.loadContract(account.address, classHash); - accountContract.connect(account); - - return { ...finalParams, account, accountContract, deployTxHash }; -} diff --git a/lib/signers/legacy.ts b/lib/signers/legacy.ts index 37a39b1..1132ce7 100644 --- a/lib/signers/legacy.ts +++ b/lib/signers/legacy.ts @@ -20,20 +20,6 @@ export class LegacyArgentSigner extends RawSigner { } } -export class LegacyMultisigSigner extends RawSigner { - constructor(public keys: RawSigner[]) { - super(); - } - - async signRaw(messageHash: string): Promise { - const keys = []; - for (const key of this.keys) { - keys.push(await key.signRaw(messageHash)); - } - return keys.flat(); - } -} - export abstract class LegacyKeyPair extends RawSigner { abstract get privateKey(): string; abstract get publicKey(): bigint; @@ -60,28 +46,3 @@ export class LegacyStarknetKeyPair extends LegacyKeyPair { return [r.toString(), s.toString()]; } } - -export class LegacyMultisigKeyPair extends LegacyKeyPair { - pk: string; - - constructor(pk?: string | bigint) { - super(); - this.pk = pk ? `${pk}` : `0x${encode.buf2hex(ec.starkCurve.utils.randomPrivateKey())}`; - } - - public get publicKey() { - return BigInt(ec.starkCurve.getStarkKey(this.pk)); - } - - public get privateKey(): string { - return this.pk; - } - - public async signRaw(messageHash: string): Promise { - const { r, s } = ec.starkCurve.sign(messageHash, this.pk); - return [this.publicKey.toString(), r.toString(), s.toString()]; - } -} - -export const randomLegacyMultisigKeyPairs = (length: number) => - Array.from({ length }, () => new LegacyMultisigKeyPair()).sort((n1, n2) => (n1.publicKey < n2.publicKey ? -1 : 1)); diff --git a/lib/signers/signers.ts b/lib/signers/signers.ts index 0f3c956..a6f4cd4 100644 --- a/lib/signers/signers.ts +++ b/lib/signers/signers.ts @@ -1,10 +1,6 @@ import { - CairoCustomEnum, - CairoOption, - CairoOptionVariant, Call, CallData, - Calldata, DeclareSignerDetails, DeployAccountSignerDetails, InvocationsSignerDetails, @@ -17,11 +13,7 @@ import { V3DeclareSignerDetails, V3DeployAccountSignerDetails, V3InvocationsSignerDetails, - ec, - encode, hash, - num, - shortString, stark, transaction, typedData, @@ -129,174 +121,3 @@ export abstract class RawSigner implements SignerInterface { return await this.signRaw(msgHash); } } - -export class MultisigSigner extends RawSigner { - constructor(public keys: KeyPair[]) { - super(); - } - - async signRaw(messageHash: string): Promise { - const keys = []; - for (const key of this.keys) { - keys.push(await key.signRaw(messageHash)); - } - return [keys.length.toString(), keys.flat()].flat(); - } -} - -export class ArgentSigner extends MultisigSigner { - constructor( - public owner: KeyPair = randomStarknetKeyPair(), - public guardian?: KeyPair, - ) { - const signers = [owner]; - if (guardian) { - signers.push(guardian); - } - super(signers); - } -} - -export abstract class KeyPair extends RawSigner { - abstract get signer(): CairoCustomEnum; - abstract get guid(): bigint; - abstract get storedValue(): bigint; - - public get compiledSigner(): Calldata { - return CallData.compile([this.signer]); - } - - public get signerAsOption() { - return new CairoOption(CairoOptionVariant.Some, { - signer: this.signer, - }); - } - public get compiledSignerAsOption() { - return CallData.compile([this.signerAsOption]); - } -} - -export class StarknetKeyPair extends KeyPair { - 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 get guid() { - return BigInt(hash.computePoseidonHash(shortString.encodeShortString("Starknet Signer"), this.publicKey)); - } - - public get storedValue() { - return this.publicKey; - } - - public get signer(): CairoCustomEnum { - return signerTypeToCustomEnum(SignerType.Starknet, { signer: this.publicKey }); - } - - public async signRaw(messageHash: string): Promise { - const { r, s } = ec.starkCurve.sign(messageHash, this.pk); - return starknetSignatureType(this.publicKey, r, s); - } -} - -export class EstimateStarknetKeyPair extends KeyPair { - readonly pubKey: bigint; - - constructor(pubKey: bigint) { - super(); - this.pubKey = pubKey; - } - - public get privateKey(): string { - throw new Error("EstimateStarknetKeyPair does not have a private key"); - } - - public get publicKey() { - return this.pubKey; - } - - public get guid() { - return BigInt(hash.computePoseidonHash(shortString.encodeShortString("Starknet Signer"), this.publicKey)); - } - - public get storedValue() { - return this.publicKey; - } - - public get signer(): CairoCustomEnum { - return signerTypeToCustomEnum(SignerType.Starknet, { signer: this.publicKey }); - } - - public async signRaw(messageHash: string): Promise { - const fakeR = "0x6cefb49a1f4eb406e8112db9b8cdf247965852ddc5ca4d74b09e42471689495"; - const fakeS = "0x25760910405a052b7f08ec533939c54948bc530c662c5d79e8ff416579087f7"; - return starknetSignatureType(this.publicKey, fakeR, fakeS); - } -} - -export function starknetSignatureType( - signer: bigint | number | string, - r: bigint | number | string, - s: bigint | number | string, -) { - return CallData.compile([signerTypeToCustomEnum(SignerType.Starknet, { signer, r, s })]); -} - -export function zeroStarknetSignatureType() { - return signerTypeToCustomEnum(SignerType.Starknet, { signer: 0 }); -} - -// reflects the signer type in signer_signature.cairo -// needs to be updated for the signer types -// used to convert signertype to guid -export enum SignerType { - Starknet, - Secp256k1, - Secp256r1, - Eip191, - Webauthn, -} - -export function signerTypeToCustomEnum(signerType: SignerType, value: any): CairoCustomEnum { - const contents = { - Starknet: undefined, - Secp256k1: undefined, - Secp256r1: undefined, - Eip191: undefined, - Webauthn: undefined, - }; - - if (signerType === SignerType.Starknet) { - contents.Starknet = value; - } else if (signerType === SignerType.Secp256k1) { - contents.Secp256k1 = value; - } else if (signerType === SignerType.Secp256r1) { - contents.Secp256r1 = value; - } else if (signerType === SignerType.Eip191) { - contents.Eip191 = value; - } else if (signerType === SignerType.Webauthn) { - contents.Webauthn = value; - } else { - throw new Error(`Unknown SignerType`); - } - - return new CairoCustomEnum(contents); -} - -export function sortByGuid(keys: KeyPair[]) { - return keys.sort((n1, n2) => (n1.guid < n2.guid ? -1 : 1)); -} - -export const randomStarknetKeyPair = () => new StarknetKeyPair(); -export const randomStarknetKeyPairs = (length: number) => Array.from({ length }, randomStarknetKeyPair); From ace7a2515a1b3264a5cc6696af136d208f6d9293 Mon Sep 17 00:00:00 2001 From: Leonard Paturel Date: Thu, 6 Jun 2024 11:27:59 +0100 Subject: [PATCH 11/29] move to lib --- {tests-integration => lib}/setupGift.ts | 2 +- tests-integration/account.test.ts | 2 +- tests-integration/factory.test.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) rename {tests-integration => lib}/setupGift.ts (98%) diff --git a/tests-integration/setupGift.ts b/lib/setupGift.ts similarity index 98% rename from tests-integration/setupGift.ts rename to lib/setupGift.ts index 370f993..9710302 100644 --- a/tests-integration/setupGift.ts +++ b/lib/setupGift.ts @@ -1,6 +1,6 @@ import { expect } from "chai"; import { Account, CallData, Contract, RPC, Uint256, ec, encode, hash, num, uint256 } from "starknet"; -import { LegacyStarknetKeyPair, deployer, manager } from "../lib"; +import { LegacyStarknetKeyPair, deployer, manager } from "."; export const GIFT_AMOUNT = 1000000000000000n; export const GIFT_MAX_FEE = 50000000000000n; diff --git a/tests-integration/account.test.ts b/tests-integration/account.test.ts index 81bc7a6..2842103 100644 --- a/tests-integration/account.test.ts +++ b/tests-integration/account.test.ts @@ -1,7 +1,7 @@ import { expect } from "chai"; import { num } from "starknet"; import { deployer, expectRevertWithErrorMessage, manager } from "../lib"; -import { GIFT_AMOUNT, GIFT_MAX_FEE, setupGift, setupGiftProtocol } from "./setupGift"; +import { GIFT_AMOUNT, GIFT_MAX_FEE, setupGift, setupGiftProtocol } from "../lib/setupGift"; describe("Gifting", function () { for (const useTxV3 of [false, true]) { diff --git a/tests-integration/factory.test.ts b/tests-integration/factory.test.ts index bbb32bc..c099514 100644 --- a/tests-integration/factory.test.ts +++ b/tests-integration/factory.test.ts @@ -1,7 +1,7 @@ import { expect } from "chai"; import { Account, RPC, num, uint256 } from "starknet"; import { LegacyStarknetKeyPair, deployer, expectRevertWithErrorMessage, genericAccount, manager } from "../lib"; -import { GIFT_AMOUNT, GIFT_MAX_FEE, setupGift, setupGiftProtocol } from "./setupGift"; +import { GIFT_AMOUNT, GIFT_MAX_FEE, setupGift, setupGiftProtocol } from "../lib/setupGift"; describe("Factory", function () { for (const useTxV3 of [false, true]) { From 2a884cb1a39363beb7592fbe3270e644a8cd1d65 Mon Sep 17 00:00:00 2001 From: Leonard Paturel Date: Thu, 6 Jun 2024 11:28:29 +0100 Subject: [PATCH 12/29] unused var --- tests-integration/account.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests-integration/account.test.ts b/tests-integration/account.test.ts index 2842103..ba7c04c 100644 --- a/tests-integration/account.test.ts +++ b/tests-integration/account.test.ts @@ -18,7 +18,7 @@ describe("Gifting", function () { it(`Test max fee too high`, async function () { const { factory, claimAccountClassHash } = await setupGiftProtocol(); - const { claimAccount, claim, tokenContract, receiver } = await setupGift(factory, claimAccountClassHash, useTxV3); + const { claimAccount, claim, receiver } = await setupGift(factory, claimAccountClassHash, useTxV3); if (useTxV3) { const estimate = await factory.estimateFee.claim_internal(claim, receiver); const newResourceBounds = { From a1a149fcd712feb4cef0c2b87c0d8016d32c1c9c Mon Sep 17 00:00:00 2001 From: Leonard Paturel Date: Thu, 6 Jun 2024 11:32:59 +0100 Subject: [PATCH 13/29] split validation tests --- tests-integration/account.test.ts | 28 +++++++++++++++++++++------- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/tests-integration/account.test.ts b/tests-integration/account.test.ts index ba7c04c..8cbf833 100644 --- a/tests-integration/account.test.ts +++ b/tests-integration/account.test.ts @@ -10,7 +10,6 @@ describe("Gifting", function () { const { claimAccount, claim, tokenContract, receiver } = await setupGift(factory, claimAccountClassHash, useTxV3); await factory.claim_internal(claim, receiver); - // Final check const finalBalance = await tokenContract.balance_of(claimAccount.address); expect(finalBalance < GIFT_MAX_FEE).to.be.true; await tokenContract.balance_of(receiver).should.eventually.equal(GIFT_AMOUNT); @@ -50,17 +49,19 @@ describe("Gifting", function () { }); } - it(`Test basic validation asserts`, async function () { + it(`Test only protocol can call claim contract`, async function () { const { factory, claimAccountClassHash } = await setupGiftProtocol(); const { claimAccount, claim, receiver } = await setupGift(factory, claimAccountClassHash); - const claimContract = await manager.loadContract(num.toHex(claimAccount.address)); - // only protocol claimContract.connect(claimAccount); await expectRevertWithErrorMessage("gift-acc/only-protocol", () => claimContract.__validate__([])); + }); + + it(`Test claim contract cant call another contract`, async function () { + const { factory, claimAccountClassHash } = await setupGiftProtocol(); + const { claimAccount, claim, receiver } = await setupGift(factory, claimAccountClassHash); - // cant call another contract const fakeFactory = await manager.deployContract("GiftFactory", { unique: true, constructorCalldata: [claimAccountClassHash, deployer.address], @@ -69,14 +70,22 @@ describe("Gifting", function () { await expectRevertWithErrorMessage("gift-acc/invalid-call-to", () => fakeFactory.claim_internal(claim, receiver, { maxFee: 400000000000000n }), ); + }); + + it(`Test claim contract can only call 'claim_internal'`, async function () { + const { factory, claimAccountClassHash } = await setupGiftProtocol(); + const { claimAccount, claim, receiver } = await setupGift(factory, claimAccountClassHash); - // wrong selector factory.connect(claimAccount); await expectRevertWithErrorMessage("gift-acc/invalid-call-selector", () => factory.get_dust(claim, receiver, { maxFee: 400000000000000n }), ); + }); + + it(`Test claim contract cant preform a multicall`, async function () { + const { factory, claimAccountClassHash } = await setupGiftProtocol(); + const { claimAccount, claim, receiver } = await setupGift(factory, claimAccountClassHash); - // multicall await expectRevertWithErrorMessage("gift-acc/invalid-call-len", () => claimAccount.execute([ { @@ -91,6 +100,11 @@ describe("Gifting", function () { }, ]), ); + }); + + it(`Test cannot call 'claim_internal' twice`, async function () { + const { factory, claimAccountClassHash } = await setupGiftProtocol(); + const { claimAccount, claim, receiver } = await setupGift(factory, claimAccountClassHash); // double claim await factory.claim_internal(claim, receiver); From 5ed73a60b9f575fffcfbea8d7c44962d8db4cc8f Mon Sep 17 00:00:00 2001 From: Leonard Paturel Date: Thu, 6 Jun 2024 11:34:54 +0100 Subject: [PATCH 14/29] fixed import --- lib/index.ts | 1 + tests-integration/account.test.ts | 11 +++++++++-- tests-integration/claim_external.test.ts | 3 +-- tests-integration/factory.test.ts | 13 +++++++++++-- 4 files changed, 22 insertions(+), 6 deletions(-) diff --git a/lib/index.ts b/lib/index.ts index 07452b0..b0799c8 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -11,6 +11,7 @@ export * from "./devnet"; export * from "./expectations"; export * from "./manager"; export * from "./receipts"; +export * from "./setupGift"; export * from "./signers/legacy"; export * from "./signers/signers"; export * from "./tokens"; diff --git a/tests-integration/account.test.ts b/tests-integration/account.test.ts index 8cbf833..12b5361 100644 --- a/tests-integration/account.test.ts +++ b/tests-integration/account.test.ts @@ -1,7 +1,14 @@ import { expect } from "chai"; import { num } from "starknet"; -import { deployer, expectRevertWithErrorMessage, manager } from "../lib"; -import { GIFT_AMOUNT, GIFT_MAX_FEE, setupGift, setupGiftProtocol } from "../lib/setupGift"; +import { + GIFT_AMOUNT, + GIFT_MAX_FEE, + deployer, + expectRevertWithErrorMessage, + manager, + setupGift, + setupGiftProtocol, +} from "../lib"; describe("Gifting", function () { for (const useTxV3 of [false, true]) { diff --git a/tests-integration/claim_external.test.ts b/tests-integration/claim_external.test.ts index 801f5fe..7da74ae 100644 --- a/tests-integration/claim_external.test.ts +++ b/tests-integration/claim_external.test.ts @@ -1,5 +1,4 @@ -import { deployer, getClaimExternalData } from "../lib"; -import { setupGift, setupGiftProtocol } from "./setupGift"; +import { deployer, getClaimExternalData, setupGift, setupGiftProtocol } from "../lib"; describe("claim_external", function () { for (const useTxV3 of [false, true]) { diff --git a/tests-integration/factory.test.ts b/tests-integration/factory.test.ts index c099514..5285f9b 100644 --- a/tests-integration/factory.test.ts +++ b/tests-integration/factory.test.ts @@ -1,7 +1,16 @@ import { expect } from "chai"; import { Account, RPC, num, uint256 } from "starknet"; -import { LegacyStarknetKeyPair, deployer, expectRevertWithErrorMessage, genericAccount, manager } from "../lib"; -import { GIFT_AMOUNT, GIFT_MAX_FEE, setupGift, setupGiftProtocol } from "../lib/setupGift"; +import { + GIFT_AMOUNT, + GIFT_MAX_FEE, + LegacyStarknetKeyPair, + deployer, + expectRevertWithErrorMessage, + genericAccount, + manager, + setupGift, + setupGiftProtocol, +} from "../lib"; describe("Factory", function () { for (const useTxV3 of [false, true]) { From f8b40cc50d7d57dedf021aef774e10bf17e4a457 Mon Sep 17 00:00:00 2001 From: Leonard Paturel Date: Thu, 6 Jun 2024 15:55:05 +0100 Subject: [PATCH 15/29] setup function --- lib/claim.ts | 89 ++++++++++++++++++- lib/deposit.ts | 68 +++++++++++++++ lib/index.ts | 3 +- lib/protocol.ts | 21 +++++ lib/setupGift.ts | 105 ----------------------- tests-integration/account.test.ts | 28 +++--- tests-integration/claim_external.test.ts | 8 +- tests-integration/factory.test.ts | 49 +++++++++-- 8 files changed, 239 insertions(+), 132 deletions(-) create mode 100644 lib/deposit.ts create mode 100644 lib/protocol.ts delete mode 100644 lib/setupGift.ts diff --git a/lib/claim.ts b/lib/claim.ts index 1f26b67..fa18cf0 100644 --- a/lib/claim.ts +++ b/lib/claim.ts @@ -1,5 +1,6 @@ -import { shortString } from "starknet"; -import { manager } from "."; +import { Account, Contract, RPC, Uint256, ec, encode, num, shortString, uint256 } from "starknet"; +import { LegacyStarknetKeyPair, deployer, manager } from "."; +import { GIFT_AMOUNT, GIFT_MAX_FEE } from "./deposit"; const typesRev1 = { StarknetDomain: [ @@ -35,3 +36,87 @@ export async function getClaimExternalData(claimExternal: ClaimExternal) { message: { ...claimExternal }, }; } + +export function buildClaim( + factory: Contract, + claimAccountClassHash: string, + giftAmount: bigint, + giftMaxFee: bigint, + tokenContract: Contract, + claimSigner: LegacyStarknetKeyPair, +): Claim { + const constructorArgs: AccountConstructorArguments = { + sender: deployer.address, + amount: uint256.bnToUint256(giftAmount), + max_fee: giftMaxFee, + token: tokenContract.address, + claim_pubkey: claimSigner.publicKey, + }; + return { + factory: factory.address, + class_hash: claimAccountClassHash, + ...constructorArgs, + }; +} + +export async function claimExternal( + factory: Contract, + receiver: string, + claim: Claim, + claimAccountAddress: string, + giftSigner: LegacyStarknetKeyPair, + account = deployer, +): Promise { + const claimExternalData = await getClaimExternalData({ receiver }); + const signature = await giftSigner.signMessage(claimExternalData, claimAccountAddress); + + factory.connect(account); + const { transaction_hash } = await factory.claim_external(claim, receiver, signature); + return transaction_hash; +} + +export interface AccountConstructorArguments { + sender: string; + amount: Uint256; + max_fee: bigint; + token: string; + claim_pubkey: bigint; +} + +export interface Claim extends AccountConstructorArguments { + factory: string; + class_hash: string; +} + +export async function claimInternal( + factory: Contract, + tokenContract: Contract, + claimSigner: LegacyStarknetKeyPair, + receiverAddress = "0x42", + forcedGiftPrivateKey = false, + useTxV3 = false, + giftAmount = GIFT_AMOUNT, + giftMaxFee = GIFT_MAX_FEE, +): Promise<{ + claimAccount: Account; + receiver: string; +}> { + const receiver = forcedGiftPrivateKey + ? receiverAddress + : `0x${encode.buf2hex(ec.starkCurve.utils.randomPrivateKey())}`; + const claimAccountClassHash = await manager.declareLocalContract("ClaimAccount"); + const claimAddress = await factory.get_claim_address( + claimAccountClassHash, + deployer.address, + giftAmount, + giftMaxFee, + tokenContract.address, + claimSigner.publicKey, + ); + const claim = buildClaim(factory, claimAccountClassHash, giftAmount, giftMaxFee, tokenContract, claimSigner); + const txVersion = useTxV3 ? RPC.ETransactionVersion.V3 : RPC.ETransactionVersion.V2; + const claimAccount = new Account(manager, num.toHex(claimAddress), claimSigner, undefined, txVersion); + factory.connect(claimAccount); + await factory.claim_internal(claim, receiver); + return { claimAccount, receiver }; +} diff --git a/lib/deposit.ts b/lib/deposit.ts new file mode 100644 index 0000000..8f64992 --- /dev/null +++ b/lib/deposit.ts @@ -0,0 +1,68 @@ +import { Account, CallData, Contract, RPC, ec, encode, hash, num, uint256 } from "starknet"; +import { AccountConstructorArguments, Claim, LegacyStarknetKeyPair, buildClaim, deployer, manager } from "./"; + +export const GIFT_AMOUNT = 1000000000000000n; +export const GIFT_MAX_FEE = 50000000000000n; + +export async function deposit( + factory: Contract, + tokenContract: Contract, + claimSignerPubKey: bigint, + sender = deployer, + amount = GIFT_AMOUNT, + feeAmount = GIFT_MAX_FEE, +) { + // Make a gift + tokenContract.connect(sender); + factory.connect(sender); + await sender.execute([ + tokenContract.populateTransaction.approve(factory.address, amount + feeAmount), + factory.populateTransaction.deposit(amount, feeAmount, tokenContract.address, claimSignerPubKey), + ]); +} + +export async function defaultDepositTestSetup( + factory: Contract, + useTxV3 = false, + forcedGiftPrivateKey = false, + giftAmount = GIFT_AMOUNT, + giftMaxFee = GIFT_MAX_FEE, +): Promise<{ + claimAccount: Account; + tokenContract: Contract; + claimSigner: LegacyStarknetKeyPair; + claim: Claim; + receiver: string; +}> { + const tokenContract = await manager.tokens.feeTokenContract(useTxV3); + + // static signer / receiver for gas profiling + const receiver = forcedGiftPrivateKey ? "0x42" : `0x${encode.buf2hex(ec.starkCurve.utils.randomPrivateKey())}`; + const claimSigner = new LegacyStarknetKeyPair(forcedGiftPrivateKey ? "0x42" : undefined); + const claimPubKey = claimSigner.publicKey; + await deposit(factory, tokenContract, claimPubKey); + + const claimAccountClassHash = await manager.declareLocalContract("ClaimAccount"); + + const constructorArgs: AccountConstructorArguments = { + sender: deployer.address, + amount: uint256.bnToUint256(giftAmount), + max_fee: giftMaxFee, + token: tokenContract.address, + claim_pubkey: claimSigner.publicKey, + }; + + const claimAddress = hash.calculateContractAddressFromHash( + 0, + claimAccountClassHash, + CallData.compile({ constructorArgs }), + factory.address, + ); + + const claim = buildClaim(factory, claimAccountClassHash, giftAmount, giftMaxFee, tokenContract, claimSigner); + + const txVersion = useTxV3 ? RPC.ETransactionVersion.V3 : RPC.ETransactionVersion.V2; + const claimAccount = new Account(manager, num.toHex(claimAddress), claimSigner, undefined, txVersion); + factory.connect(claimAccount); + return { claimAccount, tokenContract, claimSigner, claim, receiver }; +} diff --git a/lib/index.ts b/lib/index.ts index b0799c8..04c710a 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -7,11 +7,12 @@ 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 "./setupGift"; export * from "./signers/legacy"; export * from "./signers/signers"; export * from "./tokens"; diff --git a/lib/protocol.ts b/lib/protocol.ts new file mode 100644 index 0000000..bf9f1f5 --- /dev/null +++ b/lib/protocol.ts @@ -0,0 +1,21 @@ +import { Contract } from "starknet"; +import { deployer, manager } from "."; + +const cache: Record = {}; + +export async function setupGiftProtocol(): Promise<{ + factory: Contract; + claimAccountClassHash: string; +}> { + const claimAccountClassHash = await manager.declareLocalContract("ClaimAccount"); + const cachedFactory = cache["GiftFactory"]; + if (cachedFactory) { + return { factory: cachedFactory, claimAccountClassHash }; + } + const factory = await manager.deployContract("GiftFactory", { + unique: true, + constructorCalldata: [claimAccountClassHash, deployer.address], + }); + cache["GiftFactory"] = factory; + return { factory, claimAccountClassHash }; +} diff --git a/lib/setupGift.ts b/lib/setupGift.ts deleted file mode 100644 index 9710302..0000000 --- a/lib/setupGift.ts +++ /dev/null @@ -1,105 +0,0 @@ -import { expect } from "chai"; -import { Account, CallData, Contract, RPC, Uint256, ec, encode, hash, num, uint256 } from "starknet"; -import { LegacyStarknetKeyPair, deployer, manager } from "."; - -export const GIFT_AMOUNT = 1000000000000000n; -export const GIFT_MAX_FEE = 50000000000000n; - -interface AccountConstructorArguments { - sender: string; - amount: Uint256; - max_fee: bigint; - token: string; - claim_pubkey: bigint; -} - -interface Claim extends AccountConstructorArguments { - factory: string; - class_hash: string; -} - -const cache: Record = {}; - -export async function setupGiftProtocol(): Promise<{ - factory: Contract; - claimAccountClassHash: string; -}> { - const claimAccountClassHash = await manager.declareLocalContract("ClaimAccount"); - const cachedFactory = cache["GiftFactory"]; - if (cachedFactory) { - return { factory: cachedFactory, claimAccountClassHash }; - } - const factory = await manager.deployContract("GiftFactory", { - unique: true, - constructorCalldata: [claimAccountClassHash, deployer.address], - }); - cache["GiftFactory"] = factory; - return { factory, claimAccountClassHash }; -} - -export async function setupGift( - factory: Contract, - claimAccountClassHash: string, - useTxV3 = false, - useRandom = true, -): Promise<{ - claimAccount: Account; - claim: Claim; - tokenContract: Contract; - receiver: string; - giftSigner: LegacyStarknetKeyPair; -}> { - // static receiver / signer for gas profiling - const giftSigner = new LegacyStarknetKeyPair(useRandom ? undefined : "0x42"); - const claimPubKey = giftSigner.publicKey; - const receiver = useRandom ? `0x${encode.buf2hex(ec.starkCurve.utils.randomPrivateKey())}` : "0x42"; - - // Make a gift - const tokenContract = await manager.tokens.feeTokenContract(useTxV3); - tokenContract.connect(deployer); - factory.connect(deployer); - await tokenContract.approve(factory.address, GIFT_AMOUNT + GIFT_MAX_FEE); - await factory.deposit(GIFT_AMOUNT, GIFT_MAX_FEE, tokenContract.address, claimPubKey); - - // Ensure there is a contract for the claim - const claimAddress = await factory.get_claim_address( - claimAccountClassHash, - deployer.address, - GIFT_AMOUNT, - GIFT_MAX_FEE, - tokenContract.address, - claimPubKey, - ); - - const constructorArgs: AccountConstructorArguments = { - sender: deployer.address, - amount: uint256.bnToUint256(GIFT_AMOUNT), - max_fee: GIFT_MAX_FEE, - token: tokenContract.address, - claim_pubkey: claimPubKey, - }; - - const claim: Claim = { - factory: factory.address, - class_hash: claimAccountClassHash, - ...constructorArgs, - }; - - const correctAddress = hash.calculateContractAddressFromHash( - 0, - claimAccountClassHash, - CallData.compile({ constructorArgs }), - factory.address, - ); - expect(claimAddress).to.be.equal(num.toBigInt(correctAddress)); - - // Check balance of the claim contract is correct - await tokenContract.balance_of(claimAddress).should.eventually.equal(GIFT_AMOUNT + GIFT_MAX_FEE); - // Check balance receiver address == 0 - await tokenContract.balance_of(receiver).should.eventually.equal(0n); - - const txVersion = useTxV3 ? RPC.ETransactionVersion.V3 : RPC.ETransactionVersion.V2; - const claimAccount = new Account(manager, num.toHex(claimAddress), giftSigner, undefined, txVersion); - factory.connect(claimAccount); - return { claimAccount, claim, tokenContract, receiver, giftSigner }; -} diff --git a/tests-integration/account.test.ts b/tests-integration/account.test.ts index 12b5361..a475edd 100644 --- a/tests-integration/account.test.ts +++ b/tests-integration/account.test.ts @@ -3,18 +3,18 @@ import { num } from "starknet"; import { GIFT_AMOUNT, GIFT_MAX_FEE, + defaultDepositTestSetup, deployer, expectRevertWithErrorMessage, manager, - setupGift, setupGiftProtocol, } from "../lib"; describe("Gifting", function () { for (const useTxV3 of [false, true]) { it(`Testing simple claim flow using txV3: ${useTxV3}`, async function () { - const { factory, claimAccountClassHash } = await setupGiftProtocol(); - const { claimAccount, claim, tokenContract, receiver } = await setupGift(factory, claimAccountClassHash, useTxV3); + const { factory } = await setupGiftProtocol(); + const { claim, receiver, claimAccount, tokenContract } = await defaultDepositTestSetup(factory); await factory.claim_internal(claim, receiver); const finalBalance = await tokenContract.balance_of(claimAccount.address); @@ -23,8 +23,8 @@ describe("Gifting", function () { }); it(`Test max fee too high`, async function () { - const { factory, claimAccountClassHash } = await setupGiftProtocol(); - const { claimAccount, claim, receiver } = await setupGift(factory, claimAccountClassHash, useTxV3); + const { factory } = await setupGiftProtocol(); + const { claim, receiver, claimAccount } = await defaultDepositTestSetup(factory, useTxV3); if (useTxV3) { const estimate = await factory.estimateFee.claim_internal(claim, receiver); const newResourceBounds = { @@ -57,8 +57,8 @@ describe("Gifting", function () { } it(`Test only protocol can call claim contract`, async function () { - const { factory, claimAccountClassHash } = await setupGiftProtocol(); - const { claimAccount, claim, receiver } = await setupGift(factory, claimAccountClassHash); + const { factory } = await setupGiftProtocol(); + const { claimAccount } = await defaultDepositTestSetup(factory); const claimContract = await manager.loadContract(num.toHex(claimAccount.address)); claimContract.connect(claimAccount); @@ -67,7 +67,7 @@ describe("Gifting", function () { it(`Test claim contract cant call another contract`, async function () { const { factory, claimAccountClassHash } = await setupGiftProtocol(); - const { claimAccount, claim, receiver } = await setupGift(factory, claimAccountClassHash); + const { claim, receiver, claimAccount } = await defaultDepositTestSetup(factory); const fakeFactory = await manager.deployContract("GiftFactory", { unique: true, @@ -80,8 +80,8 @@ describe("Gifting", function () { }); it(`Test claim contract can only call 'claim_internal'`, async function () { - const { factory, claimAccountClassHash } = await setupGiftProtocol(); - const { claimAccount, claim, receiver } = await setupGift(factory, claimAccountClassHash); + const { factory } = await setupGiftProtocol(); + const { claim, receiver, claimAccount } = await defaultDepositTestSetup(factory); factory.connect(claimAccount); await expectRevertWithErrorMessage("gift-acc/invalid-call-selector", () => @@ -90,8 +90,8 @@ describe("Gifting", function () { }); it(`Test claim contract cant preform a multicall`, async function () { - const { factory, claimAccountClassHash } = await setupGiftProtocol(); - const { claimAccount, claim, receiver } = await setupGift(factory, claimAccountClassHash); + const { factory } = await setupGiftProtocol(); + const { claim, receiver, claimAccount } = await defaultDepositTestSetup(factory); await expectRevertWithErrorMessage("gift-acc/invalid-call-len", () => claimAccount.execute([ @@ -110,8 +110,8 @@ describe("Gifting", function () { }); it(`Test cannot call 'claim_internal' twice`, async function () { - const { factory, claimAccountClassHash } = await setupGiftProtocol(); - const { claimAccount, claim, receiver } = await setupGift(factory, claimAccountClassHash); + const { factory } = await setupGiftProtocol(); + const { claim, receiver, claimAccount } = await defaultDepositTestSetup(factory); // double claim await factory.claim_internal(claim, receiver); diff --git a/tests-integration/claim_external.test.ts b/tests-integration/claim_external.test.ts index 7da74ae..74502b0 100644 --- a/tests-integration/claim_external.test.ts +++ b/tests-integration/claim_external.test.ts @@ -1,13 +1,13 @@ -import { deployer, getClaimExternalData, setupGift, setupGiftProtocol } from "../lib"; +import { defaultDepositTestSetup, deployer, getClaimExternalData, setupGiftProtocol } from "../lib"; describe("claim_external", function () { for (const useTxV3 of [false, true]) { it(`Testing claim_external flow using txV3: ${useTxV3}`, async function () { - const { factory, claimAccountClassHash } = await setupGiftProtocol(); - const { claimAccount, claim, receiver, giftSigner } = await setupGift(factory, claimAccountClassHash, useTxV3); + const { factory } = await setupGiftProtocol(); + const { claim, receiver, claimAccount, claimSigner } = await defaultDepositTestSetup(factory); const claimExternalData = await getClaimExternalData({ receiver }); - const signature = await giftSigner.signMessage(claimExternalData, claimAccount.address); + const signature = await claimSigner.signMessage(claimExternalData, claimAccount.address); factory.connect(deployer); await factory.claim_external(claim, receiver, signature); diff --git a/tests-integration/factory.test.ts b/tests-integration/factory.test.ts index 5285f9b..c838e19 100644 --- a/tests-integration/factory.test.ts +++ b/tests-integration/factory.test.ts @@ -1,22 +1,59 @@ import { expect } from "chai"; -import { Account, RPC, num, uint256 } from "starknet"; +import { Account, CallData, RPC, hash, num, uint256 } from "starknet"; import { + AccountConstructorArguments, GIFT_AMOUNT, GIFT_MAX_FEE, LegacyStarknetKeyPair, + defaultDepositTestSetup, deployer, expectRevertWithErrorMessage, genericAccount, manager, - setupGift, setupGiftProtocol, } from "../lib"; describe("Factory", function () { + it(`Test calculate claim address`, async function () { + const { factory, claimAccountClassHash } = await setupGiftProtocol(); + const { tokenContract, claimSigner } = await defaultDepositTestSetup(factory); + const a = factory.populateTransaction.get_claim_address( + claimAccountClassHash, + deployer.address, + GIFT_AMOUNT, + GIFT_MAX_FEE, + tokenContract.address, + claimSigner.publicKey, + ); + const claimAddress = await factory.get_claim_address( + claimAccountClassHash, + deployer.address, + GIFT_AMOUNT, + GIFT_MAX_FEE, + tokenContract.address, + claimSigner.publicKey, + ); + + const constructorArgs: AccountConstructorArguments = { + sender: deployer.address, + amount: uint256.bnToUint256(GIFT_AMOUNT), + max_fee: GIFT_MAX_FEE, + token: tokenContract.address, + claim_pubkey: claimSigner.publicKey, + }; + + const correctAddress = hash.calculateContractAddressFromHash( + 0, + claimAccountClassHash, + CallData.compile({ constructorArgs }), + factory.address, + ); + expect(claimAddress).to.be.equal(num.toBigInt(correctAddress)); + }); for (const useTxV3 of [false, true]) { it(`get_dust: ${useTxV3}`, async function () { - const { factory, claimAccountClassHash } = await setupGiftProtocol(); - const { claimAccount, claim, tokenContract, receiver } = await setupGift(factory, claimAccountClassHash, useTxV3); + const { factory } = await setupGiftProtocol(); + const { tokenContract, claim, receiver, claimAccount } = await defaultDepositTestSetup(factory); const receiverDust = `0x2${Math.floor(Math.random() * 1000)}`; await factory.claim_internal(claim, receiver); @@ -37,8 +74,8 @@ describe("Factory", function () { } it(`Test Cancel Claim`, async function () { - const { factory, claimAccountClassHash } = await setupGiftProtocol(); - const { claimAccount, claim, tokenContract, receiver } = await setupGift(factory, claimAccountClassHash); + const { factory } = await setupGiftProtocol(); + const { tokenContract, claim, receiver, claimAccount } = await defaultDepositTestSetup(factory); const balanceSenderBefore = await tokenContract.balance_of(deployer.address); factory.connect(deployer); From 09904867058563c051cbf820c6346cb4a684fca1 Mon Sep 17 00:00:00 2001 From: Leonard Paturel Date: Thu, 6 Jun 2024 16:07:51 +0100 Subject: [PATCH 16/29] claimInternal var change --- lib/claim.ts | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/lib/claim.ts b/lib/claim.ts index fa18cf0..0e74d24 100644 --- a/lib/claim.ts +++ b/lib/claim.ts @@ -92,8 +92,7 @@ export async function claimInternal( factory: Contract, tokenContract: Contract, claimSigner: LegacyStarknetKeyPair, - receiverAddress = "0x42", - forcedGiftPrivateKey = false, + forcedGiftPrivateKey = "0x42", useTxV3 = false, giftAmount = GIFT_AMOUNT, giftMaxFee = GIFT_MAX_FEE, @@ -101,9 +100,7 @@ export async function claimInternal( claimAccount: Account; receiver: string; }> { - const receiver = forcedGiftPrivateKey - ? receiverAddress - : `0x${encode.buf2hex(ec.starkCurve.utils.randomPrivateKey())}`; + const receiver = forcedGiftPrivateKey || `0x${encode.buf2hex(ec.starkCurve.utils.randomPrivateKey())}`; const claimAccountClassHash = await manager.declareLocalContract("ClaimAccount"); const claimAddress = await factory.get_claim_address( claimAccountClassHash, From 48c3b2b3ac43f4b2039557e9c745ec717aecdaef Mon Sep 17 00:00:00 2001 From: Leonard Paturel Date: Thu, 6 Jun 2024 16:08:54 +0100 Subject: [PATCH 17/29] reorder claim.ts --- lib/claim.ts | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/lib/claim.ts b/lib/claim.ts index 0e74d24..0ca1bd8 100644 --- a/lib/claim.ts +++ b/lib/claim.ts @@ -37,6 +37,19 @@ export async function getClaimExternalData(claimExternal: ClaimExternal) { }; } +export interface AccountConstructorArguments { + sender: string; + amount: Uint256; + max_fee: bigint; + token: string; + claim_pubkey: bigint; +} + +export interface Claim extends AccountConstructorArguments { + factory: string; + class_hash: string; +} + export function buildClaim( factory: Contract, claimAccountClassHash: string, @@ -75,19 +88,6 @@ export async function claimExternal( return transaction_hash; } -export interface AccountConstructorArguments { - sender: string; - amount: Uint256; - max_fee: bigint; - token: string; - claim_pubkey: bigint; -} - -export interface Claim extends AccountConstructorArguments { - factory: string; - class_hash: string; -} - export async function claimInternal( factory: Contract, tokenContract: Contract, From c23cf51a85d7826171fb18d7fafed70714fb0a13 Mon Sep 17 00:00:00 2001 From: Leonard Paturel Date: Thu, 6 Jun 2024 16:10:48 +0100 Subject: [PATCH 18/29] better naming --- lib/claim.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/claim.ts b/lib/claim.ts index 0ca1bd8..55f250c 100644 --- a/lib/claim.ts +++ b/lib/claim.ts @@ -92,7 +92,7 @@ export async function claimInternal( factory: Contract, tokenContract: Contract, claimSigner: LegacyStarknetKeyPair, - forcedGiftPrivateKey = "0x42", + giftPrivateKey = "0x42", useTxV3 = false, giftAmount = GIFT_AMOUNT, giftMaxFee = GIFT_MAX_FEE, @@ -100,7 +100,7 @@ export async function claimInternal( claimAccount: Account; receiver: string; }> { - const receiver = forcedGiftPrivateKey || `0x${encode.buf2hex(ec.starkCurve.utils.randomPrivateKey())}`; + const receiver = giftPrivateKey || `0x${encode.buf2hex(ec.starkCurve.utils.randomPrivateKey())}`; const claimAccountClassHash = await manager.declareLocalContract("ClaimAccount"); const claimAddress = await factory.get_claim_address( claimAccountClassHash, From 0eb6f40d15721b508bd13ee85971ad219c39796f Mon Sep 17 00:00:00 2001 From: Leonard Paturel Date: Thu, 6 Jun 2024 16:57:36 +0100 Subject: [PATCH 19/29] only pass in pub/priv key --- lib/claim.ts | 17 +++++++++-------- lib/deposit.ts | 9 ++++++++- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/lib/claim.ts b/lib/claim.ts index 55f250c..e0dcb10 100644 --- a/lib/claim.ts +++ b/lib/claim.ts @@ -56,14 +56,14 @@ export function buildClaim( giftAmount: bigint, giftMaxFee: bigint, tokenContract: Contract, - claimSigner: LegacyStarknetKeyPair, + claimPubkey: bigint, ): Claim { const constructorArgs: AccountConstructorArguments = { sender: deployer.address, amount: uint256.bnToUint256(giftAmount), max_fee: giftMaxFee, token: tokenContract.address, - claim_pubkey: claimSigner.publicKey, + claim_pubkey: claimPubkey, }; return { factory: factory.address, @@ -91,8 +91,9 @@ export async function claimExternal( export async function claimInternal( factory: Contract, tokenContract: Contract, - claimSigner: LegacyStarknetKeyPair, - giftPrivateKey = "0x42", + claimSignerPrivateKey: string, + claimSignerPublicKey: bigint, + receiverAddress = "0x42", useTxV3 = false, giftAmount = GIFT_AMOUNT, giftMaxFee = GIFT_MAX_FEE, @@ -100,7 +101,7 @@ export async function claimInternal( claimAccount: Account; receiver: string; }> { - const receiver = giftPrivateKey || `0x${encode.buf2hex(ec.starkCurve.utils.randomPrivateKey())}`; + const receiver = receiverAddress || `0x${encode.buf2hex(ec.starkCurve.utils.randomPrivateKey())}`; const claimAccountClassHash = await manager.declareLocalContract("ClaimAccount"); const claimAddress = await factory.get_claim_address( claimAccountClassHash, @@ -108,11 +109,11 @@ export async function claimInternal( giftAmount, giftMaxFee, tokenContract.address, - claimSigner.publicKey, + claimSignerPublicKey, ); - const claim = buildClaim(factory, claimAccountClassHash, giftAmount, giftMaxFee, tokenContract, claimSigner); + const claim = buildClaim(factory, claimAccountClassHash, giftAmount, giftMaxFee, tokenContract, claimSignerPublicKey); const txVersion = useTxV3 ? RPC.ETransactionVersion.V3 : RPC.ETransactionVersion.V2; - const claimAccount = new Account(manager, num.toHex(claimAddress), claimSigner, undefined, txVersion); + const claimAccount = new Account(manager, num.toHex(claimAddress), claimSignerPrivateKey, undefined, txVersion); factory.connect(claimAccount); await factory.claim_internal(claim, receiver); return { claimAccount, receiver }; diff --git a/lib/deposit.ts b/lib/deposit.ts index 8f64992..9c8bf6b 100644 --- a/lib/deposit.ts +++ b/lib/deposit.ts @@ -59,7 +59,14 @@ export async function defaultDepositTestSetup( factory.address, ); - const claim = buildClaim(factory, claimAccountClassHash, giftAmount, giftMaxFee, tokenContract, claimSigner); + const claim = buildClaim( + factory, + claimAccountClassHash, + giftAmount, + giftMaxFee, + tokenContract, + claimSigner.publicKey, + ); const txVersion = useTxV3 ? RPC.ETransactionVersion.V3 : RPC.ETransactionVersion.V2; const claimAccount = new Account(manager, num.toHex(claimAddress), claimSigner, undefined, txVersion); From 1f518b6b7b85f850969c6869ddca10f596982d05 Mon Sep 17 00:00:00 2001 From: Leonard Paturel Date: Thu, 6 Jun 2024 17:01:10 +0100 Subject: [PATCH 20/29] claim external update --- lib/claim.ts | 17 +++++++++++++++-- tests-integration/factory.test.ts | 8 -------- 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/lib/claim.ts b/lib/claim.ts index e0dcb10..e26076a 100644 --- a/lib/claim.ts +++ b/lib/claim.ts @@ -1,4 +1,4 @@ -import { Account, Contract, RPC, Uint256, ec, encode, num, shortString, uint256 } from "starknet"; +import { Account, CallData, Contract, RPC, Uint256, ec, encode, hash, num, shortString, uint256 } from "starknet"; import { LegacyStarknetKeyPair, deployer, manager } from "."; import { GIFT_AMOUNT, GIFT_MAX_FEE } from "./deposit"; @@ -76,10 +76,23 @@ export async function claimExternal( factory: Contract, receiver: string, claim: Claim, - claimAccountAddress: string, giftSigner: LegacyStarknetKeyPair, account = deployer, ): Promise { + const constructorArgs: AccountConstructorArguments = { + sender: deployer.address, + amount: claim.amount, + max_fee: claim.max_fee, + token: claim.token, + claim_pubkey: claim.claim_pubkey, + }; + const claimAccountAddress = hash.calculateContractAddressFromHash( + 0, + claim.class_hash, + CallData.compile({ constructorArgs }), + factory.address, + ); + const claimExternalData = await getClaimExternalData({ receiver }); const signature = await giftSigner.signMessage(claimExternalData, claimAccountAddress); diff --git a/tests-integration/factory.test.ts b/tests-integration/factory.test.ts index c838e19..7813926 100644 --- a/tests-integration/factory.test.ts +++ b/tests-integration/factory.test.ts @@ -17,14 +17,6 @@ describe("Factory", function () { it(`Test calculate claim address`, async function () { const { factory, claimAccountClassHash } = await setupGiftProtocol(); const { tokenContract, claimSigner } = await defaultDepositTestSetup(factory); - const a = factory.populateTransaction.get_claim_address( - claimAccountClassHash, - deployer.address, - GIFT_AMOUNT, - GIFT_MAX_FEE, - tokenContract.address, - claimSigner.publicKey, - ); const claimAddress = await factory.get_claim_address( claimAccountClassHash, deployer.address, From 9f50df282f5c825474cf61365c860a9040777778 Mon Sep 17 00:00:00 2001 From: Leonard Paturel Date: Fri, 7 Jun 2024 10:39:09 +0100 Subject: [PATCH 21/29] comments --- lib/claim.ts | 32 ++++++++++++++++++------------- lib/deposit.ts | 29 +++++++--------------------- tests-integration/account.test.ts | 8 +++++--- 3 files changed, 31 insertions(+), 38 deletions(-) diff --git a/lib/claim.ts b/lib/claim.ts index e26076a..a5df4ba 100644 --- a/lib/claim.ts +++ b/lib/claim.ts @@ -1,4 +1,15 @@ -import { Account, CallData, Contract, RPC, Uint256, ec, encode, hash, num, shortString, uint256 } from "starknet"; +import { + Account, + CallData, + Contract, + RPC, + TransactionReceipt, + Uint256, + hash, + num, + shortString, + uint256, +} from "starknet"; import { LegacyStarknetKeyPair, deployer, manager } from "."; import { GIFT_AMOUNT, GIFT_MAX_FEE } from "./deposit"; @@ -76,9 +87,9 @@ export async function claimExternal( factory: Contract, receiver: string, claim: Claim, - giftSigner: LegacyStarknetKeyPair, + giftPrivateKey: string, account = deployer, -): Promise { +): Promise { const constructorArgs: AccountConstructorArguments = { sender: deployer.address, amount: claim.amount, @@ -93,12 +104,12 @@ export async function claimExternal( factory.address, ); + const giftSigner = new LegacyStarknetKeyPair(giftPrivateKey); const claimExternalData = await getClaimExternalData({ receiver }); const signature = await giftSigner.signMessage(claimExternalData, claimAccountAddress); factory.connect(account); - const { transaction_hash } = await factory.claim_external(claim, receiver, signature); - return transaction_hash; + return await factory.claim_external(claim, receiver, signature); } export async function claimInternal( @@ -106,15 +117,11 @@ export async function claimInternal( tokenContract: Contract, claimSignerPrivateKey: string, claimSignerPublicKey: bigint, - receiverAddress = "0x42", + receiver = "0x42", useTxV3 = false, giftAmount = GIFT_AMOUNT, giftMaxFee = GIFT_MAX_FEE, -): Promise<{ - claimAccount: Account; - receiver: string; -}> { - const receiver = receiverAddress || `0x${encode.buf2hex(ec.starkCurve.utils.randomPrivateKey())}`; +): Promise { const claimAccountClassHash = await manager.declareLocalContract("ClaimAccount"); const claimAddress = await factory.get_claim_address( claimAccountClassHash, @@ -128,6 +135,5 @@ export async function claimInternal( const txVersion = useTxV3 ? RPC.ETransactionVersion.V3 : RPC.ETransactionVersion.V2; const claimAccount = new Account(manager, num.toHex(claimAddress), claimSignerPrivateKey, undefined, txVersion); factory.connect(claimAccount); - await factory.claim_internal(claim, receiver); - return { claimAccount, receiver }; + return await factory.claim_internal(claim, receiver); } diff --git a/lib/deposit.ts b/lib/deposit.ts index 9c8bf6b..2d5915b 100644 --- a/lib/deposit.ts +++ b/lib/deposit.ts @@ -1,5 +1,5 @@ -import { Account, CallData, Contract, RPC, ec, encode, hash, num, uint256 } from "starknet"; -import { AccountConstructorArguments, Claim, LegacyStarknetKeyPair, buildClaim, deployer, manager } from "./"; +import { CallData, Contract, hash, uint256 } from "starknet"; +import { AccountConstructorArguments, LegacyStarknetKeyPair, deployer, manager } from "./"; export const GIFT_AMOUNT = 1000000000000000n; export const GIFT_MAX_FEE = 50000000000000n; @@ -24,21 +24,18 @@ export async function deposit( export async function defaultDepositTestSetup( factory: Contract, useTxV3 = false, - forcedGiftPrivateKey = false, + giftPrivateKey?: string, giftAmount = GIFT_AMOUNT, giftMaxFee = GIFT_MAX_FEE, ): Promise<{ - claimAccount: Account; + claimAddress: string; tokenContract: Contract; claimSigner: LegacyStarknetKeyPair; - claim: Claim; - receiver: string; }> { const tokenContract = await manager.tokens.feeTokenContract(useTxV3); - // static signer / receiver for gas profiling - const receiver = forcedGiftPrivateKey ? "0x42" : `0x${encode.buf2hex(ec.starkCurve.utils.randomPrivateKey())}`; - const claimSigner = new LegacyStarknetKeyPair(forcedGiftPrivateKey ? "0x42" : undefined); + // static signer for gas profiling + const claimSigner = new LegacyStarknetKeyPair(giftPrivateKey || "0x42"); const claimPubKey = claimSigner.publicKey; await deposit(factory, tokenContract, claimPubKey); @@ -59,17 +56,5 @@ export async function defaultDepositTestSetup( factory.address, ); - const claim = buildClaim( - factory, - claimAccountClassHash, - giftAmount, - giftMaxFee, - tokenContract, - claimSigner.publicKey, - ); - - const txVersion = useTxV3 ? RPC.ETransactionVersion.V3 : RPC.ETransactionVersion.V2; - const claimAccount = new Account(manager, num.toHex(claimAddress), claimSigner, undefined, txVersion); - factory.connect(claimAccount); - return { claimAccount, tokenContract, claimSigner, claim, receiver }; + return { claimAddress, tokenContract, claimSigner }; } diff --git a/tests-integration/account.test.ts b/tests-integration/account.test.ts index a475edd..bab1e5a 100644 --- a/tests-integration/account.test.ts +++ b/tests-integration/account.test.ts @@ -3,6 +3,7 @@ import { num } from "starknet"; import { GIFT_AMOUNT, GIFT_MAX_FEE, + claimInternal, defaultDepositTestSetup, deployer, expectRevertWithErrorMessage, @@ -14,10 +15,11 @@ describe("Gifting", function () { for (const useTxV3 of [false, true]) { it(`Testing simple claim flow using txV3: ${useTxV3}`, async function () { const { factory } = await setupGiftProtocol(); - const { claim, receiver, claimAccount, tokenContract } = await defaultDepositTestSetup(factory); - await factory.claim_internal(claim, receiver); + const { tokenContract, claimSigner, claimAddress } = await defaultDepositTestSetup(factory); + const receiver = `0x2${Math.floor(Math.random() * 1000)}`; + await claimInternal(factory, tokenContract, claimSigner.privateKey, claimSigner.publicKey, receiver, useTxV3); - const finalBalance = await tokenContract.balance_of(claimAccount.address); + const finalBalance = await tokenContract.balance_of(claimAddress); expect(finalBalance < GIFT_MAX_FEE).to.be.true; await tokenContract.balance_of(receiver).should.eventually.equal(GIFT_AMOUNT); }); From 8653eb2562845ebb5df00929a853442c4ef6215b Mon Sep 17 00:00:00 2001 From: Leonard Paturel Date: Fri, 7 Jun 2024 14:02:16 +0100 Subject: [PATCH 22/29] major restructure --- lib/claim.ts | 118 ++++++---------- lib/deposit.ts | 43 ++++-- tests-integration/account.test.ts | 173 +++++++++++------------ tests-integration/claim_external.test.ts | 11 +- tests-integration/factory.test.ts | 123 ++++++---------- 5 files changed, 209 insertions(+), 259 deletions(-) diff --git a/lib/claim.ts b/lib/claim.ts index a5df4ba..7ffd236 100644 --- a/lib/claim.ts +++ b/lib/claim.ts @@ -1,17 +1,5 @@ -import { - Account, - CallData, - Contract, - RPC, - TransactionReceipt, - Uint256, - hash, - num, - shortString, - uint256, -} from "starknet"; -import { LegacyStarknetKeyPair, deployer, manager } from "."; -import { GIFT_AMOUNT, GIFT_MAX_FEE } from "./deposit"; +import { Account, RPC, TransactionReceipt, UniversalDetails, ec, encode, num, shortString, uint256 } from "starknet"; +import { LegacyStarknetKeyPair, calculateClaimAddress, deployer, ethAddress, manager, strkAddress } from "."; const typesRev1 = { StarknetDomain: [ @@ -50,7 +38,7 @@ export async function getClaimExternalData(claimExternal: ClaimExternal) { export interface AccountConstructorArguments { sender: string; - amount: Uint256; + amount: bigint; max_fee: bigint; token: string; claim_pubkey: bigint; @@ -61,79 +49,65 @@ export interface Claim extends AccountConstructorArguments { class_hash: string; } -export function buildClaim( - factory: Contract, - claimAccountClassHash: string, - giftAmount: bigint, - giftMaxFee: bigint, - tokenContract: Contract, - claimPubkey: bigint, -): Claim { - const constructorArgs: AccountConstructorArguments = { - sender: deployer.address, - amount: uint256.bnToUint256(giftAmount), - max_fee: giftMaxFee, - token: tokenContract.address, - claim_pubkey: claimPubkey, - }; +export function buildCallDataClaim(claim: Claim) { return { - factory: factory.address, - class_hash: claimAccountClassHash, - ...constructorArgs, + ...claim, + amount: uint256.bnToUint256(claim.amount), }; } export async function claimExternal( - factory: Contract, - receiver: string, claim: Claim, + receiver: string, giftPrivateKey: string, account = deployer, ): Promise { - const constructorArgs: AccountConstructorArguments = { - sender: deployer.address, - amount: claim.amount, - max_fee: claim.max_fee, - token: claim.token, - claim_pubkey: claim.claim_pubkey, - }; - const claimAccountAddress = hash.calculateContractAddressFromHash( - 0, - claim.class_hash, - CallData.compile({ constructorArgs }), - factory.address, - ); - + const claimAddress = calculateClaimAddress(claim); const giftSigner = new LegacyStarknetKeyPair(giftPrivateKey); const claimExternalData = await getClaimExternalData({ receiver }); - const signature = await giftSigner.signMessage(claimExternalData, claimAccountAddress); + const signature = await giftSigner.signMessage(claimExternalData, claimAddress); - factory.connect(account); - return await factory.claim_external(claim, receiver, signature); + return (await account.execute([ + { + contractAddress: claim.factory, + calldata: [buildCallDataClaim(claim), receiver, signature], + entrypoint: "claim_external", + }, + ])) as TransactionReceipt; } export async function claimInternal( - factory: Contract, - tokenContract: Contract, + claim: Claim, + receiver: string, claimSignerPrivateKey: string, - claimSignerPublicKey: bigint, - receiver = "0x42", - useTxV3 = false, - giftAmount = GIFT_AMOUNT, - giftMaxFee = GIFT_MAX_FEE, + details?: UniversalDetails, ): Promise { - const claimAccountClassHash = await manager.declareLocalContract("ClaimAccount"); - const claimAddress = await factory.get_claim_address( - claimAccountClassHash, - deployer.address, - giftAmount, - giftMaxFee, - tokenContract.address, - claimSignerPublicKey, - ); - const claim = buildClaim(factory, claimAccountClassHash, giftAmount, giftMaxFee, tokenContract, claimSignerPublicKey); - const txVersion = useTxV3 ? RPC.ETransactionVersion.V3 : RPC.ETransactionVersion.V2; + const claimAddress = calculateClaimAddress(claim); + + const txVersion = useTxv3(claim.token) ? RPC.ETransactionVersion.V3 : RPC.ETransactionVersion.V2; const claimAccount = new Account(manager, num.toHex(claimAddress), claimSignerPrivateKey, undefined, txVersion); - factory.connect(claimAccount); - return await factory.claim_internal(claim, receiver); + return (await claimAccount.execute( + [ + { + contractAddress: claim.factory, + calldata: [buildCallDataClaim(claim), receiver], + entrypoint: "claim_internal", + }, + ], + undefined, + { ...details }, + )) as TransactionReceipt; +} + +export const randomReceiver = (): string => { + return `0x${encode.buf2hex(ec.starkCurve.utils.randomPrivateKey())}`; +}; + +export function useTxv3(tokenAddress: string): boolean { + if (tokenAddress === ethAddress) { + return false; + } else if (tokenAddress === strkAddress) { + return true; + } + throw new Error(`Unsupported token`); } diff --git a/lib/deposit.ts b/lib/deposit.ts index 2d5915b..af8d2e5 100644 --- a/lib/deposit.ts +++ b/lib/deposit.ts @@ -1,5 +1,5 @@ -import { CallData, Contract, hash, uint256 } from "starknet"; -import { AccountConstructorArguments, LegacyStarknetKeyPair, deployer, manager } from "./"; +import { CallData, Contract, ec, encode, hash, uint256 } from "starknet"; +import { AccountConstructorArguments, Claim, LegacyStarknetKeyPair, deployer, manager } from "./"; export const GIFT_AMOUNT = 1000000000000000n; export const GIFT_MAX_FEE = 50000000000000n; @@ -28,33 +28,46 @@ export async function defaultDepositTestSetup( giftAmount = GIFT_AMOUNT, giftMaxFee = GIFT_MAX_FEE, ): Promise<{ - claimAddress: string; - tokenContract: Contract; - claimSigner: LegacyStarknetKeyPair; + claim: Claim; + claimPrivateKey: string; }> { const tokenContract = await manager.tokens.feeTokenContract(useTxV3); // static signer for gas profiling - const claimSigner = new LegacyStarknetKeyPair(giftPrivateKey || "0x42"); + const claimSigner = new LegacyStarknetKeyPair( + giftPrivateKey || `0x${encode.buf2hex(ec.starkCurve.utils.randomPrivateKey())}`, + ); const claimPubKey = claimSigner.publicKey; await deposit(factory, tokenContract, claimPubKey); - const claimAccountClassHash = await manager.declareLocalContract("ClaimAccount"); + const claimClassHash = await factory.get_latest_claim_class_hash(); - const constructorArgs: AccountConstructorArguments = { + const claim: Claim = { + factory: factory.address, + class_hash: claimClassHash, sender: deployer.address, - amount: uint256.bnToUint256(giftAmount), + amount: giftAmount, max_fee: giftMaxFee, token: tokenContract.address, - claim_pubkey: claimSigner.publicKey, + claim_pubkey: claimPubKey, + }; + return { claim, claimPrivateKey: claimSigner.privateKey }; +} + +export function calculateClaimAddress(claim: Claim): string { + const constructorArgs: AccountConstructorArguments = { + sender: claim.sender, + amount: claim.amount, + max_fee: claim.max_fee, + token: claim.token, + claim_pubkey: claim.claim_pubkey, }; const claimAddress = hash.calculateContractAddressFromHash( 0, - claimAccountClassHash, - CallData.compile({ constructorArgs }), - factory.address, + claim.class_hash, + CallData.compile({ ...constructorArgs, amount: uint256.bnToUint256(claim.amount) }), + claim.factory, ); - - return { claimAddress, tokenContract, claimSigner }; + return claimAddress; } diff --git a/tests-integration/account.test.ts b/tests-integration/account.test.ts index bab1e5a..8bde5aa 100644 --- a/tests-integration/account.test.ts +++ b/tests-integration/account.test.ts @@ -1,13 +1,13 @@ import { expect } from "chai"; import { num } from "starknet"; import { - GIFT_AMOUNT, GIFT_MAX_FEE, + calculateClaimAddress, claimInternal, defaultDepositTestSetup, - deployer, expectRevertWithErrorMessage, manager, + randomReceiver, setupGiftProtocol, } from "../lib"; @@ -15,123 +15,122 @@ describe("Gifting", function () { for (const useTxV3 of [false, true]) { it(`Testing simple claim flow using txV3: ${useTxV3}`, async function () { const { factory } = await setupGiftProtocol(); - const { tokenContract, claimSigner, claimAddress } = await defaultDepositTestSetup(factory); - const receiver = `0x2${Math.floor(Math.random() * 1000)}`; - await claimInternal(factory, tokenContract, claimSigner.privateKey, claimSigner.publicKey, receiver, useTxV3); + const { claim, claimPrivateKey } = await defaultDepositTestSetup(factory); + const receiver = randomReceiver(); + await claimInternal(claim, receiver, claimPrivateKey); - const finalBalance = await tokenContract.balance_of(claimAddress); - expect(finalBalance < GIFT_MAX_FEE).to.be.true; - await tokenContract.balance_of(receiver).should.eventually.equal(GIFT_AMOUNT); + const claimAddress = calculateClaimAddress(claim); + + const token = await manager.loadContract(claim.token); + const finalBalance = await token.balance_of(claimAddress); + expect(finalBalance < claim.max_fee).to.be.true; + await token.balance_of(receiver).should.eventually.equal(claim.amount); }); - it(`Test max fee too high`, async function () { + it(`Test max fee too high using txV3: ${useTxV3}`, async function () { const { factory } = await setupGiftProtocol(); - const { claim, receiver, claimAccount } = await defaultDepositTestSetup(factory, useTxV3); + const { claim, claimPrivateKey } = await defaultDepositTestSetup(factory); + const receiver = randomReceiver(); if (useTxV3) { - const estimate = await factory.estimateFee.claim_internal(claim, receiver); const newResourceBounds = { - ...estimate.resourceBounds, l2_gas: { - ...estimate.resourceBounds.l2_gas, - max_amount: GIFT_MAX_FEE + 1n, - max_price_per_unit: num.toHexString(4), + max_amount: num.toHexString(GIFT_MAX_FEE * 1000n), + max_price_per_unit: num.toHexString(10), + }, + l1_gas: { + max_amount: num.toHexString(GIFT_MAX_FEE * 1000n), + max_price_per_unit: num.toHexString(10), }, + tip: 1, }; await expectRevertWithErrorMessage("gift-acc/max-fee-too-high-v3", () => - claimAccount.execute( - [ - { - contractAddress: factory.address, - calldata: [claim, receiver], - entrypoint: "claim_internal", - }, - ], - undefined, - { resourceBounds: newResourceBounds, tip: 1 }, - ), + claimInternal(claim, receiver, claimPrivateKey, { resourceBounds: newResourceBounds }), ); } else { await expectRevertWithErrorMessage("gift-acc/max-fee-too-high-v1", () => - factory.claim_internal(claim, receiver, { maxFee: GIFT_MAX_FEE + 1n }), + claimInternal(claim, receiver, claimPrivateKey, { + maxFee: GIFT_MAX_FEE + 1n, + }), ); } }); } - it(`Test only protocol can call claim contract`, async function () { - const { factory } = await setupGiftProtocol(); - const { claimAccount } = await defaultDepositTestSetup(factory); - const claimContract = await manager.loadContract(num.toHex(claimAccount.address)); + // it(`Test only protocol can call claim contract`, async function () { + // const { factory } = await setupGiftProtocol(); + // const { claim, claimPrivateKey } = await defaultDepositTestSetup(factory); + // const receiver = randomReceiver(); - claimContract.connect(claimAccount); - await expectRevertWithErrorMessage("gift-acc/only-protocol", () => claimContract.__validate__([])); - }); + // await expectRevertWithErrorMessage("gift-acc/only-protocol", () => + // claimInternal(factory, tokenContract, claimClassHash, claimPrivateKey, receiver), + // ); + // }); - it(`Test claim contract cant call another contract`, async function () { - const { factory, claimAccountClassHash } = await setupGiftProtocol(); - const { claim, receiver, claimAccount } = await defaultDepositTestSetup(factory); + // it(`Test claim contract cant call another contract`, async function () { + // const { factory } = await setupGiftProtocol(); + // const { claim, claimPrivateKey } = await defaultDepositTestSetup(factory); + // const receiver = randomReceiver(); - const fakeFactory = await manager.deployContract("GiftFactory", { - unique: true, - constructorCalldata: [claimAccountClassHash, deployer.address], - }); - fakeFactory.connect(claimAccount); - await expectRevertWithErrorMessage("gift-acc/invalid-call-to", () => - fakeFactory.claim_internal(claim, receiver, { maxFee: 400000000000000n }), - ); - }); + // const claimFakeFactory = { ...claim, factory: "0x123" }; + // await expectRevertWithErrorMessage("gift-acc/invalid-call-to", () => + // claimInternal(claimFakeFactory, claimPrivateKey, receiver), + // ); + // }); - it(`Test claim contract can only call 'claim_internal'`, async function () { - const { factory } = await setupGiftProtocol(); - const { claim, receiver, claimAccount } = await defaultDepositTestSetup(factory); + // it(`Test claim contract can only call 'claim_internal'`, async function () { + // const { factory } = await setupGiftProtocol(); + // const { tokenContract, claimAddress, claimClassHash, claimPrivateKey } = await defaultDepositTestSetup(factory); + // const receiver = randomReceiver(); - factory.connect(claimAccount); - await expectRevertWithErrorMessage("gift-acc/invalid-call-selector", () => - factory.get_dust(claim, receiver, { maxFee: 400000000000000n }), - ); - }); + // const claimAccount = new Account(manager, num.toHex(claimAddress), claimPrivateKey, undefined); - it(`Test claim contract cant preform a multicall`, async function () { - const { factory } = await setupGiftProtocol(); - const { claim, receiver, claimAccount } = await defaultDepositTestSetup(factory); - - await expectRevertWithErrorMessage("gift-acc/invalid-call-len", () => - claimAccount.execute([ - { - contractAddress: factory.address, - calldata: [claim, receiver], - entrypoint: "claim_internal", - }, - { - contractAddress: factory.address, - calldata: [claim, receiver], - entrypoint: "claim_internal", - }, - ]), - ); - }); + // let claim = buildClaim( + // factory, + // claimAccountClassHash, + // GIFT_AMOUNT, + // GIFT_MAX_FEE, + // tokenContract, + // claimSigner.publicKey, + // ); + + // factory.connect(claimAccount); + // await expectRevertWithErrorMessage("gift-acc/invalid-call-selector", () => + // factory.get_dust(claim, receiver, { maxFee: 400000000000000n }), + // ); + // }); + + // it(`Test claim contract cant preform a multicall`, async function () { + // const { factory } = await setupGiftProtocol(); + // const { tokenContract, claimSigner } = await defaultDepositTestSetup(factory); + // const receiver = randomReceiver(); + + // await expectRevertWithErrorMessage("gift-acc/invalid-call-len", () => + // claimAccount.execute([ + // { + // contractAddress: factory.address, + // calldata: [claim, receiver], + // entrypoint: "claim_internal", + // }, + // { + // contractAddress: factory.address, + // calldata: [claim, receiver], + // entrypoint: "claim_internal", + // }, + // ]), + // ); + // }); it(`Test cannot call 'claim_internal' twice`, async function () { const { factory } = await setupGiftProtocol(); - const { claim, receiver, claimAccount } = await defaultDepositTestSetup(factory); + const { claim, claimPrivateKey } = await defaultDepositTestSetup(factory); + const receiver = randomReceiver(); // double claim - await factory.claim_internal(claim, receiver); + await claimInternal(claim, receiver, claimPrivateKey); await expectRevertWithErrorMessage("gift-acc/invalid-claim-nonce", () => - claimAccount.execute( - [ - { - contractAddress: factory.address, - calldata: [claim, receiver], - entrypoint: "claim_internal", - }, - ], - undefined, - { skipValidate: false }, - ), + claimInternal(claim, receiver, claimPrivateKey, { skipValidate: false }), ); }); - // TODO Tests: // - claim_external // - check with wrong claim data diff --git a/tests-integration/claim_external.test.ts b/tests-integration/claim_external.test.ts index 74502b0..200b235 100644 --- a/tests-integration/claim_external.test.ts +++ b/tests-integration/claim_external.test.ts @@ -1,16 +1,13 @@ -import { defaultDepositTestSetup, deployer, getClaimExternalData, setupGiftProtocol } from "../lib"; +import { claimExternal, defaultDepositTestSetup, randomReceiver, setupGiftProtocol } from "../lib"; describe("claim_external", function () { for (const useTxV3 of [false, true]) { it(`Testing claim_external flow using txV3: ${useTxV3}`, async function () { const { factory } = await setupGiftProtocol(); - const { claim, receiver, claimAccount, claimSigner } = await defaultDepositTestSetup(factory); + const { claim, claimPrivateKey } = await defaultDepositTestSetup(factory); + const receiver = randomReceiver(); - const claimExternalData = await getClaimExternalData({ receiver }); - const signature = await claimSigner.signMessage(claimExternalData, claimAccount.address); - - factory.connect(deployer); - await factory.claim_external(claim, receiver, signature); + await claimExternal(claim, receiver, claimPrivateKey); }); } }); diff --git a/tests-integration/factory.test.ts b/tests-integration/factory.test.ts index 7813926..ae3f9b4 100644 --- a/tests-integration/factory.test.ts +++ b/tests-integration/factory.test.ts @@ -1,147 +1,114 @@ import { expect } from "chai"; -import { Account, CallData, RPC, hash, num, uint256 } from "starknet"; +import { ec, encode, num } from "starknet"; import { - AccountConstructorArguments, GIFT_AMOUNT, GIFT_MAX_FEE, LegacyStarknetKeyPair, + calculateClaimAddress, + claimInternal, defaultDepositTestSetup, deployer, expectRevertWithErrorMessage, genericAccount, manager, + randomReceiver, setupGiftProtocol, } from "../lib"; describe("Factory", function () { it(`Test calculate claim address`, async function () { - const { factory, claimAccountClassHash } = await setupGiftProtocol(); - const { tokenContract, claimSigner } = await defaultDepositTestSetup(factory); + const { factory } = await setupGiftProtocol(); + const { claim } = await defaultDepositTestSetup(factory); + const claimAddress = await factory.get_claim_address( - claimAccountClassHash, + claim.class_hash, deployer.address, GIFT_AMOUNT, GIFT_MAX_FEE, - tokenContract.address, - claimSigner.publicKey, + claim.token, + claim.claim_pubkey, ); - const constructorArgs: AccountConstructorArguments = { - sender: deployer.address, - amount: uint256.bnToUint256(GIFT_AMOUNT), - max_fee: GIFT_MAX_FEE, - token: tokenContract.address, - claim_pubkey: claimSigner.publicKey, - }; - - const correctAddress = hash.calculateContractAddressFromHash( - 0, - claimAccountClassHash, - CallData.compile({ constructorArgs }), - factory.address, - ); + const correctAddress = calculateClaimAddress(claim); expect(claimAddress).to.be.equal(num.toBigInt(correctAddress)); }); for (const useTxV3 of [false, true]) { it(`get_dust: ${useTxV3}`, async function () { const { factory } = await setupGiftProtocol(); - const { tokenContract, claim, receiver, claimAccount } = await defaultDepositTestSetup(factory); - const receiverDust = `0x2${Math.floor(Math.random() * 1000)}`; + const { claim, claimPrivateKey } = await defaultDepositTestSetup(factory); + const receiver = randomReceiver(); + const receiverDust = randomReceiver(); - await factory.claim_internal(claim, receiver); + await claimInternal(claim, receiver, claimPrivateKey); + const claimAddress = calculateClaimAddress(claim); + const token = await manager.loadContract(claim.token); // Final check - const dustBalance = await tokenContract.balance_of(claimAccount.address); + + const dustBalance = await token.balance_of(claimAddress); expect(dustBalance < GIFT_MAX_FEE).to.be.true; - await tokenContract.balance_of(receiver).should.eventually.equal(GIFT_AMOUNT); + await token.balance_of(receiver).should.eventually.equal(GIFT_AMOUNT); // Test dust - await tokenContract.balance_of(receiverDust).should.eventually.equal(0n); + await token.balance_of(receiverDust).should.eventually.equal(0n); factory.connect(deployer); await factory.get_dust(claim, receiverDust); - await tokenContract.balance_of(claimAccount.address).should.eventually.equal(0n); - await tokenContract.balance_of(receiverDust).should.eventually.equal(dustBalance); + await token.balance_of(claimAddress).should.eventually.equal(0n); + await token.balance_of(receiverDust).should.eventually.equal(dustBalance); }); } it(`Test Cancel Claim`, async function () { const { factory } = await setupGiftProtocol(); - const { tokenContract, claim, receiver, claimAccount } = await defaultDepositTestSetup(factory); + const { claim, claimPrivateKey } = await defaultDepositTestSetup(factory); + const receiver = randomReceiver(); + const token = await manager.loadContract(claim.token); + const claimAddress = calculateClaimAddress(claim); - const balanceSenderBefore = await tokenContract.balance_of(deployer.address); + const balanceSenderBefore = await token.balance_of(deployer.address); factory.connect(deployer); const { transaction_hash } = await factory.cancel(claim); const txFee = BigInt((await manager.getTransactionReceipt(transaction_hash)).actual_fee.amount); // Check balance of the sender is correct - await tokenContract + await token .balance_of(deployer.address) - .should.eventually.equal(balanceSenderBefore + GIFT_AMOUNT + GIFT_MAX_FEE - txFee); + .should.eventually.equal(balanceSenderBefore + claim.amount + claim.max_fee - txFee); // Check balance claim address address == 0 - await tokenContract.balance_of(claimAccount.address).should.eventually.equal(0n); + await token.balance_of(claimAddress).should.eventually.equal(0n); - factory.connect(claimAccount); - await expectRevertWithErrorMessage("gift/already-claimed-or-cancel", () => factory.claim_internal(claim, receiver)); + await expectRevertWithErrorMessage("gift/already-claimed-or-cancel", () => + claimInternal(claim, receiver, claimPrivateKey), + ); }); - it(`Test pausable`, async function () { + it.only(`Test pausable`, async function () { // Deploy factory - const { factory, claimAccountClassHash } = await setupGiftProtocol(); - const signer = new LegacyStarknetKeyPair(); - const claimPubkey = signer.publicKey; - const GIFT_AMOUNT = 1000000000000000n; - const GIFT_MAX_FEE = 50000000000000n; - const receiver = `0x5${Math.floor(Math.random() * 1000)}`; - - // Make a gift + const { factory } = await setupGiftProtocol(); + const receiver = randomReceiver(); + const claimSigner = new LegacyStarknetKeyPair(`0x${encode.buf2hex(ec.starkCurve.utils.randomPrivateKey())}`); + + // approvals const tokenContract = await manager.tokens.feeTokenContract(false); tokenContract.connect(deployer); factory.connect(deployer); await tokenContract.approve(factory.address, GIFT_AMOUNT + GIFT_MAX_FEE); + // pause / unpause factory.connect(genericAccount); await expectRevertWithErrorMessage("Caller is not the owner", () => factory.pause()); factory.connect(deployer); await factory.pause(); await expectRevertWithErrorMessage("Pausable: paused", () => - factory.deposit(GIFT_AMOUNT, GIFT_MAX_FEE, tokenContract.address, claimPubkey), + factory.deposit(GIFT_AMOUNT, GIFT_MAX_FEE, tokenContract.address, claimSigner.publicKey), ); await factory.unpause(); - await factory.deposit(GIFT_AMOUNT, GIFT_MAX_FEE, tokenContract.address, claimPubkey); - - // Ensure there is a contract for the claim - const claimAddress = await factory.get_claim_address( - claimAccountClassHash, - deployer.address, - GIFT_AMOUNT, - GIFT_MAX_FEE, - tokenContract.address, - claimPubkey, - ); - - const claim = { - factory: factory.address, - class_hash: claimAccountClassHash, - sender: deployer.address, - amount: uint256.bnToUint256(GIFT_AMOUNT), - max_fee: GIFT_MAX_FEE, - token: tokenContract.address, - claim_pubkey: claimPubkey, - }; - - const claimContract = await manager.loadContract(num.toHex(claimAddress)); - const claimAccount = new Account(manager, claimContract.address, signer, undefined, RPC.ETransactionVersion.V2); - - // Check balance of the claim contract is correct - await tokenContract.balance_of(claimAddress).should.eventually.equal(GIFT_AMOUNT + GIFT_MAX_FEE); - // Check balance receiver address == 0 - await tokenContract.balance_of(receiver).should.eventually.equal(0n); - - factory.connect(claimAccount); - await factory.claim_internal(claim, receiver); + const { claim } = await defaultDepositTestSetup(factory, false, claimSigner.privateKey); + await claimInternal(claim, receiver, claimSigner.privateKey); // Final check + const claimAddress = calculateClaimAddress(claim); const dustBalance = await tokenContract.balance_of(claimAddress); expect(dustBalance < GIFT_MAX_FEE).to.be.true; await tokenContract.balance_of(receiver).should.eventually.equal(GIFT_AMOUNT); From c5bda7e7dcbc66f0731267354dbefdb91164e5b8 Mon Sep 17 00:00:00 2001 From: Leonard Paturel Date: Fri, 7 Jun 2024 14:03:26 +0100 Subject: [PATCH 23/29] pausable --- tests-integration/factory.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests-integration/factory.test.ts b/tests-integration/factory.test.ts index ae3f9b4..ba2506a 100644 --- a/tests-integration/factory.test.ts +++ b/tests-integration/factory.test.ts @@ -82,7 +82,7 @@ describe("Factory", function () { ); }); - it.only(`Test pausable`, async function () { + it(`Test pausable`, async function () { // Deploy factory const { factory } = await setupGiftProtocol(); const receiver = randomReceiver(); From 4fc7d611f9cef0b855bcf56b12e1d6e3b8608eed Mon Sep 17 00:00:00 2001 From: Leonard Paturel Date: Fri, 7 Jun 2024 14:23:40 +0100 Subject: [PATCH 24/29] fix (nearly) all broken tests --- tests-integration/account.test.ts | 137 +++++++++++++++++++----------- 1 file changed, 86 insertions(+), 51 deletions(-) diff --git a/tests-integration/account.test.ts b/tests-integration/account.test.ts index 8bde5aa..58af4d2 100644 --- a/tests-integration/account.test.ts +++ b/tests-integration/account.test.ts @@ -1,7 +1,8 @@ import { expect } from "chai"; -import { num } from "starknet"; +import { Account, RPC, num } from "starknet"; import { GIFT_MAX_FEE, + buildCallDataClaim, calculateClaimAddress, claimInternal, defaultDepositTestSetup, @@ -56,69 +57,103 @@ describe("Gifting", function () { }); } - // it(`Test only protocol can call claim contract`, async function () { - // const { factory } = await setupGiftProtocol(); - // const { claim, claimPrivateKey } = await defaultDepositTestSetup(factory); - // const receiver = randomReceiver(); + it(`Test only protocol can call claim contract`, async function () { + const { factory } = await setupGiftProtocol(); + const { claim, claimPrivateKey } = await defaultDepositTestSetup(factory); - // await expectRevertWithErrorMessage("gift-acc/only-protocol", () => - // claimInternal(factory, tokenContract, claimClassHash, claimPrivateKey, receiver), - // ); - // }); + const claimAddress = calculateClaimAddress(claim); + + const claimAccount = new Account( + manager, + num.toHex(claimAddress), + claimPrivateKey, + undefined, + RPC.ETransactionVersion.V2, + ); + const claimContract = await manager.loadContract(claimAddress); + claimContract.connect(claimAccount); + await expectRevertWithErrorMessage("gift-acc/only-protocol", () => claimContract.__validate__([])); + }); - // it(`Test claim contract cant call another contract`, async function () { - // const { factory } = await setupGiftProtocol(); + // it.only(`Test claim contract cant call another contract`, async function () { + // const { factory, claimAccountClassHash } = await setupGiftProtocol(); // const { claim, claimPrivateKey } = await defaultDepositTestSetup(factory); // const receiver = randomReceiver(); - // const claimFakeFactory = { ...claim, factory: "0x123" }; - // await expectRevertWithErrorMessage("gift-acc/invalid-call-to", () => - // claimInternal(claimFakeFactory, claimPrivateKey, receiver), + // const fakeFactory = await manager.deployContract("GiftFactory", { + // unique: true, + // constructorCalldata: [claimAccountClassHash, deployer.address], + // }); + + // const fakeClaim = { ...claim, factory: fakeFactory.address }; + // const claimAddress = calculateClaimAddress(claim); + + // const claimAccount = new Account( + // manager, + // num.toHex(claimAddress), + // claimPrivateKey, + // undefined, + // RPC.ETransactionVersion.V2, // ); + // fakeFactory.connect(claimAccount); + // await fakeFactory.claim_internal(buildCallDataClaim(fakeClaim), receiver, { maxFee: 400000000000000n }); + + // // await expectRevertWithErrorMessage("gift-acc/invalid-call-to", () => + // // claimInternal(fakeClaim, receiver, claimPrivateKey), + // // ); // }); - // it(`Test claim contract can only call 'claim_internal'`, async function () { - // const { factory } = await setupGiftProtocol(); - // const { tokenContract, claimAddress, claimClassHash, claimPrivateKey } = await defaultDepositTestSetup(factory); - // const receiver = randomReceiver(); + it(`Test claim contract can only call 'claim_internal'`, async function () { + const { factory } = await setupGiftProtocol(); + const { claim, claimPrivateKey } = await defaultDepositTestSetup(factory); + const receiver = randomReceiver(); - // const claimAccount = new Account(manager, num.toHex(claimAddress), claimPrivateKey, undefined); + const claimAddress = calculateClaimAddress(claim); - // let claim = buildClaim( - // factory, - // claimAccountClassHash, - // GIFT_AMOUNT, - // GIFT_MAX_FEE, - // tokenContract, - // claimSigner.publicKey, - // ); + const claimAccount = new Account( + manager, + num.toHex(claimAddress), + claimPrivateKey, + undefined, + RPC.ETransactionVersion.V2, + ); - // factory.connect(claimAccount); - // await expectRevertWithErrorMessage("gift-acc/invalid-call-selector", () => - // factory.get_dust(claim, receiver, { maxFee: 400000000000000n }), - // ); - // }); + factory.connect(claimAccount); + await expectRevertWithErrorMessage("gift-acc/invalid-call-selector", () => + factory.get_dust(claim, receiver, { maxFee: 400000000000000n }), + ); + }); - // it(`Test claim contract cant preform a multicall`, async function () { - // const { factory } = await setupGiftProtocol(); - // const { tokenContract, claimSigner } = await defaultDepositTestSetup(factory); - // const receiver = randomReceiver(); + it(`Test claim contract cant preform a multicall`, async function () { + const { factory } = await setupGiftProtocol(); + const { claim, claimPrivateKey } = await defaultDepositTestSetup(factory); + const receiver = randomReceiver(); - // await expectRevertWithErrorMessage("gift-acc/invalid-call-len", () => - // claimAccount.execute([ - // { - // contractAddress: factory.address, - // calldata: [claim, receiver], - // entrypoint: "claim_internal", - // }, - // { - // contractAddress: factory.address, - // calldata: [claim, receiver], - // entrypoint: "claim_internal", - // }, - // ]), - // ); - // }); + const claimAddress = calculateClaimAddress(claim); + + const claimAccount = new Account( + manager, + num.toHex(claimAddress), + claimPrivateKey, + undefined, + RPC.ETransactionVersion.V2, + ); + + await expectRevertWithErrorMessage("gift-acc/invalid-call-len", () => + claimAccount.execute([ + { + contractAddress: factory.address, + calldata: [buildCallDataClaim(claim), receiver], + entrypoint: "claim_internal", + }, + { + contractAddress: factory.address, + calldata: [buildCallDataClaim(claim), receiver], + entrypoint: "claim_internal", + }, + ]), + ); + }); it(`Test cannot call 'claim_internal' twice`, async function () { const { factory } = await setupGiftProtocol(); From 4a8542e9c0e0f212664602366e8858c0aa3ef497 Mon Sep 17 00:00:00 2001 From: gaetbout Date: Fri, 7 Jun 2024 16:20:06 +0200 Subject: [PATCH 25/29] fix gas l2 using v3 --- tests-integration/account.test.ts | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/tests-integration/account.test.ts b/tests-integration/account.test.ts index 58af4d2..12dab9c 100644 --- a/tests-integration/account.test.ts +++ b/tests-integration/account.test.ts @@ -16,7 +16,7 @@ describe("Gifting", function () { for (const useTxV3 of [false, true]) { it(`Testing simple claim flow using txV3: ${useTxV3}`, async function () { const { factory } = await setupGiftProtocol(); - const { claim, claimPrivateKey } = await defaultDepositTestSetup(factory); + const { claim, claimPrivateKey } = await defaultDepositTestSetup(factory, useTxV3); const receiver = randomReceiver(); await claimInternal(claim, receiver, claimPrivateKey); @@ -30,22 +30,21 @@ describe("Gifting", function () { it(`Test max fee too high using txV3: ${useTxV3}`, async function () { const { factory } = await setupGiftProtocol(); - const { claim, claimPrivateKey } = await defaultDepositTestSetup(factory); + const { claim, claimPrivateKey } = await defaultDepositTestSetup(factory, useTxV3); const receiver = randomReceiver(); if (useTxV3) { const newResourceBounds = { l2_gas: { - max_amount: num.toHexString(GIFT_MAX_FEE * 1000n), - max_price_per_unit: num.toHexString(10), + max_amount: num.toHexString(GIFT_MAX_FEE), + max_price_per_unit: num.toHexString(1), }, l1_gas: { - max_amount: num.toHexString(GIFT_MAX_FEE * 1000n), - max_price_per_unit: num.toHexString(10), + max_amount: num.toHexString(10), + max_price_per_unit: num.toHexString(36000000000n), // Current devnet gas price }, - tip: 1, }; await expectRevertWithErrorMessage("gift-acc/max-fee-too-high-v3", () => - claimInternal(claim, receiver, claimPrivateKey, { resourceBounds: newResourceBounds }), + claimInternal(claim, receiver, claimPrivateKey, { resourceBounds: newResourceBounds, tip: 1 }), ); } else { await expectRevertWithErrorMessage("gift-acc/max-fee-too-high-v1", () => From 846913e808210770d90b422c6cd13972267d7e50 Mon Sep 17 00:00:00 2001 From: gaetbout Date: Fri, 7 Jun 2024 16:52:01 +0200 Subject: [PATCH 26/29] update to latest devnet --- Dockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 3487c75..d9a9554 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,8 +1,8 @@ # Use the base image -FROM shardlabs/starknet-devnet-rs:c4185522228f61ba04619151eb5706d4610fb00f +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"] \ No newline at end of file +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 From 30b1781bda4cf2a8bac7651cfe19bad8f8c4864c Mon Sep 17 00:00:00 2001 From: Leonard Paturel Date: Fri, 7 Jun 2024 16:46:21 +0100 Subject: [PATCH 27/29] fixed tests --- lib/claim.ts | 11 +++--- lib/deposit.ts | 66 +++++++++++++++++++++---------- tests-integration/account.test.ts | 63 +++++++++++++++-------------- tests-integration/factory.test.ts | 15 +++---- 4 files changed, 90 insertions(+), 65 deletions(-) diff --git a/lib/claim.ts b/lib/claim.ts index 7ffd236..625adc1 100644 --- a/lib/claim.ts +++ b/lib/claim.ts @@ -38,9 +38,10 @@ export async function getClaimExternalData(claimExternal: ClaimExternal) { export interface AccountConstructorArguments { sender: string; - amount: bigint; - max_fee: bigint; - token: string; + gift_token: string; + gift_amount: bigint; + fee_token: string; + fee_amount: bigint; claim_pubkey: bigint; } @@ -52,7 +53,7 @@ export interface Claim extends AccountConstructorArguments { export function buildCallDataClaim(claim: Claim) { return { ...claim, - amount: uint256.bnToUint256(claim.amount), + gift_amount: uint256.bnToUint256(claim.gift_amount), }; } @@ -84,7 +85,7 @@ export async function claimInternal( ): Promise { const claimAddress = calculateClaimAddress(claim); - const txVersion = useTxv3(claim.token) ? RPC.ETransactionVersion.V3 : RPC.ETransactionVersion.V2; + const txVersion = useTxv3(claim.fee_token) ? RPC.ETransactionVersion.V3 : RPC.ETransactionVersion.V2; const claimAccount = new Account(manager, num.toHex(claimAddress), claimSignerPrivateKey, undefined, txVersion); return (await claimAccount.execute( [ diff --git a/lib/deposit.ts b/lib/deposit.ts index af8d2e5..c2b66f9 100644 --- a/lib/deposit.ts +++ b/lib/deposit.ts @@ -1,30 +1,40 @@ -import { CallData, Contract, ec, encode, hash, uint256 } from "starknet"; +import { Account, CallData, Contract, ec, encode, hash, uint256 } from "starknet"; import { AccountConstructorArguments, Claim, LegacyStarknetKeyPair, deployer, manager } from "./"; export const GIFT_AMOUNT = 1000000000000000n; export const GIFT_MAX_FEE = 50000000000000n; export async function deposit( - factory: Contract, - tokenContract: Contract, + sender: Account, + giftAmount: bigint, + feeAmount: bigint, + factoryAddress: string, + feeTokenAddress: string, + giftTokenAddress: string, claimSignerPubKey: bigint, - sender = deployer, - amount = GIFT_AMOUNT, - feeAmount = GIFT_MAX_FEE, ) { - // Make a gift - tokenContract.connect(sender); - factory.connect(sender); - await sender.execute([ - tokenContract.populateTransaction.approve(factory.address, amount + feeAmount), - factory.populateTransaction.deposit(amount, feeAmount, tokenContract.address, claimSignerPubKey), - ]); + const factory = await manager.loadContract(factoryAddress); + const feeToken = await manager.loadContract(feeTokenAddress); + const giftToken = await manager.loadContract(giftTokenAddress); + if (feeTokenAddress === giftTokenAddress) { + await sender.execute([ + feeToken.populateTransaction.approve(factory.address, giftAmount + feeAmount), + factory.populateTransaction.deposit(giftTokenAddress, giftAmount, feeTokenAddress, feeAmount, claimSignerPubKey), + ]); + } else { + await sender.execute([ + feeToken.populateTransaction.approve(factory.address, feeAmount), + giftToken.populateTransaction.approve(factory.address, giftAmount), + factory.populateTransaction.deposit(giftTokenAddress, giftAmount, feeTokenAddress, feeAmount, claimSignerPubKey), + ]); + } } export async function defaultDepositTestSetup( factory: Contract, useTxV3 = false, giftPrivateKey?: string, + giftTokenAddress?: string, giftAmount = GIFT_AMOUNT, giftMaxFee = GIFT_MAX_FEE, ): Promise<{ @@ -38,7 +48,15 @@ export async function defaultDepositTestSetup( giftPrivateKey || `0x${encode.buf2hex(ec.starkCurve.utils.randomPrivateKey())}`, ); const claimPubKey = claimSigner.publicKey; - await deposit(factory, tokenContract, claimPubKey); + await deposit( + deployer, + giftAmount, + giftMaxFee, + factory.address, + tokenContract.address, + giftTokenAddress || tokenContract.address, + claimPubKey, + ); const claimClassHash = await factory.get_latest_claim_class_hash(); @@ -46,27 +64,33 @@ export async function defaultDepositTestSetup( factory: factory.address, class_hash: claimClassHash, sender: deployer.address, - amount: giftAmount, - max_fee: giftMaxFee, - token: tokenContract.address, + gift_token: giftTokenAddress || tokenContract.address, + gift_amount: giftAmount, + fee_token: tokenContract.address, + fee_amount: giftMaxFee, claim_pubkey: claimPubKey, }; + return { claim, claimPrivateKey: claimSigner.privateKey }; } export function calculateClaimAddress(claim: Claim): string { const constructorArgs: AccountConstructorArguments = { sender: claim.sender, - amount: claim.amount, - max_fee: claim.max_fee, - token: claim.token, + gift_token: claim.gift_token, + gift_amount: claim.gift_amount, + fee_token: claim.fee_token, + fee_amount: claim.fee_amount, claim_pubkey: claim.claim_pubkey, }; const claimAddress = hash.calculateContractAddressFromHash( 0, claim.class_hash, - CallData.compile({ ...constructorArgs, amount: uint256.bnToUint256(claim.amount) }), + CallData.compile({ + ...constructorArgs, + gift_amount: uint256.bnToUint256(claim.gift_amount), + }), claim.factory, ); return claimAddress; diff --git a/tests-integration/account.test.ts b/tests-integration/account.test.ts index 12dab9c..a98c1c3 100644 --- a/tests-integration/account.test.ts +++ b/tests-integration/account.test.ts @@ -6,6 +6,7 @@ import { calculateClaimAddress, claimInternal, defaultDepositTestSetup, + deployer, expectRevertWithErrorMessage, manager, randomReceiver, @@ -18,14 +19,14 @@ describe("Gifting", function () { const { factory } = await setupGiftProtocol(); const { claim, claimPrivateKey } = await defaultDepositTestSetup(factory, useTxV3); const receiver = randomReceiver(); - await claimInternal(claim, receiver, claimPrivateKey); - const claimAddress = calculateClaimAddress(claim); - const token = await manager.loadContract(claim.token); + await claimInternal(claim, receiver, claimPrivateKey); + + const token = await manager.loadContract(claim.gift_token); const finalBalance = await token.balance_of(claimAddress); - expect(finalBalance < claim.max_fee).to.be.true; - await token.balance_of(receiver).should.eventually.equal(claim.amount); + expect(finalBalance < claim.fee_amount).to.be.true; + await token.balance_of(receiver).should.eventually.equal(claim.gift_amount); }); it(`Test max fee too high using txV3: ${useTxV3}`, async function () { @@ -74,33 +75,31 @@ describe("Gifting", function () { await expectRevertWithErrorMessage("gift-acc/only-protocol", () => claimContract.__validate__([])); }); - // it.only(`Test claim contract cant call another contract`, async function () { - // const { factory, claimAccountClassHash } = await setupGiftProtocol(); - // const { claim, claimPrivateKey } = await defaultDepositTestSetup(factory); - // const receiver = randomReceiver(); - - // const fakeFactory = await manager.deployContract("GiftFactory", { - // unique: true, - // constructorCalldata: [claimAccountClassHash, deployer.address], - // }); - - // const fakeClaim = { ...claim, factory: fakeFactory.address }; - // const claimAddress = calculateClaimAddress(claim); - - // const claimAccount = new Account( - // manager, - // num.toHex(claimAddress), - // claimPrivateKey, - // undefined, - // RPC.ETransactionVersion.V2, - // ); - // fakeFactory.connect(claimAccount); - // await fakeFactory.claim_internal(buildCallDataClaim(fakeClaim), receiver, { maxFee: 400000000000000n }); - - // // await expectRevertWithErrorMessage("gift-acc/invalid-call-to", () => - // // claimInternal(fakeClaim, receiver, claimPrivateKey), - // // ); - // }); + it.only(`Test claim contract cant call another contract`, async function () { + const { factory, claimAccountClassHash } = await setupGiftProtocol(); + const { claim, claimPrivateKey } = await defaultDepositTestSetup(factory); + const receiver = randomReceiver(); + + const fakeFactory = await manager.deployContract("GiftFactory", { + unique: true, + constructorCalldata: [claimAccountClassHash, deployer.address], + }); + + const claimAddress = calculateClaimAddress(claim); + + const claimAccount = new Account( + manager, + num.toHex(claimAddress), + claimPrivateKey, + undefined, + RPC.ETransactionVersion.V2, + ); + fakeFactory.connect(claimAccount); + + await expectRevertWithErrorMessage("gift-acc/invalid-call-to", () => + fakeFactory.claim_internal(buildCallDataClaim(claim), receiver, { maxFee: 400000000000000n }), + ); + }); it(`Test claim contract can only call 'claim_internal'`, async function () { const { factory } = await setupGiftProtocol(); diff --git a/tests-integration/factory.test.ts b/tests-integration/factory.test.ts index ba2506a..3a17e8d 100644 --- a/tests-integration/factory.test.ts +++ b/tests-integration/factory.test.ts @@ -23,9 +23,10 @@ describe("Factory", function () { const claimAddress = await factory.get_claim_address( claim.class_hash, deployer.address, - GIFT_AMOUNT, - GIFT_MAX_FEE, - claim.token, + claim.gift_token, + claim.gift_amount, + claim.fee_token, + claim.fee_amount, claim.claim_pubkey, ); @@ -41,7 +42,7 @@ describe("Factory", function () { await claimInternal(claim, receiver, claimPrivateKey); const claimAddress = calculateClaimAddress(claim); - const token = await manager.loadContract(claim.token); + const token = await manager.loadContract(claim.gift_token); // Final check @@ -63,7 +64,7 @@ describe("Factory", function () { const { factory } = await setupGiftProtocol(); const { claim, claimPrivateKey } = await defaultDepositTestSetup(factory); const receiver = randomReceiver(); - const token = await manager.loadContract(claim.token); + const token = await manager.loadContract(claim.gift_token); const claimAddress = calculateClaimAddress(claim); const balanceSenderBefore = await token.balance_of(deployer.address); @@ -73,7 +74,7 @@ describe("Factory", function () { // Check balance of the sender is correct await token .balance_of(deployer.address) - .should.eventually.equal(balanceSenderBefore + claim.amount + claim.max_fee - txFee); + .should.eventually.equal(balanceSenderBefore + claim.gift_amount + claim.fee_amount - txFee); // Check balance claim address address == 0 await token.balance_of(claimAddress).should.eventually.equal(0n); @@ -100,7 +101,7 @@ describe("Factory", function () { factory.connect(deployer); await factory.pause(); await expectRevertWithErrorMessage("Pausable: paused", () => - factory.deposit(GIFT_AMOUNT, GIFT_MAX_FEE, tokenContract.address, claimSigner.publicKey), + factory.deposit(tokenContract.address, GIFT_AMOUNT, tokenContract.address, GIFT_MAX_FEE, claimSigner.publicKey), ); await factory.unpause(); From 906ce54b7baeea87457ca883d5477d1e92e9f045 Mon Sep 17 00:00:00 2001 From: Leonard Paturel Date: Fri, 7 Jun 2024 16:47:55 +0100 Subject: [PATCH 28/29] remove only --- tests-integration/account.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests-integration/account.test.ts b/tests-integration/account.test.ts index a98c1c3..1594fa6 100644 --- a/tests-integration/account.test.ts +++ b/tests-integration/account.test.ts @@ -75,7 +75,7 @@ describe("Gifting", function () { await expectRevertWithErrorMessage("gift-acc/only-protocol", () => claimContract.__validate__([])); }); - it.only(`Test claim contract cant call another contract`, async function () { + it(`Test claim contract cant call another contract`, async function () { const { factory, claimAccountClassHash } = await setupGiftProtocol(); const { claim, claimPrivateKey } = await defaultDepositTestSetup(factory); const receiver = randomReceiver(); From 73d96a122ebcef0fbbfed3c05ff905f5e7a93687 Mon Sep 17 00:00:00 2001 From: Leonard Paturel Date: Fri, 7 Jun 2024 16:52:20 +0100 Subject: [PATCH 29/29] remove export --- lib/claim.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/claim.ts b/lib/claim.ts index 625adc1..dd95d09 100644 --- a/lib/claim.ts +++ b/lib/claim.ts @@ -100,11 +100,7 @@ export async function claimInternal( )) as TransactionReceipt; } -export const randomReceiver = (): string => { - return `0x${encode.buf2hex(ec.starkCurve.utils.randomPrivateKey())}`; -}; - -export function useTxv3(tokenAddress: string): boolean { +function useTxv3(tokenAddress: string): boolean { if (tokenAddress === ethAddress) { return false; } else if (tokenAddress === strkAddress) { @@ -112,3 +108,7 @@ export function useTxv3(tokenAddress: string): boolean { } throw new Error(`Unsupported token`); } + +export const randomReceiver = (): string => { + return `0x${encode.buf2hex(ec.starkCurve.utils.randomPrivateKey())}`; +};