diff --git a/.env.devnet.example b/.env.devnet.example deleted file mode 100644 index 7f960bd5..00000000 --- a/.env.devnet.example +++ /dev/null @@ -1,38 +0,0 @@ -## These values should not be changed during the course of the tutorials ## -# Source chain key for Sepolia on CC3 Devnet (different from its chain ID, which is 11155111) -# Chain key is the source chain identifier for all ASC interactions on Creditcoin -# You can obtain the source chain key through the SupportedChains smart contract deployed on CC3 Devnet -SOURCE_CHAIN_KEY=1 -# Proof Builder URL for CC3 Devnet -PROOF_BUILDER_URL="https://prover.cc3-devnet.creditcoin.network/" -# ERC20 burner contract on Sepolia -SOURCE_CHAIN_CONTRACT_ADDRESS="0x53311D909aC66Fb2a37A90466991B17BFaFB6EBa" -# ASC Minter contract on CC3 Devnet -ASC_MINTER_CONTRACT_ADDRESS="0x914Cf96BF28b7b4921db27b264ecEd71aC91134E" -# ASC Mintable Token contract on CC3 Devnet -ASC_MINTABLE_TOKEN="0x9d960f72121189de0ca0bcE0133359a1285a4a7C" # betterleaks:allow -# Creditcoin RPC URL for CC3 Devnet -CREDITCOIN_RPC_URL="https://rpc.cc3-devnet.creditcoin.network" - -## These values will get filled as the tutorials are performed ## -# Address of the Sepolia RPC URL e.g: https://sepolia.infura.io/v3/ -SOURCE_CHAIN_RPC_URL="" -# Address of your custom ERC20 contract for burning on Sepolia -SOURCE_CHAIN_CUSTOM_CONTRACT_ADDRESS="" -# Address of your custom ASC Minter contract on CC3 Devnet -ASC_CUSTOM_MINTER_CONTRACT_ADDRESS="" -# Your custom ASC Mintable Token contract on CC3 Devnet -ASC_CUSTOM_MINTABLE_TOKEN="" -# Your private key on CC3 Devnet and Sepolia -CREDITCOIN_WALLET_PRIVATE_KEY="" - -# Address of the custom ASC Loan manager contract on CC3 Devnet -ASC_LOAN_MANAGER_CONTRACT_ADDRESS="" -# Address of the custom Loan contract on Sepolia -SOURCE_CHAIN_LOAN_CONTRACT_ADDRESS="" -# Addres of source chain ERC20 contract from which funding will happen -SOURCE_CHAIN_ERC20_CONTRACT_ADDRESS="" -# Private key of the account to use as lender in the loan flow -LENDER_WALLET_PRIVATE_KEY="" -# Private key of the account to use as borrower in the loan flow -BORROWER_WALLET_PRIVATE_KEY="" diff --git a/.env.example b/.env.example deleted file mode 100644 index b4b62cde..00000000 --- a/.env.example +++ /dev/null @@ -1,38 +0,0 @@ -## These values should not be changed during the course of the tutorials ## -# Source chain key for Sepolia on CC3 Testnet (different from its chain ID, which is 11155111) -# Chain key is the source chain identifier for all ASC interactions on Creditcoin -# You can obtain the source chain key through the SupportedChains smart contract deployed on CC3 Testnet -SOURCE_CHAIN_KEY=1 -# Proof Builder URL for CC3 Testnet -PROOF_BUILDER_URL="https://prover.cc3-testnet.creditcoin.network" -# ERC20 burner contract on Sepolia -SOURCE_CHAIN_CONTRACT_ADDRESS="0x0F24FD9e0524BA53d3f0A4A40350Adf5370b4A53" -# ASC Minter contract on CC3 Testnet -ASC_MINTER_CONTRACT_ADDRESS="0x2Be9B8640ED32815d3B9e8C92AbcD3F15F07396f" -# ASC Mintable Token contract on CC3 Testnet -ASC_MINTABLE_TOKEN="0x914Cf96BF28b7b4921db27b264ecEd71aC91134E" # betterleaks:allow -# Creditcoin RPC URL for CC3 Testnet -CREDITCOIN_RPC_URL="https://rpc.cc3-testnet.creditcoin.network" - -## These values will get filled as the tutorials are performed ## -# Address of the Sepolia RPC URL e.g: https://sepolia.infura.io/v3/ -SOURCE_CHAIN_RPC_URL="" -# Address of your custom ERC20 contract for burning on Sepolia -SOURCE_CHAIN_CUSTOM_CONTRACT_ADDRESS="" -# Address of your custom ASC Minter contract on CC3 Testnet -ASC_CUSTOM_MINTER_CONTRACT_ADDRESS="" -# Address of your custom MintableToken -ASC_CUSTOM_MINTABLE_TOKEN="" -# Your private key on CC3 Testnet and Sepolia -CREDITCOIN_WALLET_PRIVATE_KEY="" - -# Address of the custom ASC Loan manager contract on CC3 Testnet -ASC_LOAN_MANAGER_CONTRACT_ADDRESS="" -# Address of the custom Loan contract on Sepolia -SOURCE_CHAIN_LOAN_CONTRACT_ADDRESS="" -# Addres of source chain ERC20 contract from which funding will happen -SOURCE_CHAIN_ERC20_CONTRACT_ADDRESS="" -# Private key of the account to use as lender in the loan flow -LENDER_WALLET_PRIVATE_KEY="" -# Private key of the account to use as borrower in the loan flow -BORROWER_WALLET_PRIVATE_KEY="" diff --git a/.eslintrc.js b/.eslintrc.js index 8387e6e5..c31d33b7 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -32,6 +32,7 @@ module.exports = { // "eslint-plugin-react", '@typescript-eslint', ], + ignorePatterns: ['vendor/**', 'node_modules/**', 'dist/**', 'lib/**', 'out/**'], rules: { '@typescript-eslint/adjacent-overload-signatures': 'error', '@typescript-eslint/array-type': [ diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..62aa89a3 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,3 @@ +bridge/contracts/abi/*.json text eol=lf +loan/contracts/abi/*.json text eol=lf +shared/contracts/abi/*.json text eol=lf diff --git a/.github/install-solidity-compiler.sh b/.github/install-solidity-compiler.sh index 091d8506..9c626a76 100755 --- a/.github/install-solidity-compiler.sh +++ b/.github/install-solidity-compiler.sh @@ -4,6 +4,6 @@ set -euo pipefail -curl -L https://github.com/ethereum/solidity/releases/download/v0.8.29/solc-static-linux > /usr/bin/solc +curl -L https://github.com/ethereum/solidity/releases/download/v0.8.30/solc-static-linux > /usr/bin/solc chmod a+x /usr/bin/solc diff --git a/.github/workflows/bridge-offchain-worker.yml b/.github/workflows/bridge-offchain-worker.yml index f0715105..4d94340b 100644 --- a/.github/workflows/bridge-offchain-worker.yml +++ b/.github/workflows/bridge-offchain-worker.yml @@ -163,11 +163,14 @@ jobs: cache: 'yarn' - run: npm install -g yarn + - name: Install dependencies + run: yarn install --ignore-engines + - name: Setup id: setup + env: + FOUNDRY_VIA_IR: "true" run: | - yarn install - SOURCE_CHAIN_RPC_URL="${{ steps.env.outputs.source_chain_rpc_url }}" echo "INFO: generate a new wallet" @@ -198,7 +201,7 @@ jobs: --broadcast \ --rpc-url "$SOURCE_CHAIN_RPC_URL" \ --private-key "$WALLET_PK" \ - contracts/sol/TestERC20.sol:TestERC20 2>&1) + shared/contracts/sol/TestERC20.sol:TestERC20 2>&1) echo "$OUTPUT" SOURCE_CHAIN_CONTRACT_ADDRESS=$(echo "$OUTPUT" | grep "Deployed to:" | cut -f2 -d: | tr -d " ") echo "source_chain_contract_address=$SOURCE_CHAIN_CONTRACT_ADDRESS" >> "$GITHUB_OUTPUT" @@ -208,18 +211,25 @@ jobs: --broadcast \ --rpc-url "$CREDITCOIN_RPC_URL" \ --private-key "$WALLET_PK" \ - node_modules/@gluwa/usc-contracts/contracts/decoding/EvmV1Decoder.sol:EvmV1Decoder 2>&1) + node_modules/@gluwa/asc-contracts/contracts/common/EvmV1Decoder.sol:EvmV1Decoder 2>&1) echo "$OUTPUT" DECODER_CONTRACT_ADDRESS=$(echo "$OUTPUT" | grep "Deployed to:" | cut -f2 -d: | tr -d " ") echo "INFO: deploy ASCMinter.sol to Creditcoin chain" + set +e OUTPUT=$(~/.foundry/bin/forge create \ --broadcast \ --rpc-url "$CREDITCOIN_RPC_URL" \ --private-key "$WALLET_PK" \ - --libraries "node_modules/@gluwa/usc-contracts/contracts/decoding/EvmV1Decoder.sol:EvmV1Decoder:$DECODER_CONTRACT_ADDRESS" \ - contracts/sol/ASCMinter.sol:ASCMinter 2>&1) + --libraries "node_modules/@gluwa/asc-contracts/contracts/common/EvmV1Decoder.sol:EvmV1Decoder:$DECODER_CONTRACT_ADDRESS" \ + bridge/contracts/sol/ASCMinter.sol:ASCMinter 2>&1) + STATUS=$? + set -e echo "$OUTPUT" + if [ "$STATUS" -ne 0 ]; then + echo "ERROR: ASCMinter forge create failed (exit $STATUS)" + exit "$STATUS" + fi ASC_MINTER_CONTRACT_ADDRESS=$(echo "$OUTPUT" | grep "Deployed to:" | cut -f2 -d: | tr -d " ") echo "asc_minter_contract_address=$ASC_MINTER_CONTRACT_ADDRESS" >> "$GITHUB_OUTPUT" @@ -228,7 +238,7 @@ jobs: --broadcast \ --rpc-url "$CREDITCOIN_RPC_URL" \ --private-key "$WALLET_PK" \ - contracts/sol/BridgeTestToken.sol:BridgeTestToken \ + bridge/contracts/sol/BridgeTestToken.sol:BridgeTestToken \ --constructor-args "$ASC_MINTER_CONTRACT_ADDRESS" 2>&1) echo "$OUTPUT" ASC_MINTABLE_TOKEN=$(echo "$OUTPUT" | grep "Deployed to:" | cut -f2 -d: | tr -d " ") diff --git a/.github/workflows/custom-contracts-bridging.yml b/.github/workflows/custom-contracts-bridging.yml index f1cdf554..baf6b671 100644 --- a/.github/workflows/custom-contracts-bridging.yml +++ b/.github/workflows/custom-contracts-bridging.yml @@ -163,11 +163,14 @@ jobs: cache: 'yarn' - run: npm install -g yarn + - name: Install dependencies + run: yarn install --ignore-engines + - name: Setup id: setup + env: + FOUNDRY_VIA_IR: "true" run: | - yarn install - SOURCE_CHAIN_RPC_URL="${{ steps.env.outputs.source_chain_rpc_url }}" echo "INFO: generate a new wallet" @@ -198,7 +201,7 @@ jobs: --broadcast \ --rpc-url "$SOURCE_CHAIN_RPC_URL" \ --private-key "$WALLET_PK" \ - contracts/sol/TestERC20.sol:TestERC20 2>&1) + shared/contracts/sol/TestERC20.sol:TestERC20 2>&1) echo "$OUTPUT" SOURCE_CHAIN_CONTRACT_ADDRESS=$(echo "$OUTPUT" | grep "Deployed to:" | cut -f2 -d: | tr -d " ") echo "source_chain_contract_address=$SOURCE_CHAIN_CONTRACT_ADDRESS" >> "$GITHUB_OUTPUT" @@ -208,22 +211,29 @@ jobs: --broadcast \ --rpc-url "$CREDITCOIN_RPC_URL" \ --private-key "$WALLET_PK" \ - node_modules/@gluwa/usc-contracts/contracts/decoding/EvmV1Decoder.sol:EvmV1Decoder 2>&1) + node_modules/@gluwa/asc-contracts/contracts/common/EvmV1Decoder.sol:EvmV1Decoder 2>&1) echo "$OUTPUT" DECODER_CONTRACT_ADDRESS=$(echo "$OUTPUT" | grep "Deployed to:" | cut -f2 -d: | tr -d " ") echo "INFO: Modify ASCMinter.sol to mint 2x" - sed -i "s/ASCMintableToken(wrappedTokenAddress).mint(burntFrom, burntValue);/ASCMintableToken(wrappedTokenAddress).mint(burntFrom, burntValue * 2);/" contracts/sol/ASCMinter.sol + sed -i "s/ASCMintableToken(wrappedTokenAddress).mint(burntFrom, burntValue);/ASCMintableToken(wrappedTokenAddress).mint(burntFrom, burntValue * 2);/" bridge/contracts/sol/ASCMinter.sol git diff echo "INFO: deploy ASCMinter.sol to Creditcoin chain" + set +e OUTPUT=$(~/.foundry/bin/forge create \ --broadcast \ --rpc-url "$CREDITCOIN_RPC_URL" \ --private-key "$WALLET_PK" \ - --libraries "node_modules/@gluwa/usc-contracts/contracts/decoding/EvmV1Decoder.sol:EvmV1Decoder:$DECODER_CONTRACT_ADDRESS" \ - contracts/sol/ASCMinter.sol:ASCMinter 2>&1) + --libraries "node_modules/@gluwa/asc-contracts/contracts/common/EvmV1Decoder.sol:EvmV1Decoder:$DECODER_CONTRACT_ADDRESS" \ + bridge/contracts/sol/ASCMinter.sol:ASCMinter 2>&1) + STATUS=$? + set -e echo "$OUTPUT" + if [ "$STATUS" -ne 0 ]; then + echo "ERROR: ASCMinter forge create failed (exit $STATUS)" + exit "$STATUS" + fi ASC_MINTER_CONTRACT_ADDRESS=$(echo "$OUTPUT" | grep "Deployed to:" | cut -f2 -d: | tr -d " ") echo "asc_minter_contract_address=$ASC_MINTER_CONTRACT_ADDRESS" >> "$GITHUB_OUTPUT" @@ -232,7 +242,7 @@ jobs: --broadcast \ --rpc-url "$CREDITCOIN_RPC_URL" \ --private-key "$WALLET_PK" \ - contracts/sol/BridgeTestToken.sol:BridgeTestToken \ + bridge/contracts/sol/BridgeTestToken.sol:BridgeTestToken \ --constructor-args "$ASC_MINTER_CONTRACT_ADDRESS" 2>&1) echo "$OUTPUT" ASC_MINTABLE_TOKEN=$(echo "$OUTPUT" | grep "Deployed to:" | cut -f2 -d: | tr -d " ") diff --git a/.github/workflows/examples-devnet.yml b/.github/workflows/examples-devnet.yml index 0c295714..a1eab1af 100644 --- a/.github/workflows/examples-devnet.yml +++ b/.github/workflows/examples-devnet.yml @@ -43,9 +43,7 @@ jobs: secrets: inherit loan-flow: - if: false - # chain examples b/c they use the same wallet and we get - # replacement transaction underpriced and/or nonce errors + # Run after other devnet examples (shared wallet) to avoid nonce collisions. needs: bridge-offchain-worker uses: ./.github/workflows/loan-flow.yml with: diff --git a/.github/workflows/examples.yml b/.github/workflows/examples.yml index 8ae1f496..bf9e45d9 100644 --- a/.github/workflows/examples.yml +++ b/.github/workflows/examples.yml @@ -25,6 +25,7 @@ jobs: with: source_chain_kind: "anvil" asc_kind: "local" + secrets: inherit custom-contracts-bridging: needs: build @@ -32,6 +33,7 @@ jobs: with: source_chain_kind: "anvil" asc_kind: "local" + secrets: inherit bridge-offchain-worker: needs: build @@ -39,6 +41,7 @@ jobs: with: source_chain_kind: "anvil" asc_kind: "local" + secrets: inherit loan-flow: needs: build @@ -46,3 +49,4 @@ jobs: with: source_chain_kind: "anvil" asc_kind: "local" + secrets: inherit diff --git a/.github/workflows/hello-bridge.yml b/.github/workflows/hello-bridge.yml index df74f99a..c253a0e3 100644 --- a/.github/workflows/hello-bridge.yml +++ b/.github/workflows/hello-bridge.yml @@ -163,11 +163,15 @@ jobs: cache: 'yarn' - run: npm install -g yarn + - name: Install dependencies + run: yarn install --ignore-engines + - name: Setup id: setup + env: + # ASCMinter + package ASCBase need via-IR (stack too deep otherwise). + FOUNDRY_VIA_IR: "true" run: | - yarn install - SOURCE_CHAIN_RPC_URL="${{ steps.env.outputs.source_chain_rpc_url }}" echo "INFO: generate a new wallet" @@ -198,7 +202,7 @@ jobs: --broadcast \ --rpc-url "$SOURCE_CHAIN_RPC_URL" \ --private-key ${{ steps.env.outputs.source_chain_private_key }} \ - contracts/sol/TestERC20.sol:TestERC20 2>&1) + shared/contracts/sol/TestERC20.sol:TestERC20 2>&1) echo "$OUTPUT" SOURCE_CHAIN_CONTRACT_ADDRESS=$(echo "$OUTPUT" | grep "Deployed to:" | cut -f2 -d: | tr -d " ") echo "source_chain_contract_address=$SOURCE_CHAIN_CONTRACT_ADDRESS" >> "$GITHUB_OUTPUT" @@ -208,18 +212,25 @@ jobs: --broadcast \ --rpc-url "$CREDITCOIN_RPC_URL" \ --private-key ${{ steps.env.outputs.execution_chain_private_key }} \ - node_modules/@gluwa/usc-contracts/contracts/decoding/EvmV1Decoder.sol:EvmV1Decoder 2>&1) + node_modules/@gluwa/asc-contracts/contracts/common/EvmV1Decoder.sol:EvmV1Decoder 2>&1) echo "$OUTPUT" DECODER_CONTRACT_ADDRESS=$(echo "$OUTPUT" | grep "Deployed to:" | cut -f2 -d: | tr -d " ") echo "INFO: deploy ASCMinter.sol to Creditcoin chain" + set +e OUTPUT=$(~/.foundry/bin/forge create \ --broadcast \ --rpc-url "$CREDITCOIN_RPC_URL" \ --private-key ${{ steps.env.outputs.execution_chain_private_key }} \ - --libraries "node_modules/@gluwa/usc-contracts/contracts/decoding/EvmV1Decoder.sol:EvmV1Decoder:$DECODER_CONTRACT_ADDRESS" \ - contracts/sol/ASCMinter.sol:ASCMinter 2>&1) + --libraries "node_modules/@gluwa/asc-contracts/contracts/common/EvmV1Decoder.sol:EvmV1Decoder:$DECODER_CONTRACT_ADDRESS" \ + bridge/contracts/sol/ASCMinter.sol:ASCMinter 2>&1) + STATUS=$? + set -e echo "$OUTPUT" + if [ "$STATUS" -ne 0 ]; then + echo "ERROR: ASCMinter forge create failed (exit $STATUS)" + exit "$STATUS" + fi ASC_MINTER_CONTRACT_ADDRESS=$(echo "$OUTPUT" | grep "Deployed to:" | cut -f2 -d: | tr -d " ") echo "minter_contract_address=$ASC_MINTER_CONTRACT_ADDRESS" >> "$GITHUB_OUTPUT" @@ -228,7 +239,7 @@ jobs: --broadcast \ --rpc-url "$CREDITCOIN_RPC_URL" \ --private-key "$WALLET_PK" \ - contracts/sol/BridgeTestToken.sol:BridgeTestToken \ + bridge/contracts/sol/BridgeTestToken.sol:BridgeTestToken \ --constructor-args "$ASC_MINTER_CONTRACT_ADDRESS" 2>&1) echo "$OUTPUT" ASC_MINTABLE_TOKEN=$(echo "$OUTPUT" | grep "Deployed to:" | cut -f2 -d: | tr -d " ") diff --git a/.github/workflows/javascript-checks.yaml b/.github/workflows/javascript-checks.yaml index 6491479e..80f119cc 100644 --- a/.github/workflows/javascript-checks.yaml +++ b/.github/workflows/javascript-checks.yaml @@ -40,13 +40,11 @@ jobs: echo "$HOME/.foundry/bin" >> "$GITHUB_PATH" + - name: Install forge-std + run: git clone --depth 1 https://github.com/foundry-rs/forge-std lib/forge-std + - name: Install dependencies - run: | - if [ -f yarn.lock ]; then - yarn install - else - npm clean-install - fi + run: yarn install --ignore-engines - name: Execute command ${{ matrix.command }} run: | diff --git a/.github/workflows/loan-flow.yml b/.github/workflows/loan-flow.yml index 8dd33790..6b09b467 100644 --- a/.github/workflows/loan-flow.yml +++ b/.github/workflows/loan-flow.yml @@ -28,6 +28,7 @@ permissions: read-all jobs: example: runs-on: ${{ fromJSON(inputs.runs-on) }} + timeout-minutes: 90 steps: - name: Anvil ENV id: anvil @@ -163,11 +164,17 @@ jobs: cache: 'yarn' - run: npm install -g yarn + - name: Install dependencies + run: yarn install --ignore-engines + + - name: Install forge-std + run: git clone --depth 1 https://github.com/foundry-rs/forge-std lib/forge-std + - name: Setup id: setup + env: + FOUNDRY_VIA_IR: "true" run: | - yarn install - SOURCE_CHAIN_RPC_URL="${{ steps.env.outputs.source_chain_rpc_url }}" echo "INFO: generate a new wallet" @@ -179,9 +186,9 @@ jobs: WALLET_PK=$(echo "$OUTPUT" | grep "Private key:" | cut -f2 -d: | tr -d ' ') echo "creditcoin_wallet_pk=$WALLET_PK" >> "$GITHUB_OUTPUT" - echo "INFO: get 0.001 ETH on source chain" + echo "INFO: get 0.01 ETH on source chain" ~/.foundry/bin/cast send "$WALLET_ADDRESS" "0x" \ - --value 1000000000000000 \ + --value 10000000000000000 \ --private-key ${{ steps.env.outputs.source_chain_private_key }} \ --rpc-url "$SOURCE_CHAIN_RPC_URL" @@ -194,22 +201,36 @@ jobs: --rpc-url "$CREDITCOIN_RPC_URL" echo "INFO: deploy TestERC20.sol to source chain" + set +e OUTPUT=$(~/.foundry/bin/forge create \ --broadcast \ --rpc-url "$SOURCE_CHAIN_RPC_URL" \ - --private-key "$WALLET_PK" \ - contracts/sol/TestERC20.sol:TestERC20 2>&1) + --private-key ${{ steps.env.outputs.source_chain_private_key }} \ + shared/contracts/sol/TestERC20.sol:TestERC20 2>&1) + STATUS=$? + set -e echo "$OUTPUT" + if [ "$STATUS" -ne 0 ]; then + echo "ERROR: TestERC20 forge create failed (exit $STATUS)" + exit "$STATUS" + fi SOURCE_CHAIN_CONTRACT_ADDRESS=$(echo "$OUTPUT" | grep "Deployed to:" | cut -f2 -d: | tr -d " ") echo "source_chain_contract_address=$SOURCE_CHAIN_CONTRACT_ADDRESS" >> "$GITHUB_OUTPUT" echo "INFO: deploy AuxiliaryLoanContract.sol to source chain" + set +e OUTPUT=$(~/.foundry/bin/forge create \ --broadcast \ --rpc-url "$SOURCE_CHAIN_RPC_URL" \ --private-key "$WALLET_PK" \ - contracts/sol/AuxiliaryLoanContract.sol:AuxiliaryLoanContract 2>&1) + loan/contracts/sol/AuxiliaryLoanContract.sol:AuxiliaryLoanContract 2>&1) + STATUS=$? + set -e echo "$OUTPUT" + if [ "$STATUS" -ne 0 ]; then + echo "ERROR: AuxiliaryLoanContract forge create failed (exit $STATUS)" + exit "$STATUS" + fi AUX_LOAN_CONTRACT_ADDRESS=$(echo "$OUTPUT" | grep "Deployed to:" | cut -f2 -d: | tr -d " ") echo "aux_loan_contract_address=$AUX_LOAN_CONTRACT_ADDRESS" >> "$GITHUB_OUTPUT" @@ -218,18 +239,25 @@ jobs: --broadcast \ --rpc-url "$CREDITCOIN_RPC_URL" \ --private-key "$WALLET_PK" \ - node_modules/@gluwa/usc-contracts/contracts/decoding/EvmV1Decoder.sol:EvmV1Decoder 2>&1) + node_modules/@gluwa/asc-contracts/contracts/common/EvmV1Decoder.sol:EvmV1Decoder 2>&1) echo "$OUTPUT" DECODER_CONTRACT_ADDRESS=$(echo "$OUTPUT" | grep "Deployed to:" | cut -f2 -d: | tr -d " ") echo "INFO: deploy ASCLoanManager.sol to Creditcoin" + set +e OUTPUT=$(~/.foundry/bin/forge create \ --broadcast \ --rpc-url "$CREDITCOIN_RPC_URL" \ --private-key "$WALLET_PK" \ - --libraries "node_modules/@gluwa/usc-contracts/contracts/decoding/EvmV1Decoder.sol:EvmV1Decoder:$DECODER_CONTRACT_ADDRESS" \ - contracts/sol/ASCLoanManager.sol:ASCLoanManager 2>&1) + --libraries "node_modules/@gluwa/asc-contracts/contracts/common/EvmV1Decoder.sol:EvmV1Decoder:$DECODER_CONTRACT_ADDRESS" \ + loan/contracts/sol/ASCLoanManager.sol:ASCLoanManager 2>&1) + STATUS=$? + set -e echo "$OUTPUT" + if [ "$STATUS" -ne 0 ]; then + echo "ERROR: ASCLoanManager forge create failed (exit $STATUS)" + exit "$STATUS" + fi ASC_LOAN_MANAGER_CONTRACT_ADDRESS=$(echo "$OUTPUT" | grep "Deployed to:" | cut -f2 -d: | tr -d " ") echo "asc_loan_manager_contract_address=$ASC_LOAN_MANAGER_CONTRACT_ADDRESS" >> "$GITHUB_OUTPUT" @@ -300,6 +328,7 @@ jobs: echo "CREDITCOIN_WALLET_PRIVATE_KEY='${{ steps.setup.outputs.creditcoin_wallet_pk }}'" >> .env echo "LENDER_WALLET_PRIVATE_KEY='${{ steps.setup.outputs.lender_wallet_pk }}'" >> .env echo "BORROWER_WALLET_PRIVATE_KEY='${{ steps.setup.outputs.borrower_wallet_pk }}'" >> .env + echo "LOAN_REGISTER_SOURCE_FUNDING_IN_REGISTER=1" >> .env - name: Exercise example run: | @@ -308,13 +337,13 @@ jobs: echo "INFO: Lender should have 5 tokens initially" OUTPUT=$(yarn utils:check_balance "$SOURCE_CHAIN_ERC20_CONTRACT_ADDRESS" "${{ steps.setup.outputs.lender_wallet_address }}" "$SOURCE_CHAIN_RPC_URL") echo "$OUTPUT" - echo "$OUTPUT" | grep "Raw Balance: 5000000000" + echo "$OUTPUT" | grep "Raw Balance: 5000000000000000000" echo "$OUTPUT" | grep "Formatted Balance: 5.0" echo "INFO: Borrower should have 3 tokens initially" OUTPUT=$(yarn utils:check_balance "$SOURCE_CHAIN_ERC20_CONTRACT_ADDRESS" "${{ steps.setup.outputs.borrower_wallet_address }}" "$SOURCE_CHAIN_RPC_URL") echo "$OUTPUT" - echo "$OUTPUT" | grep "Raw Balance: 3000000000" + echo "$OUTPUT" | grep "Raw Balance: 3000000000000000000" echo "$OUTPUT" | grep "Formatted Balance: 3.0" @@ -330,26 +359,15 @@ jobs: echo "$OUTPUT" | grep "Source loan contract registered with transaction hash:" - echo "INFO: start offchain worker" - yarn loan_flow:start_worker > /var/tmp/loan-flow-logs/offchain-worker.log 2>&1 & - sleep 10 - - echo "INFO: Register a loan" # , , OUTPUT=$(yarn loan_flow:register_loan 1000 500 1000) echo "$OUTPUT" echo "$OUTPUT" | grep "Loan registered with transaction hash:" + echo "$OUTPUT" | grep "Registered loan .* for funding on source chain" LOAN_ID=$(echo "$OUTPUT" | grep "Loan successfully registered with ID:" | cut -f2 -d: | tr -d ' ') - # wait for the off-chain worker to see the event above - # in particular for the source block to be attested to - sleep 90 - grep "Detected LoanRegistered event for loanId: $LOAN_ID" /var/tmp/loan-flow-logs/offchain-worker.log - grep "Registered loan $LOAN_ID for funding on source chain" /var/tmp/loan-flow-logs/offchain-worker.log - - echo "INFO: inspect loan $LOAN_ID" yarn loan_flow:inspect_loan "$LOAN_ID" @@ -357,19 +375,8 @@ jobs: echo "INFO: Lender completely funds the loan" OUTPUT=$(yarn loan_flow:fund_loan "$LOAN_ID" 1000) echo "$OUTPUT" - - # wait for the off-chain worker to see the event above - # in particular for the source block to be attested to - sleep 90 - grep "Detected LoanFunded event for loanId: $LOAN_ID" /var/tmp/loan-flow-logs/offchain-worker.log - - - # wait for off-chain worker to prove the event above - sleep 600 - echo "---- last 3 lines of offchain-worker.log for debugging ----" - tail -n 3 /var/tmp/loan-flow-logs/offchain-worker.log - echo "INFO: Printing LoanFunded result:" - grep "Loan $LOAN_ID has been marked as funded on Creditcoin" /var/tmp/loan-flow-logs/offchain-worker.log + echo "$OUTPUT" | grep "Loan funded:" + yarn loan_flow:wait_loan_status "$LOAN_ID" Funded 1500 echo "INFO: inspect loan $LOAN_ID" @@ -379,18 +386,8 @@ jobs: echo "INFO: Borrower repays loan + interest" OUTPUT=$(yarn loan_flow:repay_loan "$LOAN_ID" 1050) echo "$OUTPUT" - - - sleep 90 - grep "Detected LoanRepaid event for loanId: $LOAN_ID" /var/tmp/loan-flow-logs/offchain-worker.log - - - # wait for off-chain worker to prove the event above - sleep 600 - echo "---- last 3 lines of offchain-worker.log for debugging ----" - tail -n 3 /var/tmp/loan-flow-logs/offchain-worker.log - echo "INFO: Printing LoanRepaid result:" - grep "Loan $LOAN_ID has been marked as fully repaid on Creditcoin" /var/tmp/loan-flow-logs/offchain-worker.log + echo "$OUTPUT" | grep "Loan repaid:" + yarn loan_flow:wait_loan_status "$LOAN_ID" Repaid 1500 echo "INFO: inspect loan $LOAN_ID" @@ -401,13 +398,11 @@ jobs: OUTPUT=$(yarn utils:check_balance "$SOURCE_CHAIN_ERC20_CONTRACT_ADDRESS" "${{ steps.setup.outputs.lender_wallet_address }}" "$SOURCE_CHAIN_RPC_URL") echo "$OUTPUT" echo "$OUTPUT" | grep "Raw Balance: 5000000000000000050" - echo "$OUTPUT" | grep "Formatted Balance: 5.00000000000000005" echo "INFO: Borrower should have 3 tokens - interest" OUTPUT=$(yarn utils:check_balance "$SOURCE_CHAIN_ERC20_CONTRACT_ADDRESS" "${{ steps.setup.outputs.borrower_wallet_address }}" "$SOURCE_CHAIN_RPC_URL") echo "$OUTPUT" echo "$OUTPUT" | grep "Raw Balance: 2999999999999999950" - echo "$OUTPUT" | grep "Formatted Balance: 2.99999999999999995" - name: Upload logs if: always() diff --git a/.github/workflows/solidity.yml b/.github/workflows/solidity.yml index bb63de21..9692f189 100644 --- a/.github/workflows/solidity.yml +++ b/.github/workflows/solidity.yml @@ -20,10 +20,12 @@ jobs: - name: Install foundry run: | export FOUNDRY_DIR="$HOME/.foundry" - # first install it curl -L https://foundry.paradigm.xyz | bash ~/.foundry/bin/foundryup + - name: Install forge-std + run: git clone --depth 1 https://github.com/foundry-rs/forge-std lib/forge-std + - name: Install Node.js uses: actions/setup-node@v6 with: @@ -31,11 +33,23 @@ jobs: cache: 'yarn' - run: npm install -g yarn - - name: Compile smart contracts + - name: Install dependencies + run: yarn install --ignore-engines + + - name: Compile and test smart contracts run: | - yarn install + echo "INFO: forge remappings (bridge)" + ~/.foundry/bin/forge remappings --root bridge || true + echo "INFO: forge remappings (loan)" + ~/.foundry/bin/forge remappings --root loan || true + ls -la node_modules/@gluwa/asc-contracts/contracts || true + ls -la node_modules/@gluwa/asc-contracts/contracts/readability || true + ls -la node_modules/@gluwa/asc-contracts/contracts/common || true - ~/.foundry/bin/forge build + ~/.foundry/bin/forge build --root bridge + ~/.foundry/bin/forge build --root loan + ~/.foundry/bin/forge test --root bridge + ~/.foundry/bin/forge test --root loan check-abi-on-disk: runs-on: ubuntu-26.04 @@ -53,21 +67,25 @@ jobs: cache: 'yarn' - run: npm install -g yarn + - name: Install dependencies + run: yarn install --ignore-engines + - name: Regenerate the ABI run: | - yarn install - solc --version - ./contracts/abi-creator.sh + chmod +x ./bridge/contracts/abi-creator.sh ./loan/contracts/abi-creator.sh ./shared/contracts/abi-creator.sh + ./bridge/contracts/abi-creator.sh + ./loan/contracts/abi-creator.sh + ./shared/contracts/abi-creator.sh - name: Check for mismatch run: | echo "========== DEBUG DEBUG DEBUG ==========" - git status --short - git diff + git status --short -- bridge/contracts/abi loan/contracts/abi shared/contracts/abi + git diff -- bridge/contracts/abi loan/contracts/abi shared/contracts/abi echo "=======================================" - if [ -n "$(git status --short)" ]; then + if [ -n "$(git status --short -- bridge/contracts/abi loan/contracts/abi shared/contracts/abi)" ]; then echo "FAIL: found ABI mismatch on disk vs. freshly generated from source" echo "TODO: update ABI on disk and commit the changes to git" exit 1 diff --git a/.gitignore b/.gitignore index 389a8e50..18f05843 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ **/dist/ **/target/ **/node_modules/ +lib/forge-std/ **/*.rs.bk *.swp **/._* @@ -26,3 +27,6 @@ megalinter-reports/ # Local secrets โ€” copy from .env.example / .env.devnet.example (see README) .env .env.devnet + +.tools/ +vendor/ diff --git a/.lycheeignore b/.lycheeignore index 0c601727..c2bf8c00 100644 --- a/.lycheeignore +++ b/.lycheeignore @@ -2,6 +2,7 @@ http://localhost(.*) ws|wss://(.*) bzz-raw://(.*) +file://.*\.env rpc\.(.*)\.creditcoin\.network/ staticsitellvmhtml.z13.web.core.windows.net thedoctor0/zip-release@(.*) @@ -24,3 +25,10 @@ https://github.com/gluwa/creditcoin3/pull(.*) https://developer.metamask.io(.*) https://(.*).creditcoin.network/ https://blockchain.googleapis.com/v1/projects/(.*) +https://github.com/gluwa/usc-contracts(.*) +https://github.com/gluwa/asc-contracts(.*) +https://docs.creditcoin.org/attestcoin-protocol(.*) +https://docs\.attestcoin\.org +https://www\.npmjs\.com/package/@gluwa/asc-contracts +https://cloud\.google\.com/application/web3/faucet/ethereum/sepolia +https://discord\.com diff --git a/.markdownlint.yaml b/.markdownlint.yaml new file mode 100644 index 00000000..1fd03c2a --- /dev/null +++ b/.markdownlint.yaml @@ -0,0 +1,8 @@ +# Tutorial docs: long lines, optional link defs, and tree fences are intentional. +default: true +MD013: false +MD028: false +MD033: false +MD040: false +MD041: false +MD053: false diff --git a/.mega-linter.yml b/.mega-linter.yml index 9c2adaf4..43faf870 100644 --- a/.mega-linter.yml +++ b/.mega-linter.yml @@ -26,5 +26,7 @@ DISABLE_LINTERS: - TYPESCRIPT_ES - TYPESCRIPT_STANDARD - SPELL_CSPELL + # Tutorial markdown uses long command/URL lines; table formatter is noisy on existing docs. + - MARKDOWN_MARKDOWN_TABLE_FORMATTER SHOW_ELAPSED_TIME: true diff --git a/.prettierignore b/.prettierignore index ecb28d00..351bc7cf 100644 --- a/.prettierignore +++ b/.prettierignore @@ -10,5 +10,13 @@ node_modules # Package-lock package-lock.json +# CI checkout of gluwa/asc-contracts +vendor + # TS Config tsconfig.json + +# ABI files are generated by solc+jq (abi-creator.sh); do not reformat with Prettier. +bridge/contracts/abi +loan/contracts/abi +shared/contracts/abi diff --git a/README.md b/README.md index 6b164c5c..330db5cf 100644 --- a/README.md +++ b/README.md @@ -1,87 +1,36 @@ -# ๐ŸŒ‰ ASC Testnet Bridge Examples ๐ŸŒ‰ +# ๐ŸŒ‰ Attestcoin Protocol Examples ๐ŸŒ‰ -This repository is designed as a starting point for users and builders alike to explore the new -Creditcoin cross-chain features brought forwards in the new `CC3 Testnet` update. Learn how to use -the Creditcoin Decentralized Oracle through a series of guided tutorials where you get to set up and -interact with your own decentralized bridge! +This repository is a starting point for exploring cross-chain inter-operability using the Attestcoin protocol. Today, it contains several examples covering Attestcoin readability. Coming soon: examples using Attestcoin writability! -## Before you start! +By running through the tutorials in this repository, you will set up and interact with your own decentralized bridge and loan examples. -Before attempting any of the tutorials, make sure the following are installed and available in your system: +## Before you start + +Before attempting any of the tutorials, make sure the following are installed: - [yarn] - [foundry] -After that install the required dependencies to run the examples, to do so simply run the -following command in the root of this repo: +Install dependencies from the repository root: ```sh yarn ``` -You will also need to set up the right version of foundry with `foundryup`: +Set up Foundry: ```bash foundryup --version v1.2.3 - -``` - -Next, create your own `.env` from the template. `.env` holds your wallet private key, so it is -git-ignored and never committed โ€” `.env.example` is the tracked copy, which carries only public -network defaults: - -```bash -cp .env.example .env -``` - -> [!CAUTION] -> Keep your private key in `.env` only. Never put it in `.env.example`, which is tracked by git. - -Finally, source the `.env` file to load your configuration globally for all examples: - -```bash -source .env ``` -This ensures that all tutorial commands can access your wallet private key and RPC URLs without needing to manually substitute them. - ## Tutorials -Each tutorial is built to be as self-contained as possible. However, we still recommend you go -through them in the following order: - -1. ๐Ÿ“š [Hello Bridge] -2. ๐Ÿ“š [Custom Contracts Bridging] -3. ๐Ÿ“š [Bridge Offchain Worker] -4. ๐Ÿ“š [Loan Flow] - -## Content - -Below is brief overview of each tutorial's content. - -### Hello Bridge - -Learn how to use the Creditcoin Decentralized Oracle from the perspective of an end user. [Hello -Bridge] makes use of pre-existing smart contracts on the Sepolia and Creditcoin Testnet so as to -minimize the amount of setup needed. - -### Custom Contracts Bridging - -Learn how to setup your own trustless cross-chain bridging logic by deploying your own contracts. -[Custom Contracts Bridging] teaches you the perspective of a DApp builder by showing you how to set -up custom logic across each chain you are deploying to. - -### Bridge Offchain Worker - -Streamline UX by automating away most transactions in the bridging process. [Bridge Offchain Worker] -shows you how offchain workers can be used to simplify the user flow of your cross-chain DApp. - -### Loan Flow +We recommend going through them in this order: -Building on top of what we learned with the [Bridge Offchain Worker] we take it a step further by -implementing a loaning system using both a ASC contract, a source chain helper and an offchain worker -to seamlesly coordinate between the two, and of course making use of our Oracle proving capabilities to -add the final touches of trust into the process. +1. ๐Ÿ“š [Hello Bridge] โ€” pre-deployed contracts, manual proof submit +2. ๐Ÿ“š [Custom Contracts Bridging] โ€” deploy your ASC + optional worker (ยง7) +3. ๐Ÿ“š [Bridge Offchain Worker] โ€” short reference (main flow in custom-contracts ยง7) +4. ๐Ÿ“š [Loan Flow] โ€” reuses decoder library from bridge ## External Resources @@ -91,10 +40,10 @@ add the final touches of trust into the process. [yarn]: https://yarnpkg.com/getting-started/install [foundry]: https://getfoundry.sh/ -[Hello Bridge]: ./hello-bridge/README.md -[Custom Contracts Bridging]: ./custom-contracts-bridging/README.md -[Bridge Offchain Worker]: ./bridge-offchain-worker/README.md -[Loan Flow]: ./loan-flow/README.md +[Hello Bridge]: ./bridge/hello-bridge/README.md +[Custom Contracts Bridging]: ./bridge/custom-contracts-bridging/README.md +[Bridge Offchain Worker]: ./bridge/bridge-offchain-worker/README.md +[Loan Flow]: ./loan/README.md [ASC Architecture Overview]: https://docs.attestcoin.org/attestcoin-protocol/architecture [DApp Builder Infrastructure]: https://docs.attestcoin.org/attestcoin-protocol/dapp-builder-infrastructure [Attestcoin Readability Subsystems]: https://docs.attestcoin.org/attestcoin-protocol/attestcoin-readability diff --git a/VERIFICATION.md b/VERIFICATION.md new file mode 100644 index 00000000..86bcf6a4 --- /dev/null +++ b/VERIFICATION.md @@ -0,0 +1,80 @@ +# Verification checklist + +Use this to validate the repository locally before release or deployment. + +## Automated (CI + local) + +From the repository root (requires [Foundry](https://getfoundry.sh/) on your `PATH`). Bridge and loan both inherit readability `ASCBase` and `EvmV1Decoder` from `@gluwa/asc-contracts` (`contracts/readability/`, `contracts/common/`): + +```sh +yarn +git clone --depth 1 https://github.com/foundry-rs/forge-std lib/forge-std # once +yarn verify +``` + +This runs: + +| Step | Command | What it checks | +| ---------- | ---------------- | ------------------------------------------------------------------------------------------- | +| TypeScript | `yarn typecheck` | Tutorial scripts compile | +| Lint | `yarn eslint` | TS/JS style | +| Contracts | `yarn build` | `forge build` for bridge + loan | +| Unit tests | `forge test` | Loan source-contract binding; bridge burn-log parsing + emitter registration / query dedupe | + +### CI (`solidity.yml`) + +- Compiles bridge and loan contracts +- Regenerates ABIs and fails on drift +- Runs `forge test` for bridge and loan (requires `yarn install` + `forge-std`) + +## Network preflight (optional, needs `.env`) + +Does **not** submit transactions. Verifies RPC, proof builder, native verifier precompile (`0xFD2`), and chain-key attestation: + +```sh +source .env +yarn utils:check_setup network # RPC + attestation only +yarn utils:check_setup hello # + hello-bridge contract addresses +yarn utils:check_setup bridge # + wallet key for custom bridge +yarn utils:check_setup loan # + loan contracts and lender/borrower keys +``` + +## Manual end-to-end โ€” Hello Bridge + +- [ ] `yarn utils:check_setup hello` passes +- [ ] Mint test tokens on Sepolia (`cast send โ€ฆ mint`) +- [ ] Burn tokens (`cast send โ€ฆ burn`) โ€” save tx hash +- [ ] `yarn hello_bridge:submit_query ` โ€” expect `Proof generation successful!` then `Tokens minted!` +- [ ] `yarn utils:check_balance $ASC_MINTABLE_TOKEN $WALLET` โ€” balance matches burn amount + +Expected mint output (abbreviated): + +```text +Block attested! Generating proof... +Proof generation successful! +Proof submitted: 0x... +Tokens minted! Contract: 0x..., To: 0x..., Amount: 50000000000000000000, QueryId: 0x... +``` + +## Manual end-to-end โ€” Custom bridge + loan + +- [ ] Deploy `EvmV1Decoder` once; set `EVM_V1_DECODER_LIBRARY_ADDRESS` +- [ ] Deploy `ASCMinter`, wrapped token, `wrapOriginToken` (registers the Sepolia burn emitter) +- [ ] Burn โ†’ `yarn custom_bridge:submit_query` โ†’ mint succeeds once +- [ ] Second submit with same proof reverts (`Query already processed`) +- [ ] Proof of a burn from an unregistered emitter reverts (`No wrapped token for emitter`) โ€” also covered by `forge test --root bridge` (`ASCMinterSecurity.t.sol`) +- [ ] Deploy loan stack; `yarn loan_flow:register_source_contract` +- [ ] Fund/repay on Sepolia; worker or manual proof updates loan status +- [ ] Spoofed `LoanFunded` from unregistered contract cannot mark loan funded (covered by unit tests) + +## Loan source-contract binding (unit tests) + +`forge test --root loan` must pass: + +- `testFundLog_rejectsUnregisteredSource` +- `testFundLog_rejectsWrongEmitter` +- `testRepayLog_rejectsWrongEmitter` +- `testFundLog_acceptsRegisteredEmitter` +- `testRepayLog_acceptsRegisteredEmitter` + +These assert fund/repay proofs must come from the registered `sourceLoanContract` address. diff --git a/bridge-offchain-worker/README.md b/bridge-offchain-worker/README.md deleted file mode 100644 index c80a2f4c..00000000 --- a/bridge-offchain-worker/README.md +++ /dev/null @@ -1,166 +0,0 @@ -# Bridge Offchain Worker - -> [!TIP] -> This tutorial builds on the previous [Custom Contract Bridging] example -make sure to check it out -> before moving on! - -So far we have seen [how to initiate a trustless bridge transaction] and -[how to customize our trustless bridging logic]. In this tutorial, we will be seeing how to automate -our interaction with the Creditcoin oracle so that end users only have to submit _a single transaction_ -on the Sepolia _source chain_. - -## What is an Offchain Worker - -An Offchain Worker is a script responsible for watching the state of a _source chain_: in this -case, Sepolia. In more complex cases this would also listen to state changes on the Creditcoin execution chain. The worker queries to our -_Attestcoin Smart Contract_ on Creditcoin in response to specific events on -each chain. With an offchain worker, all the end user needs to do is sign a single transaction -on the source chain kicking off cross-chain interaction. - -## 0. Install - -For our tutorial scripts to function properly, we need to install dependencies first. - -```bash -yarn install -``` - -## 1. Setup - -This is the same as in [Hello Bridge]. If you have not already done so, follow the installation -steps in the [setup] section there. - -Additionally you need to have the `ASC_CUSTOM_MINTER_CONTRACT_ADDRESS` environment variable set as seen in the [Custom Contract Bridging] tutorial. - -Once that is done, you will need to set up some additional configuration for the offchain worker. -Check the `.env` file in the root of the repository and make sure it contains the following: - -```env -# ============================================================================ # -# Source Chain Configuration # -# ============================================================================ # - -# RPC endpoint for Sepolia -SOURCE_CHAIN_RPC_URL="https://sepolia.infura.io/v3/" - -# Address of the ERC20 token contract on source chain -SOURCE_CHAIN_CUSTOM_CONTRACT_ADDRESS= - -# ============================================================================ # -# Creditcoin ASC Chain Configuration # -# ============================================================================ # - -# Address of your custom minter contract on Creditcoin -ASC_CUSTOM_MINTER_CONTRACT_ADDRESS= - -# Private key of the wallet that will submit mint requests -CREDITCOIN_WALLET_PRIVATE_KEY= -``` - -Once everyting is fine reload your `.env` file with: - -```sh -source .env -``` - -## 2. Start the Offchain Worker - -Once you have your worker configured, it's time to start automating some queries! - -Run the following command to start the worker: - -```sh -yarn offchain:start_worker -``` - -Once it's up and running, you start to see the following logs: - -```bash -Starting... -Worker started! Listening for burn events... -Polling source chain from block 11073265 -Polling ASC chain from block 4969330 -``` - -## 3. Burning the tokens you want to bridge - -Like we did in the previous tutorials, we start the bridging process by burning the funds we want to -bridge on Sepolia. This time however this will be the only transaction we need to submit! The rest -will be handled automatically by the worker ๐Ÿค– - -Open a new terminal window and source the `.env` file once again - -```sh -source .env -``` - -Then run the following command to initiate the burn: - -```sh -cast send --rpc-url $SOURCE_CHAIN_RPC_URL \ - $SOURCE_CHAIN_CUSTOM_CONTRACT_ADDRESS \ - "burn(uint256)" 2000 \ - --private-key $CREDITCOIN_WALLET_PRIVATE_KEY -``` - -> [!TIP] -> If your worker is not running when the transaction is being processed on the source chain it will not pick up -> the event! This example is made for simplicity, a more robust worker would be able to read events from -> previous blocks and have more complex event filters. - -## 4. Monitor the Offchain Worker - -At this point, you should see the worker picking up the event. - -```bash -Detected burn of 2000 tokens from 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 at 0xb5f8f1000432f92521021642a915999407c61ed1a9f13c2e6c37f6ac9b6eb6f0 -Transaction 0xb5f8f1000432f92521021642a915999407c61ed1a9f13c2e6c37f6ac9b6eb6f0 found in block 418 -Waiting for block 418 attestation on Creditcoin... -``` - -Eventually, you should see a message like this one: - -```bash -Tokens minted! Contract: 0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512, To: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266, Amount: 2000, QueryId: 0x3bfcf46c80011ef4280b7212e4fa11e5be314b12c9ddd56a74e83c2964b3e9be -``` - -That's it! All it took was a single transaction on your end to initiate the bridging process, -providing for a _truly native UX_. - -## 5. Check Balance in CC3 Testnet ERC20 Contract - -As a final check, we can take a look at the balance of your account on Creditcoin to confirm that -the bridging process was successful. - -Run the following command to check your funds: - -```sh -WALLET_ADDRESS=$(cast wallet address --private-key $CREDITCOIN_WALLET_PRIVATE_KEY) -yarn utils:check_balance $ASC_CUSTOM_MINTABLE_TOKEN $WALLET_ADDRESS -``` - -It should show something like this: - -```bash -๐Ÿ”— Using RPC URL: https://rpc.cc3-testnet.creditcoin.network -๐Ÿ“ฆ Token: Bridge Test Token (BTKT) -๐Ÿงพ Raw Balance: 100000000000000004000 -๐Ÿ’ฐ Formatted Balance: 100.000000000000004 BTKT -``` - -## Conclusion - -Congratulations! You've managed to run your first offchain worker! - -The next tutorial will take what we've learned here and increase the complexity with a more real world -example: a [Loan Flow], in which a user can lend tokens to another and through the oracle validate that the -repayments truly happened. - -[Custom Contract Bridging]: ../custom-contracts-bridging/README.md -[how to initiate a trustless bridge transaction]: ../hello-bridge/README.md -[how to customize our trustless bridging logic]: ../custom-contracts-bridging/README.md -[Hello Bridge]: ../hello-bridge/README.md -[setup]: ../hello-bridge/README.md#1-setup -[Custom Contracts Bridging]: ../custom-contracts-bridging/README.md#33-update-environment-with-your-asc-contract-address -[ASC Gitbook]: https://docs.attestcoin.org/ -[Loan Flow]: ../loan-flow/README.md diff --git a/bridge/.env.example b/bridge/.env.example new file mode 100644 index 00000000..0396a48c --- /dev/null +++ b/bridge/.env.example @@ -0,0 +1,21 @@ +# Bridge example environment +# Convenience split from root `.env` โ€” use the same CC3 Testnet values. + +## Pre-deployed / network defaults (hello-bridge) +# Source chain key for Sepolia on CC3 Testnet (different from its chain ID, which is 11155111) +SOURCE_CHAIN_KEY=1 +PROOF_BUILDER_URL="https://prover.cc3-testnet.creditcoin.network" +SOURCE_CHAIN_CONTRACT_ADDRESS="0x0F24FD9e0524BA53d3f0A4A40350Adf5370b4A53" +ASC_MINTER_CONTRACT_ADDRESS="0x2Be9B8640ED32815d3B9e8C92AbcD3F15F07396f" +ASC_MINTABLE_TOKEN="0x914Cf96BF28b7b4921db27b264ecEd71aC91134E" # betterleaks:allow +CREDITCOIN_RPC_URL="https://rpc.cc3-testnet.creditcoin.network" +EVM_V1_DECODER_LIBRARY_ADDRESS="0x04B9ae8562D8Cc5bbbBbBB759080dDC30B56D18B" + +## User-provided +SOURCE_CHAIN_RPC_URL="" +CREDITCOIN_WALLET_PRIVATE_KEY="" + +## User-deployed (custom-contracts-bridging + offchain worker) +SOURCE_CHAIN_CUSTOM_CONTRACT_ADDRESS="" +ASC_CUSTOM_MINTER_CONTRACT_ADDRESS="" +ASC_CUSTOM_MINTABLE_TOKEN="" diff --git a/bridge/README.md b/bridge/README.md new file mode 100644 index 00000000..b005b801 --- /dev/null +++ b/bridge/README.md @@ -0,0 +1,11 @@ +# Bridge examples (simplified ASC bridge) + +These tutorials implement a simplified one-way bridge using Attestcoin readability. Tutorials implementing a full two-way bridge using both readability and writability will be added soon, after the writability core code passes 3rd party audit. + +## Tutorials in This Directory (recommended order) + +1. [Hello Bridge](./hello-bridge/README.md) +2. [Custom Contracts Bridging](./custom-contracts-bridging/README.md) +3. [Bridge Offchain Worker](./bridge-offchain-worker/README.md) + +Deployment troubleshooting: [contracts/CONTRIBUTOR_NOTES.md](./contracts/CONTRIBUTOR_NOTES.md). diff --git a/bridge/bridge-offchain-worker/README.md b/bridge/bridge-offchain-worker/README.md new file mode 100644 index 00000000..a3b92d78 --- /dev/null +++ b/bridge/bridge-offchain-worker/README.md @@ -0,0 +1,33 @@ +# Bridge Offchain Worker + +Optional UX automation for the custom bridge: users only burn on Sepolia; the worker finishes the mint on Creditcoin so they do not run `yarn custom_bridge:submit_query` by hand. It is not required for security โ€” anyone with a valid proof can still call `ASCMinter.execute` manually, but the ASC still only mints for source-chain emitters registered via `wrapOriginToken`. + +**Flow:** poll Sepolia for `TokensBurnedForBridging` โ†’ wait for attestation and fetch a USC proof from the Proof Builder โ†’ submit that proof via `ASCMinter.execute` on CC3 Testnet โ†’ log `TokensMinted` on Creditcoin (requires the burn contract to already be registered with `wrapOriginToken`). Same burn โ†’ proof โ†’ mint path as the tutorial, without a write-ability Relayer. + +> [!NOTE] +> **Start here:** [Custom Contract Bridging ยง7 โ€” Automate proof submission](../custom-contracts-bridging/README.md#7-automate-proof-submission-optional) for the integrated tutorial step. + +Implementation: `worker.ts`. + +## Quick start + +Prerequisites: complete [Custom Contract Bridging](../custom-contracts-bridging/README.md) (deployed `ASCMinter`, wrapped token, and `.env` vars). + +```sh +source bridge/.env +yarn offchain:start_worker +``` + +Burn on Sepolia in another terminal; the worker handles attestation, proof fetch, and mint. + +## Required env vars + +- `SOURCE_CHAIN_RPC_URL`, `SOURCE_CHAIN_CUSTOM_CONTRACT_ADDRESS` +- `ASC_CUSTOM_MINTER_CONTRACT_ADDRESS`, `ASC_CUSTOM_MINTABLE_TOKEN` +- `CREDITCOIN_RPC_URL`, `CREDITCOIN_WALLET_PRIVATE_KEY`, `SOURCE_CHAIN_KEY`, `PROOF_BUILDER_URL` + +## Next + +[Loan Flow](../../loan/README.md) โ€” cross-chain loan state with a similar worker pattern. + +[Custom Contract Bridging]: ../custom-contracts-bridging/README.md diff --git a/bridge-offchain-worker/worker.ts b/bridge/bridge-offchain-worker/worker.ts similarity index 97% rename from bridge-offchain-worker/worker.ts rename to bridge/bridge-offchain-worker/worker.ts index 75f5d09d..688b2aa8 100644 --- a/bridge-offchain-worker/worker.ts +++ b/bridge/bridge-offchain-worker/worker.ts @@ -1,7 +1,6 @@ -import dotenv from 'dotenv'; import { Contract, ethers } from 'ethers'; -import burnerAbi from '../contracts/abi/TestERC20.json'; +import burnerAbi from '../../shared/contracts/abi/TestERC20.json'; import ASCMinterABI from '../contracts/abi/ASCMinter.json'; import { generateProofFor, @@ -12,9 +11,10 @@ import { POLLING_INTERVAL_MS, isValidContractAddress, isValidPrivateKey, -} from '../utils'; +} from '../../shared/utils'; +import { loadEnv } from '../../shared/env'; -dotenv.config({ override: true }); +loadEnv('bridge'); // Graceful shutdown flag let isShuttingDown = false; diff --git a/bridge/contracts/ABI_GENERATION.md b/bridge/contracts/ABI_GENERATION.md new file mode 100644 index 00000000..2cf83ef2 --- /dev/null +++ b/bridge/contracts/ABI_GENERATION.md @@ -0,0 +1,3 @@ +# ABI generation + +Moved to [CONTRIBUTOR_NOTES.md](./CONTRIBUTOR_NOTES.md). diff --git a/bridge/contracts/CONTRIBUTOR_NOTES.md b/bridge/contracts/CONTRIBUTOR_NOTES.md new file mode 100644 index 00000000..16d1ae6b --- /dev/null +++ b/bridge/contracts/CONTRIBUTOR_NOTES.md @@ -0,0 +1,52 @@ +# Bridge contracts โ€” contributor notes + +Tutorial users can skip this file. It covers ABI regeneration and deployment troubleshooting for maintainers. + +## Regenerate ABIs + +After changing contracts under `bridge/contracts/sol/`: + +```sh +yarn +./bridge/contracts/abi-creator.sh +``` + +For loan contracts: `./loan/contracts/abi-creator.sh`. + +Bridge example contracts (`ASCMinter`, tokens) inherit shared readability `ASCBase` from `@gluwa/asc-contracts` (`contracts/readability/ASCBase.sol`). `EvmV1Decoder` comes from the same package (`contracts/common/`). + +`ASCMinter` mints only when the burn logโ€™s emitting contract is registered via `wrapOriginToken` (`wrappedTokens`). Regenerate ABIs after changing that surface. + +## Deploy troubleshooting + +Common `forge create` issues when deploying `ASCMinter` with a linked `EvmV1Decoder` library: + +### Nonce / "already known" + +- Wait 10โ€“30s and retry `ASCMinter` deploy (decoder address unchanged). +- Or raise gas: `--gas-price `. +- Or set explicit nonce: `cast nonce
--rpc-url $CREDITCOIN_RPC_URL` then `--nonce`. + +### Insufficient funds + +Fund the deployer on Creditcoin testnet (Discord faucet). Check balance: + +```bash +cast balance --rpc-url $CREDITCOIN_RPC_URL +``` + +### Hangs on confirmation + +`forge create` waits for inclusion; slow RPCs can take 1โ€“5 minutes. Check the explorer if unsure. + +### Library link errors + +Ensure `--libraries` uses the deployed decoder address: + +```bash +--libraries node_modules/@gluwa/asc-contracts/contracts/common/EvmV1Decoder.sol:EvmV1Decoder: +``` + +The library is published in npm at `contracts/common/EvmV1Decoder.sol`. Tutorials and Solidity imports both use that path via `@gluwa/asc-contracts` from `node_modules`. + +After deploy, submit proofs with `yarn hello_bridge:submit_query` or `yarn custom_bridge:submit_query`. diff --git a/bridge/contracts/DEPLOYMENT.md b/bridge/contracts/DEPLOYMENT.md new file mode 100644 index 00000000..468ea5be --- /dev/null +++ b/bridge/contracts/DEPLOYMENT.md @@ -0,0 +1,5 @@ +# Deployment troubleshooting + +Moved to [CONTRIBUTOR_NOTES.md](./CONTRIBUTOR_NOTES.md). + +Tutorial deploy steps live in [custom-contracts-bridging/README.md](../custom-contracts-bridging/README.md). diff --git a/bridge/contracts/abi-creator.sh b/bridge/contracts/abi-creator.sh new file mode 100755 index 00000000..99d17f93 --- /dev/null +++ b/bridge/contracts/abi-creator.sh @@ -0,0 +1,30 @@ +#!/bin/bash + +# WARNING: execute this from the repository root. +set -euo pipefail + +sol_directory="bridge/contracts/sol" +abi_directory="bridge/contracts/abi" + +extract_abi() { + local path="$1" + local name="$2" + jq -c --arg path "$path" --arg name "$name" ' + (.contracts // {}) as $c + | ($c[$path + ":" + $name].abi + // (($c | to_entries | map(select(.key | endswith(":" + $name))) | .[0].value.abi) // null) + // {}) + ' +} + +for p in "$sol_directory"/*.sol; do + file=$(basename "$p") + contract_name="${file%.sol}" + file_with_extension="$contract_name.json" + solc --base-path . --include-path "node_modules" \ + "@openzeppelin/=node_modules/@openzeppelin/" \ + "@gluwa/asc-contracts/=node_modules/@gluwa/asc-contracts/" \ + "$p" \ + --combined-json abi --overwrite --json-indent 2 | \ + extract_abi "$p" "$contract_name" | jq --indent 2 . > "$abi_directory/$file_with_extension" +done diff --git a/contracts/abi/ASCMinter.json b/bridge/contracts/abi/ASCMinter.json similarity index 90% rename from contracts/abi/ASCMinter.json rename to bridge/contracts/abi/ASCMinter.json index 33dd2d9c..a6c70bd6 100644 --- a/contracts/abi/ASCMinter.json +++ b/bridge/contracts/abi/ASCMinter.json @@ -10,6 +10,25 @@ "name": "InvalidAction", "type": "error" }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "emitter", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "wrappedToken", + "type": "address" + } + ], + "name": "EmitterRegistered", + "type": "event" + }, { "anonymous": false, "inputs": [ diff --git a/contracts/abi/BridgeTestToken.json b/bridge/contracts/abi/BridgeTestToken.json similarity index 100% rename from contracts/abi/BridgeTestToken.json rename to bridge/contracts/abi/BridgeTestToken.json diff --git a/contracts/abi/MintableToken.json b/bridge/contracts/abi/MintableToken.json similarity index 100% rename from contracts/abi/MintableToken.json rename to bridge/contracts/abi/MintableToken.json diff --git a/contracts/sol/ASCMinter.sol b/bridge/contracts/sol/ASCMinter.sol similarity index 70% rename from contracts/sol/ASCMinter.sol rename to bridge/contracts/sol/ASCMinter.sol index 354e6b81..c23ddb17 100644 --- a/contracts/sol/ASCMinter.sol +++ b/bridge/contracts/sol/ASCMinter.sol @@ -2,12 +2,17 @@ pragma solidity ^0.8.23; import {ASCMintableToken, ASC_MINTER} from "./MintableToken.sol"; -import {ASCBase} from "./ASCBase.sol"; -import {EvmV1Decoder} from "@gluwa/usc-contracts/contracts/decoding/EvmV1Decoder.sol"; - +import {ASCBase} from "@gluwa/asc-contracts/contracts/readability/ASCBase.sol"; +import {EvmV1Decoder} from "@gluwa/asc-contracts/contracts/common/EvmV1Decoder.sol"; + +/** + * @title ASCMinter + * @notice Simplified ASC that mints wrapped tokens after a proved source-chain burn. + * @dev Mints only when the burn log's emitting contract is registered via {wrapOriginToken}. + */ contract ASCMinter is ASCBase { enum MinterActions { - Mint // 0 + Mint // 0 โ€” matches pre-deployed stock minter used by hello-bridge } error InvalidAction(uint8 action); @@ -16,9 +21,16 @@ contract ASCMinter is ASCBase { 0x17dc4d6f69d484e59be774c29b47d2fa4c14af2e01df42fc5643ac968f4d427e; event TokensMinted(address indexed wrappedTokenAddress, address indexed burntFrom, uint256 amount, bytes32 indexed queryId); + event EmitterRegistered(address indexed emitter, address indexed wrappedToken); + /// @notice Source-chain burn emitter โ†’ wrapped ASCMintableToken on Creditcoin. mapping(address => address) public wrappedTokens; + /** + * @notice Register a source-chain burn emitter and map it to a Creditcoin wrapped token. + * @param originToken Source-chain ERC20 that emits `TokensBurnedForBridging` (the emitter). + * @param targetToken Creditcoin ASCMintableToken that this ASC may mint. + */ function wrapOriginToken(address originToken, address targetToken) external { require(originToken != address(0), "Origin token cannot be the zero address"); require(targetToken != address(0), "Target token cannot be the zero address"); @@ -27,6 +39,8 @@ contract ASCMinter is ASCBase { require(ASCMintableToken(targetToken).hasRole(ASC_MINTER, address(this)), "Target token must be ASCMintableToken and support AccessControl"); wrappedTokens[originToken] = targetToken; + + emit EmitterRegistered(originToken, targetToken); } function _processAndEmitEvent(uint8 action, bytes32 queryId, bytes memory encodedTransaction) internal override { @@ -50,15 +64,10 @@ contract ASCMinter is ASCBase { EvmV1Decoder.getLogsByEventSignature(receipt, BURN_EVENT_SIGNATURE); require(burnLogs.length > 0, "No burn events found"); - // Check if the burn is valid - (address originTokenAddress, address burntFrom, uint256 burntValue) = _processBurnLogs(burnLogs); - - address wrappedTokenAddress = wrappedTokens[originTokenAddress]; + (address emitter, address burntFrom, uint256 burntValue) = _processBurnLogs(burnLogs); - require( - wrappedTokenAddress != address(0), - "Origin token not registered in wrapped tokens!" - ); + address wrappedTokenAddress = wrappedTokens[emitter]; + require(wrappedTokenAddress != address(0), "No wrapped token for emitter"); ASCMintableToken(wrappedTokenAddress).mint(burntFrom, burntValue); @@ -68,7 +77,7 @@ contract ASCMinter is ASCBase { function _processBurnLogs(EvmV1Decoder.LogEntry[] memory burnLogs) internal pure - returns (address originTokenAddress, address from, uint256 value) + returns (address emitter, address from, uint256 value) { // For this demonstration we only process the first burn log found within a transaction. // We only expect a single burn log per transaction in this demo anyways @@ -78,13 +87,14 @@ contract ASCMinter is ASCBase { require(log.topics.length == 2, "Invalid TokensBurnedForBridging topics"); require(log.topics[0] == BURN_EVENT_SIGNATURE, "Not TokensBurnedForBridging event"); - originTokenAddress = log.address_; + // Log `address` is the source-chain contract that emitted the burn (the emitter). + emitter = log.address_; from = address(uint160(uint256(log.topics[1]))); // data is a single uint256 (32 bytes) require(log.data.length == 32, "Not burn event: data len"); value = abi.decode(log.data, (uint256)); - return (originTokenAddress, from, value); + return (emitter, from, value); } } diff --git a/contracts/sol/BridgeTestToken.sol b/bridge/contracts/sol/BridgeTestToken.sol similarity index 100% rename from contracts/sol/BridgeTestToken.sol rename to bridge/contracts/sol/BridgeTestToken.sol diff --git a/contracts/sol/MintableToken.sol b/bridge/contracts/sol/MintableToken.sol similarity index 100% rename from contracts/sol/MintableToken.sol rename to bridge/contracts/sol/MintableToken.sol diff --git a/custom-contracts-bridging/README.md b/bridge/custom-contracts-bridging/README.md similarity index 57% rename from custom-contracts-bridging/README.md rename to bridge/custom-contracts-bridging/README.md index d1907882..c4edcfe8 100644 --- a/custom-contracts-bridging/README.md +++ b/bridge/custom-contracts-bridging/README.md @@ -1,5 +1,8 @@ # Custom Contract Bridging +> [!NOTE] +> This tutorial implements a simplified one-way bridge using Attestcoin readability. Tutorials implementing a full two-way bridge using both readability and writability will be added soon, after the writability core code passes 3rd party audit. + > [!TIP] > This tutorial builds on the previous [Hello Bridge] example -make sure to check it out before > moving on! @@ -30,7 +33,7 @@ address with 1,000,000 `TEST` coins, so we won't have to mint `TEST` tokens manu Make sure to first load your `.env` file with: ```sh -source .env +source bridge/.env ``` After, run the following command to deploy the contract: @@ -40,7 +43,7 @@ forge create \ --broadcast \ --rpc-url $SOURCE_CHAIN_RPC_URL \ --private-key $CREDITCOIN_WALLET_PRIVATE_KEY \ - contracts/sol/TestERC20.sol:TestERC20 + shared/contracts/sol/TestERC20.sol:TestERC20 ``` This should display some output containing the address of your test `ERC20` contract: @@ -53,7 +56,7 @@ Transaction hash: 0xfb0aaf396684bf0727019e5271d7e0dedee1dea9e5a4a1ef7456662d0ac0 Save the contract address shown in `Deployed to:`. You will be needing it in the next step. -Additionally update the `.env` file at the root of the repository with the address, like so: +Additionally update the `.env` file at `bridge/.env` with the address, like so: ```env SOURCE_CHAIN_CUSTOM_CONTRACT_ADDRESS= @@ -62,82 +65,52 @@ SOURCE_CHAIN_CUSTOM_CONTRACT_ADDRESS= Once again, reload your `.env` file with: ```sh -source .env +source bridge/.env ``` -## 3. Deploy Your Own Custom Bridging Contract - -In the next two steps we will be deploying our own bridging contract called `ASCMinter.sol` - -Attestcoin smart contracts (ASCs) such as `ASCMinter` are intended to be deployed by DApp -builders. Here, our ASC is used only for bridging tokens. A ASC exposes functions which -internally make use of the Creditcoin Oracle to verify cross-chain data. It then interprets -those data and uses them to trigger DApp business logic. +## 3. Deploy your bridging stack -For instance, our `ASCMinter` looks for fields like `from`, `to`, and `amount` in the -cross-chain data we submit to it. With those fields, the contract can verify whether or not a -token burn took place, how many tokens it needs to mint on Creditcoin, and which address it -should mint them to. +You will deploy the stock `ASCMinter` from this repo โ€” **no contract edits required** for the default path. -### 3.1 Modify The Bridge Smart Contract - -As an exercise, we will be modifying our `ASCMinter` so that it mints _twice_ the amount -of tokens which were burned on our _source chain_. +### 3.1 Deploy `EvmV1Decoder` (once per Creditcoin network) > [!NOTE] -> This is for demonstration purposes only, as bridging this way dilutes the value of our `TEST` -> token each time we bridge it. - -Start by opening the file `contracts/sol/ASCMinter.sol`. Next, navigate to the following line -inside of the `_processMint` function: - -```sol -ASCMintableToken(wrappedTokenAddress).mint(burntFrom, burntValue); -``` - -Update it so that your `ASCMinter` contract mints twice the `burntValue` -of tokens it should on Creditcoin. The resulting line should look something like: - -```sol -ASCMintableToken(wrappedTokenAddress).mint(burntFrom, burntValue * 2); -``` - -### 3.2 Deploy Your Decoder Library and Modified Contract - -First we need to deploy our `EvmV1Decoder` library so that we can reference it in our -`ASCMinter`. We do so like this: +> This contract should be pre-deployed and already stored under EVM_V1_DECODER_LIBRARY_ADDRESS in `bridge/.env.example`. So it should already be present in your `bridge/.env` as well. We include how to deploy a new one in case the existing decoder isn't available for your tutorial run. ```bash forge create \ --broadcast \ --rpc-url $CREDITCOIN_RPC_URL \ --private-key $CREDITCOIN_WALLET_PRIVATE_KEY \ - node_modules/@gluwa/usc-contracts/contracts/decoding/EvmV1Decoder.sol:EvmV1Decoder + node_modules/@gluwa/asc-contracts/contracts/common/EvmV1Decoder.sol:EvmV1Decoder ``` -You should get some output with the address of the library you just deployed: +Add the library address to your `.env` file at `bridge/.env`: -```bash -Deployer: 0x20dB67795C2AEb4De075986b0D4217A109FEF2B5 -Deployed to: 0x128A6492F875Bd92C07D7F0050fc5c265dbc849B -Transaction hash: 0x04e524d4578851b06bd2196710b0c9890fff6bf29d40999c5fb02dab0c428fca +```env +EVM_V1_DECODER_LIBRARY_ADDRESS= +``` + +Reload: + +```sh +source bridge/.env ``` -Use the contract address shown in `Deployed to:` to deploy your `ASCMinter` using the following command: +### 3.2 Deploy `ASCMinter` ```bash forge create \ --broadcast \ --rpc-url $CREDITCOIN_RPC_URL \ --private-key $CREDITCOIN_WALLET_PRIVATE_KEY \ - --libraries node_modules/@gluwa/usc-contracts/contracts/decoding/EvmV1Decoder.sol:EvmV1Decoder: \ - contracts/sol/ASCMinter.sol:ASCMinter + --libraries node_modules/@gluwa/asc-contracts/contracts/common/EvmV1Decoder.sol:EvmV1Decoder:$EVM_V1_DECODER_LIBRARY_ADDRESS \ + bridge/contracts/sol/ASCMinter.sol:ASCMinter ``` -> [!IMPORTANT] -> Don't forget to replace `` with the address of your deployed decoder contract! +If deployment fails (nonce, gas, etc.), see [Contributor deploy notes](../contracts/CONTRIBUTOR_NOTES.md). -You should get some output with the address of the contract you just deployed: +You should get output with the contract address: ```bash Deployer: 0x20dB67795C2AEb4De075986b0D4217A109FEF2B5 @@ -145,62 +118,44 @@ Deployed to: 0xCDf3e9eC93015a1B3047d087296C1cE096f33f74 Transaction hash: 0xe86e3c2f77fd050a4120dbd195668af2f3d94f3a41b5db21643c53c1ac3cc212 ``` -If you have issues with deployment during this step, see the [Deployment Troubleshooting Guide] - ### 3.3 Update environment with your ASC contract address -Save the address of the contract. And modify the following entry in the `.env` file found at the root of the -repository: +Add the address to your `.env` file at `bridge/.env`: ```env ASC_CUSTOM_MINTER_CONTRACT_ADDRESS= ``` -Once again, reload your `.env` file with: +Reload: ```sh -source .env +source bridge/.env ``` -### 3.4 Deploy Minter ERC20 Contract and register with ASC Minter +### 3.4 Deploy wrapped token and register the source emitter -Now that we've deployed our ASC which triggers the minting action, we need to connect the ERC20 contract in -which we will mint wrapped tokens! +Deploy the wrapped ERC20 owned by your minter: ```bash forge create \ --broadcast \ --rpc-url $CREDITCOIN_RPC_URL \ --private-key $CREDITCOIN_WALLET_PRIVATE_KEY \ - contracts/sol/BridgeTestToken.sol:BridgeTestToken \ + bridge/contracts/sol/BridgeTestToken.sol:BridgeTestToken \ --constructor-args "$ASC_CUSTOM_MINTER_CONTRACT_ADDRESS" ``` -You should get some output with the address of the ERC20 token you just deployed: - -```bash -Deployer: 0x20dB67795C2AEb4De075986b0D4217A109FEF2B5 -Deployed to: 0xD1A5c57654636146417B589aED99C56b9c73C510 -Transaction hash: 0x07f87a00b117f9d16c5a20a461d00cbce87aa76994af727a02d73da3aca622f1 -``` - -Modify the following entry in the `.env` file found at the root of the -repository: +Add the address to your `.env` file at `bridge/.env`: ```env ASC_CUSTOM_MINTABLE_TOKEN= ``` -Once again, reload your `.env` file with: - -```sh -source .env -``` - -Our last step is to register the ERC20 token as the wrapped version of the source chain token we intend -to bridge. +Register the Sepolia burn contract as an allowed emitter and map it to the wrapped token. +`ASCMinter` mints **only** when the proved burn logโ€™s emitting address was registered with `wrapOriginToken` โ€” a valid proof from any other contract is rejected: ```bash +source bridge/.env cast send \ --rpc-url $CREDITCOIN_RPC_URL \ $ASC_CUSTOM_MINTER_CONTRACT_ADDRESS \ @@ -208,6 +163,12 @@ cast send \ --private-key $CREDITCOIN_WALLET_PRIVATE_KEY ``` +Optional preflight before burning: + +```sh +yarn utils:check_setup bridge +``` + ## 4. Burning the tokens you want to bridge The following few steps are similar to what we did in the [Hello Bridge] example. Start by burning @@ -275,37 +236,52 @@ WALLET_ADDRESS=$(cast wallet address --private-key $CREDITCOIN_WALLET_PRIVATE_KE yarn utils:check_balance $ASC_CUSTOM_MINTABLE_TOKEN $WALLET_ADDRESS ``` -This will return your balance in whole (BTKT) token units. - -Notice how you now have _twice_ the amount of tokens you originally burned on Sepolia! +This will return your balance in whole (BTKT) token units. With the default `ASCMinter`, the minted amount should **match** the amount you burned on Sepolia. It should show something like this: ```bash ๐Ÿ”— Using RPC URL: https://rpc.cc3-testnet.creditcoin.network ๐Ÿ“ฆ Token: Bridge Test Token (BTKT) -๐Ÿงพ Raw Balance: 100000000000000000000 -๐Ÿ’ฐ Formatted Balance: 100.0 BTKT +๐Ÿงพ Raw Balance: 50000000000000000000 +๐Ÿ’ฐ Formatted Balance: 50.0 BTKT Decimals for token micro unit: 18 ``` -## Conclusion +## 7. Automate proof submission (optional) + +The same burn โ†’ proof โ†’ `execute` flow can run automatically so users only sign the **burn** on Sepolia. Start the worker (requires steps 2โ€“3 complete): -Congratulations! You've set up your first custom smart contracts which make use of the Creditcoin -Decentralized Oracle! +```sh +yarn offchain:start_worker +``` + +In another terminal, burn tokens โ€” the worker submits the mint proof when attestation completes: + +```sh +source bridge/.env +cast send --rpc-url $SOURCE_CHAIN_RPC_URL \ + $SOURCE_CHAIN_CUSTOM_CONTRACT_ADDRESS \ + "burn(uint256)" 2000 \ + --private-key $CREDITCOIN_WALLET_PRIVATE_KEY +``` + +The worker is **UX-only** โ€” anyone can still call `execute` manually with `yarn custom_bridge:submit_query`. Full worker walkthrough: [bridge-offchain-worker/README.md](../bridge-offchain-worker/README.md). + +## Optional challenge: mint 2ร— burned amount + +To experiment with customizing ASC logic, edit `bridge/contracts/sol/ASCMinter.sol` so `_processMint` mints `burntValue * 2`, redeploy the minter (reusing the same `EVM_V1_DECODER_LIBRARY_ADDRESS`), and repeat the burn + submit flow. This dilutes token supply and is for learning only. + +## Conclusion -The next tutorial: [Bridge Offchain Worker], will take another important step towards developing a mature, production ready -cross-chain DApp. That step is automation! We automate using an **offchain worker** which submits -queries automatically. This _vastly_ improves UX by making it so the -end user only has to sign a _single_ transaction to initiate the bridging procedure. +Congratulations! You've deployed your own simplified ASC bridge (`ASCMinter` + wrapped token), registered the Sepolia burn emitter, and completed a trustless mint. -In practice, DApp builders will want to conduct all cross-chain queries via an offchain worker in -order to ensure robustness and streamline UX. +Next: [Loan Flow](../../loan/README.md) reuses your decoder library and extends the pattern with cross-chain loan state. [Hello Bridge]: ../hello-bridge/README.md [setup]: ../hello-bridge/README.md#1-setup [step 2]: #2-deploy-a-test-erc20-contract-on-sepolia -[step 3.2]: #32-deploy-your-decoder-library-and-modified-contract +[step 3.2]: #32-deploy-ascminter [step 5]: #5-submit-a-mint-query-to-the-asc-contract -[Deployment Troubleshooting Guide]: ../contracts/DEPLOY.md +[Contributor deploy notes]: ../contracts/CONTRIBUTOR_NOTES.md [Bridge Offchain Worker]: ../bridge-offchain-worker/README.md diff --git a/bridge/custom-contracts-bridging/submit_query.ts b/bridge/custom-contracts-bridging/submit_query.ts new file mode 100644 index 00000000..22402d15 --- /dev/null +++ b/bridge/custom-contracts-bridging/submit_query.ts @@ -0,0 +1,20 @@ +import { printBridgeSubmitUsage, runBridgeSubmitQuery } from '../../shared/submit_bridge_query'; + +async function main() { + const config = { + usageCommand: 'yarn custom_bridge:submit_query', + exampleTxHash: '0x87c97c776a678941b5941ec0cb602a4467ff4a35f77264208575f137cb05b2a7', + minterAddressEnvVar: 'ASC_CUSTOM_MINTER_CONTRACT_ADDRESS' as const, + }; + + const args = process.argv.slice(2); + if (args.length !== 1) { + printBridgeSubmitUsage(config); + process.exit(1); + } + + await runBridgeSubmitQuery(config, args[0]); + process.exit(0); +} + +main().catch(console.error); diff --git a/bridge/foundry.toml b/bridge/foundry.toml new file mode 100644 index 00000000..4a149a39 --- /dev/null +++ b/bridge/foundry.toml @@ -0,0 +1,24 @@ +[profile.default] +out = "out" +src = "contracts/sol" +test = "test" +libs = ["../node_modules", "../lib"] +# Resolve @gluwa/asc-contracts from yarn/npm (readability ASCBase + common/EvmV1Decoder). +# Package: https://www.npmjs.com/package/@gluwa/asc-contracts +allow_paths = ["../node_modules", "../lib"] +remappings = [ + "@openzeppelin/=../node_modules/@openzeppelin/", + "@gluwa/asc-contracts/=../node_modules/@gluwa/asc-contracts/", + "forge-std/=../lib/forge-std/src/", +] +solc_version = "0.8.30" +optimizer = true +optimizer_runs = 200 +via_ir = true +evm_version = "shanghai" +verbosity = 2 + +# CI Foundry enables lint-on-build by default; remapped ../node_modules imports +# currently crash the lint engine after a successful compile. +[lint] +lint_on_build = false diff --git a/hello-bridge/README.md b/bridge/hello-bridge/README.md similarity index 88% rename from hello-bridge/README.md rename to bridge/hello-bridge/README.md index 1bfc68f6..a8fba585 100644 --- a/hello-bridge/README.md +++ b/bridge/hello-bridge/README.md @@ -1,13 +1,30 @@ # Hello Bridge +> [!NOTE] +> This tutorial implements a simplified one-way bridge using Attestcoin readability. Tutorials implementing a full two-way bridge using both readability and writability will be added soon, after the writability core code passes 3rd party audit. + This tutorial introduces you to one of the most common uses for a cross chain oracle, **cross chain -bridging!** Cross-chain bridging on Creditcoin can be broken down into three broad steps: +bridging!** The flow is: + +1. **Burn** tokens on the source chain (Sepolia). +2. **Submit proof + mint** on Creditcoin โ€” steps 2 and 3 are combined in `yarn hello_bridge:submit_query` (proof generation, attestation wait, and `ASCMinter.execute`). The pre-deployed ASC already has the Sepolia burner registered via `wrapOriginToken`; only burns from that contract mint. + +## Pre-deployed contracts (no deploy step) + +Hello Bridge uses **shared tutorial contracts** already deployed on CC3 Testnet and Sepolia. The /bridge [`.env.example`](../.env.example) includes pre-filled values for several contracts. Just copy the example env using: -1. To begin, the `ERC20` tokens to bridge are burned using a smart contract on our _source chain_ - (in this case, Sepolia). -2. Then, we generate merkle and continuity proofs corresponding to our source chain token burn -3. Using the proofs we generated, we call our minter Attestcoin smart contract (ASC) which will internally call the Creditcoin oracle's native proof verifier -4. After that the same contract will mint the tokens on Creditcoin +```sh +cp bridge/.env.example bridge/.env +``` + +Then you only need to add **`CREDITCOIN_WALLET_PRIVATE_KEY`** and **`SOURCE_CHAIN_RPC_URL`** (Infura Sepolia), then run: + +```sh +source bridge/.env +yarn utils:check_setup hello +``` + +If all checks pass, continue with [Setup](#1-setup) below. ## 0. Install @@ -51,14 +68,6 @@ Address: 0xBE7959cA1b19e159D8C0649860793dDcd125a2D5 Private key: 0xb9c179ed56514accb60c23a862194fa2a6db8bdeb815d16e2c21aa4d7dc2845d # betterleaks:allow ``` -Save this private key on the `.env` file in the root of the repository. If you have not created -it yet, copy it from the tracked template first โ€” `.env` is git-ignored, so your key stays out -of version control: - -```bash -cp .env.example .env -``` - ```env CREDITCOIN_WALLET_PRIVATE_KEY= ``` @@ -66,7 +75,7 @@ CREDITCOIN_WALLET_PRIVATE_KEY= And load into your terminal session with: ```sh -source .env +source bridge/.env ``` ### 1.2 Get some test funds (`Sepolia`) @@ -89,7 +98,7 @@ account address from [step 1.1]: ``` Note, that currently the faucet yields 100 test CTC every 24 hours. That is far more than these -tutorials need: submitting an oracle query costs only gas, on the order of 0.0002 CTC per query. +tutorials need: submitting an oracle query costs only gas, on the order of 0.000197 CTC per query. Now that your wallet is ready to make transactions on both networks, you will be needing a way to interact with it from the command line. @@ -112,7 +121,7 @@ SOURCE_CHAIN_RPC_URL="https://sepolia.infura.io/v3/" And load into your terminal session with: ```sh -source .env +source bridge/.env ``` ## 2. Minting some tokens on Sepolia diff --git a/bridge/hello-bridge/submit_query.ts b/bridge/hello-bridge/submit_query.ts new file mode 100644 index 00000000..9e4c4f08 --- /dev/null +++ b/bridge/hello-bridge/submit_query.ts @@ -0,0 +1,20 @@ +import { printBridgeSubmitUsage, runBridgeSubmitQuery } from '../../shared/submit_bridge_query'; + +async function main() { + const config = { + usageCommand: 'yarn hello_bridge:submit_query', + exampleTxHash: '0x87c97c776a678941b5941ec0cb602a4467ff4a35f77264208575f137cb05b2a7', + minterAddressEnvVar: 'ASC_MINTER_CONTRACT_ADDRESS' as const, + }; + + const args = process.argv.slice(2); + if (args.length !== 1) { + printBridgeSubmitUsage(config); + process.exit(1); + } + + await runBridgeSubmitQuery(config, args[0]); + process.exit(0); +} + +main().catch(console.error); diff --git a/bridge/test/ASCMinterBurnLog.t.sol b/bridge/test/ASCMinterBurnLog.t.sol new file mode 100644 index 00000000..a16a35cf --- /dev/null +++ b/bridge/test/ASCMinterBurnLog.t.sol @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import {Test} from "forge-std/Test.sol"; +import {ASCMinterHarness} from "./harness/ASCMinterHarness.sol"; +import {EvmV1Decoder} from "@gluwa/asc-contracts/contracts/common/EvmV1Decoder.sol"; + +contract ASCMinterBurnLogTest is Test { + ASCMinterHarness internal minter; + + bytes32 internal constant BURN_EVENT_SIGNATURE = + 0x17dc4d6f69d484e59be774c29b47d2fa4c14af2e01df42fc5643ac968f4d427e; + + function setUp() public { + minter = new ASCMinterHarness(); + } + + function testBurnLog_decodesRecipientAndAmount() public view { + address token = address(0x1111); + address burner = address(0x2222); + uint256 amount = 50 ether; + + (address origin, address from, uint256 value) = + minter.exposeProcessBurnLogs(_burnLog(token, burner, amount)); + + assertEq(origin, token); + assertEq(from, burner); + assertEq(value, amount); + } + + function testBurnLog_rejectsWrongEventSignature() public { + EvmV1Decoder.LogEntry[] memory logs = _burnLog(address(0x1), address(0x2), 1); + logs[0].topics[0] = bytes32(uint256(0xdead)); + + vm.expectRevert("Not TokensBurnedForBridging event"); + minter.exposeProcessBurnLogs(logs); + } + + function _burnLog(address token, address from, uint256 amount) + internal + pure + returns (EvmV1Decoder.LogEntry[] memory logs) + { + logs = new EvmV1Decoder.LogEntry[](1); + logs[0].address_ = token; + logs[0].topics = new bytes32[](2); + logs[0].topics[0] = BURN_EVENT_SIGNATURE; + logs[0].topics[1] = bytes32(uint256(uint160(from))); + logs[0].data = abi.encode(amount); + } +} diff --git a/bridge/test/ASCMinterSecurity.t.sol b/bridge/test/ASCMinterSecurity.t.sol new file mode 100644 index 00000000..a747212f --- /dev/null +++ b/bridge/test/ASCMinterSecurity.t.sol @@ -0,0 +1,145 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import {Test} from "forge-std/Test.sol"; +import {ASCMinterHarness} from "./harness/ASCMinterHarness.sol"; +import {BridgeTestToken} from "../contracts/sol/BridgeTestToken.sol"; +import {EvmV1Decoder} from "@gluwa/asc-contracts/contracts/common/EvmV1Decoder.sol"; +import { + INativeQueryVerifier +} from "@gluwa/asc-contracts/contracts/write-ability/common/INativeQueryVerifier.sol"; + +/// @dev Stand-in for the Creditcoin native query verifier precompile (`0xFD2`) in unit tests. +contract MockNativeQueryVerifier { + function calculateTxIndex(INativeQueryVerifier.MerkleProof calldata) external pure returns (uint64) { + return 0; + } + + function verifyAndEmit( + uint64, + uint64, + bytes calldata, + INativeQueryVerifier.MerkleProof calldata, + INativeQueryVerifier.ContinuityProof calldata + ) external pure returns (bool) { + return false; + } +} + +/// @notice Regression tests for bridge emitter registration and query replay dedupe (ASCBase). +contract ASCMinterSecurityTest is Test { + ASCMinterHarness internal minter; + + bytes32 internal constant BURN_EVENT_SIGNATURE = + 0x17dc4d6f69d484e59be774c29b47d2fa4c14af2e01df42fc5643ac968f4d427e; + + address internal constant VERIFIER_PRECOMPILE = + 0x0000000000000000000000000000000000000FD2; + + address internal originEmitter = address(0xBEEF); + address internal spoofedEmitter = address(0xBAD); + address internal burner = address(0xCAFE); + + function setUp() public { + MockNativeQueryVerifier mock = new MockNativeQueryVerifier(); + vm.etch(VERIFIER_PRECOMPILE, address(mock).code); + + minter = new ASCMinterHarness(); + } + + function testEmitterRegistration_rejectsUnregisteredEmitter() public { + bytes memory encoded = _encodedBurnTx(originEmitter, burner, 100); + + vm.expectRevert("No wrapped token for emitter"); + minter.exposeProcessMint(bytes32(0), encoded); + } + + function testEmitterRegistration_acceptsAfterWrapOriginToken() public { + BridgeTestToken wrapped = new BridgeTestToken(address(minter)); + minter.wrapOriginToken(originEmitter, address(wrapped)); + + assertEq(minter.wrappedTokens(originEmitter), address(wrapped)); + + bytes memory encoded = _encodedBurnTx(originEmitter, burner, 100); + minter.exposeProcessMint(bytes32(uint256(1)), encoded); + + assertEq(wrapped.balanceOf(burner), 100); + } + + function testEmitterRegistration_rejectsSpoofedEmitterAfterWrap() public { + BridgeTestToken wrapped = new BridgeTestToken(address(minter)); + minter.wrapOriginToken(originEmitter, address(wrapped)); + + bytes memory encoded = _encodedBurnTx(spoofedEmitter, burner, 100); + + vm.expectRevert("No wrapped token for emitter"); + minter.exposeProcessMint(bytes32(uint256(2)), encoded); + } + + function testQueryDedupe_rejectsProcessedQueryId() public { + uint64 chainKey = 1; + uint64 blockHeight = 100; + bytes32 merkleRoot = bytes32(uint256(42)); + INativeQueryVerifier.MerkleProofEntry[] memory siblings = + new INativeQueryVerifier.MerkleProofEntry[](0); + + bytes32 queryId = minter.exposeComputeQueryId(chainKey, blockHeight, merkleRoot, siblings); + minter.exposeMarkQueryProcessed(queryId); + + vm.expectRevert("Query already processed"); + minter.execute( + 0, + chainKey, + blockHeight, + "", + merkleRoot, + siblings, + bytes32(0), + new bytes32[](0) + ); + } + + function testQueryDedupe_acceptsFreshQueryId() public { + uint64 chainKey = 1; + uint64 blockHeight = 100; + bytes32 merkleRoot = bytes32(uint256(43)); + INativeQueryVerifier.MerkleProofEntry[] memory siblings = + new INativeQueryVerifier.MerkleProofEntry[](0); + + vm.expectRevert("Proof of inclusion verification failed"); + minter.execute( + 0, + chainKey, + blockHeight, + "", + merkleRoot, + siblings, + bytes32(0), + new bytes32[](0) + ); + } + + function _encodedBurnTx(address emitter, address from, uint256 amount) + internal + pure + returns (bytes memory encoded) + { + bytes32[] memory topics = new bytes32[](2); + topics[0] = BURN_EVENT_SIGNATURE; + topics[1] = bytes32(uint256(uint160(from))); + + EvmV1Decoder.LogEntryTuple[] memory logs = new EvmV1Decoder.LogEntryTuple[](1); + logs[0] = EvmV1Decoder.LogEntryTuple({ + address_: emitter, + topics: topics, + data: abi.encode(amount) + }); + + bytes[] memory chunks = new bytes[](3); + chunks[0] = abi.encode(uint64(0), uint64(21_000), address(0x1), false, address(0x2), uint256(0), bytes("")); + chunks[1] = abi.encode(uint128(1), uint256(27), bytes32(0), bytes32(0)); + chunks[2] = abi.encode(uint8(1), uint64(21_000), logs, bytes("")); + + encoded = abi.encode(uint8(0), chunks); + } +} diff --git a/bridge/test/harness/ASCMinterHarness.sol b/bridge/test/harness/ASCMinterHarness.sol new file mode 100644 index 00000000..7ee1c26b --- /dev/null +++ b/bridge/test/harness/ASCMinterHarness.sol @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import {ASCMinter} from "../../contracts/sol/ASCMinter.sol"; +import {EvmV1Decoder} from "@gluwa/asc-contracts/contracts/common/EvmV1Decoder.sol"; +import { + INativeQueryVerifier +} from "@gluwa/asc-contracts/contracts/write-ability/common/INativeQueryVerifier.sol"; + +contract ASCMinterHarness is ASCMinter { + function exposeProcessBurnLogs(EvmV1Decoder.LogEntry[] memory burnLogs) + external + pure + returns (address emitter, address from, uint256 value) + { + return _processBurnLogs(burnLogs); + } + + function exposeProcessMint(bytes32 queryId, bytes memory encodedTransaction) external { + _processMint(queryId, encodedTransaction); + } + + function exposeComputeQueryId( + uint64 chainKey, + uint64 blockHeight, + bytes32 merkleRoot, + INativeQueryVerifier.MerkleProofEntry[] calldata siblings + ) external view returns (bytes32) { + return _computeQueryId(chainKey, blockHeight, merkleRoot, siblings); + } + + function exposeMarkQueryProcessed(bytes32 queryId) external { + processedQueries[queryId] = true; + } +} diff --git a/contracts/DEPLOY.md b/contracts/DEPLOY.md deleted file mode 100644 index 6c154f05..00000000 --- a/contracts/DEPLOY.md +++ /dev/null @@ -1,161 +0,0 @@ -# Troubleshooting ASCMinter Contract Deployment - -This guide explains how to deploy the `ASCMinter` contract to Creditcoin ASC networks. The contract serves as a bridge minter that verifies cross-chain proofs and mints tokens on Creditcoin. - -## Verify Deployment - -After deployment, verify the contract was deployed correctly by checking the contract on the block explorer at https://creditcoin-testnet.blockscout.com - -Search for your contract address to see deployment details and transaction history. - -## Contract Details - -- **Contract Name**: `ASCMinter` -- **Constructor**: No parameters required -- **Mint Amount**: the amount burned on the source chain, taken from the proven burn event -- **Wrapped token**: `ASCMinter` is not itself a token. It mints through a separate - `ASCMintableToken`, registered with `wrapOriginToken`. The one used by these tutorials is - `BridgeTestToken`, name "Bridge Test Token", symbol "BTKT", 18 decimals (inherited from - OpenZeppelin ERC20). - -## Network Information - -- **RPC URL**: `https://rpc.cc3-testnet.creditcoin.network` -- **Explorer**: https://creditcoin-testnet.blockscout.com -- **Purpose**: Development and testing - -## Troubleshooting - -### Error: "already known" or "nonce already used" - -This error typically means a transaction with that nonce was already submitted to the network. This can happen when: - -- A previous transaction with the same nonce is pending -- An underfunded transaction is stuck in the mempool - -You **can** redeploy - each deployment creates a **new contract address**. - -**Solution 1: Increase gas fees** (if transaction is underfunded) - -If the transaction is stuck in the mempool due to insufficient gas fees, retry with increased gas: - -```bash -# Retry deployment with higher gas price -forge create \ - --broadcast \ - --rpc-url $CREDITCOIN_RPC_URL \ - --private-key $CREDITCOIN_WALLET_PRIVATE_KEY \ - --gas-price \ - --libraries node_modules/@gluwa/usc-contracts/contracts/decoding/EvmV1Decoder.sol:EvmV1Decoder: \ - contracts/sol/ASCMinter.sol:ASCMinter -``` - -**Solution 2: Wait and retry** (recommended) - -Usually waiting for around 10s-30s and trying to deploy again will work, if you already managed to deploy the decoder you only need to -retry redeploying the `ASCMinter` contract: - -```bash -forge create \ - --broadcast \ - --rpc-url $CREDITCOIN_RPC_URL \ - --private-key $CREDITCOIN_WALLET_PRIVATE_KEY \ - --libraries node_modules/@gluwa/usc-contracts/contracts/decoding/EvmV1Decoder.sol:EvmV1Decoder: \ - contracts/sol/ASCMinter.sol:ASCMinter -``` - -**Solution 3: Check current nonce and use next** - -```bash -# Get your current nonce -cast nonce --rpc-url $CREDITCOIN_RPC_URL - -# Deploy with the next nonce (replace X with current_nonce + 1) -forge create \ - --broadcast \ - --rpc-url $CREDITCOIN_RPC_URL \ - --private-key $CREDITCOIN_WALLET_PRIVATE_KEY \ - --nonce X \ - --libraries node_modules/@gluwa/usc-contracts/contracts/decoding/EvmV1Decoder.sol:EvmV1Decoder: \ - contracts/sol/ASCMinter.sol:ASCMinter -``` - -**Solution 4: Check if contract was already deployed** - -- Check the block explorer for your address -- Look for contract creation transactions -- Each deployment creates a unique contract address - -### Error: "insufficient funds" - -- Make sure your wallet has enough native tokens (CTC) to pay for gas fees -- Request test tokens from the [Creditcoin Discord faucet] -- Check your balance: `cast balance --rpc-url ` - -### Error: "nonce too low" - -- Wait a moment and try again -- Or manually set a higher nonce using `--nonce` flag -- Check your current nonce: `cast nonce --rpc-url ` - -### Error: Command hangs or takes too long - -`forge create` waits for transaction confirmation, which can take time on slower networks. - -**Solution:** - -- Be patient - it may take 1-5 minutes depending on network conditions -- If it hangs indefinitely, press `Ctrl+C` and check the block explorer to see if the transaction went through -- You can check transaction status manually using the block explorer - -### Error: "execution reverted" - -- Check that the contract compiled successfully: `forge build` -- Verify you're using the correct RPC URL for your target network -- Ensure your wallet has sufficient funds -- Check the contract constructor doesn't require parameters (it doesn't) - -### Error: "could not decode result data" - -This usually means the contract doesn't exist at the address or the RPC is incorrect. - -- Verify the contract address is correct -- Check you're using the right RPC URL for the network -- Ensure the contract was actually deployed (check block explorer) - -## Advanced: Manual Nonce Management - -If you're experiencing persistent nonce issues, you can manually manage nonces: - -```bash -# Get current nonce -CURRENT_NONCE=$(cast nonce --rpc-url $CREDITCOIN_RPC_URL) - -# Deploy with explicit nonce -forge create \ - --broadcast \ - --rpc-url $CREDITCOIN_RPC_URL \ - --private-key $CREDITCOIN_WALLET_PRIVATE_KEY \ - --nonce $((CURRENT_NONCE + 1)) \ - --libraries node_modules/@gluwa/usc-contracts/contracts/decoding/EvmV1Decoder.sol:EvmV1Decoder: \ - contracts/sol/ASCMinter.sol:ASCMinter -``` - -## Next Steps - -After deploying your contract: - -1. **Save the contract address** - you'll need it for all interactions -2. **Update your scripts** - replace any hardcoded contract addresses with your new address -3. **Verify the deployment** - check the block explorer to confirm the contract was deployed -4. **Submit queries** - use `yarn submit_query_2` with your contract address - -For more information on using the deployed contract, see the [README.md](../custom-contracts-bridging/README.md) for the Custom Contract Bridging example. - -## Additional Resources - -- [Foundry Documentation](https://book.getfoundry.sh/) -- [Creditcoin Documentation](https://docs.creditcoin.org/) -- [OpenZeppelin Contracts](https://docs.openzeppelin.com/contracts) - -[Creditcoin Discord faucet]: https://discord.com/channels/762302877518528522/1414985542235459707 diff --git a/contracts/abi-creator.sh b/contracts/abi-creator.sh deleted file mode 100755 index 488706ee..00000000 --- a/contracts/abi-creator.sh +++ /dev/null @@ -1,17 +0,0 @@ -#!/bin/bash - -# WARNING: execute this from the root directory of this repository ! - -sol_directory="contracts/sol" -abi_directory="contracts/abi" - -for p in "$sol_directory"/*; do - file=$(basename "$p") - contract_name="${file//.sol/}" - file_with_extension="$contract_name.json" - # Extract only the ABI from the combined JSON output - solc --base-path . --include-path "node_modules" \ - "$sol_directory/$file" \ - --combined-json abi --overwrite --json-indent 2 | \ - jq ".contracts[\"$p:$contract_name\"].abi // {}" > "$abi_directory/$file_with_extension" -done diff --git a/contracts/abi/ASCBase.json b/contracts/abi/ASCBase.json deleted file mode 100644 index 9249387e..00000000 --- a/contracts/abi/ASCBase.json +++ /dev/null @@ -1,100 +0,0 @@ -[ - { - "inputs": [], - "name": "VERIFIER", - "outputs": [ - { - "internalType": "contract INativeQueryVerifier", - "name": "", - "type": "address" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "uint8", - "name": "action", - "type": "uint8" - }, - { - "internalType": "uint64", - "name": "chainKey", - "type": "uint64" - }, - { - "internalType": "uint64", - "name": "blockHeight", - "type": "uint64" - }, - { - "internalType": "bytes", - "name": "encodedTransaction", - "type": "bytes" - }, - { - "internalType": "bytes32", - "name": "merkleRoot", - "type": "bytes32" - }, - { - "components": [ - { - "internalType": "bytes32", - "name": "hash", - "type": "bytes32" - }, - { - "internalType": "bool", - "name": "isLeft", - "type": "bool" - } - ], - "internalType": "struct INativeQueryVerifier.MerkleProofEntry[]", - "name": "siblings", - "type": "tuple[]" - }, - { - "internalType": "bytes32", - "name": "lowerEndpointDigest", - "type": "bytes32" - }, - { - "internalType": "bytes32[]", - "name": "continuityRoots", - "type": "bytes32[]" - } - ], - "name": "execute", - "outputs": [ - { - "internalType": "bool", - "name": "success", - "type": "bool" - } - ], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "bytes32", - "name": "", - "type": "bytes32" - } - ], - "name": "processedQueries", - "outputs": [ - { - "internalType": "bool", - "name": "", - "type": "bool" - } - ], - "stateMutability": "view", - "type": "function" - } -] diff --git a/contracts/abi/VerifierInterface.json b/contracts/abi/VerifierInterface.json deleted file mode 100644 index 0967ef42..00000000 --- a/contracts/abi/VerifierInterface.json +++ /dev/null @@ -1 +0,0 @@ -{} diff --git a/contracts/abi_generation.md b/contracts/abi_generation.md deleted file mode 100644 index f22dac3a..00000000 --- a/contracts/abi_generation.md +++ /dev/null @@ -1,16 +0,0 @@ -# ABI Generation - -In the case of any changes made to `TestERC20` you can replace the existing ABI with the results -generated here. - -1. From the root of the repository, first make sure that dependencies are installed: - -```sh -yarn -``` - -2. Then to generate the new ABI JSON from the root of the repository execute: - -```sh -./contracts/abi-creator.sh -``` diff --git a/contracts/sol/ASCBase.sol b/contracts/sol/ASCBase.sol deleted file mode 100644 index 487af21a..00000000 --- a/contracts/sol/ASCBase.sol +++ /dev/null @@ -1,88 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.23; - -import {INativeQueryVerifier, NativeQueryVerifierLib} from "./VerifierInterface.sol"; - -abstract contract ASCBase { - /// @notice The Native Query Verifier precompile instance - /// @dev Address: 0x0000000000000000000000000000000000000FD2 (4050 decimal) - INativeQueryVerifier public immutable VERIFIER; - - mapping(bytes32 => bool) public processedQueries; - - constructor() { - // Get the precompile instance using the helper library - VERIFIER = NativeQueryVerifierLib.getVerifier(); - } - - function _processAndEmitEvent(uint8 action, bytes32 queryId, bytes memory encodedTransaction) internal virtual; - - function execute( - uint8 action, - uint64 chainKey, - uint64 blockHeight, - bytes calldata encodedTransaction, - bytes32 merkleRoot, - INativeQueryVerifier.MerkleProofEntry[] calldata siblings, - bytes32 lowerEndpointDigest, - bytes32[] calldata continuityRoots - ) external returns (bool success) { - bytes32 queryId = _computeQueryId(chainKey, blockHeight, merkleRoot, siblings); - - require(!processedQueries[queryId], "Query already processed"); - - // First we verify the proof - bool verified = _verifyProof( - chainKey, blockHeight, encodedTransaction, merkleRoot, siblings, lowerEndpointDigest, continuityRoots - ); - require(verified, "Proof of inclusion verification failed"); - - // Mark the query as processed - processedQueries[queryId] = true; - - _processAndEmitEvent(action, queryId, encodedTransaction); - - return true; - } - - function _verifyProof( - uint64 chainKey, - uint64 blockHeight, - bytes calldata encodedTransaction, - bytes32 merkleRoot, - INativeQueryVerifier.MerkleProofEntry[] calldata siblings, - bytes32 lowerEndpointDigest, - bytes32[] calldata continuityRoots - ) internal returns (bool verified) { - INativeQueryVerifier.MerkleProof memory merkleProof = - INativeQueryVerifier.MerkleProof({root: merkleRoot, siblings: siblings}); - - INativeQueryVerifier.ContinuityProof memory continuityProof = - INativeQueryVerifier.ContinuityProof({lowerEndpointDigest: lowerEndpointDigest, roots: continuityRoots}); - - // Verify inclusion proof - verified = VERIFIER.verifyAndEmit(chainKey, blockHeight, encodedTransaction, merkleProof, continuityProof); - - return verified; - } - - function _computeQueryId( - uint64 chainKey, - uint64 blockHeight, - bytes32 merkleRoot, - INativeQueryVerifier.MerkleProofEntry[] calldata siblings - ) internal view returns (bytes32 queryId) { - INativeQueryVerifier.MerkleProof memory merkle_proof = - INativeQueryVerifier.MerkleProof({ root: merkleRoot, siblings: siblings }); - - uint256 txIndex = VERIFIER.calculateTxIndex(merkle_proof); - - assembly { - let ptr := mload(0x40) - mstore(ptr, chainKey) - mstore(add(ptr, 32), shl(192, blockHeight)) - mstore(add(ptr, 40), txIndex) - queryId := keccak256(ptr, 72) - } - } -} diff --git a/contracts/sol/VerifierInterface.sol b/contracts/sol/VerifierInterface.sol deleted file mode 100644 index 47890470..00000000 --- a/contracts/sol/VerifierInterface.sol +++ /dev/null @@ -1,39 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.23; - -interface INativeQueryVerifier { - struct MerkleProofEntry { - bytes32 hash; - bool isLeft; - } - - struct MerkleProof { - bytes32 root; - MerkleProofEntry[] siblings; - } - - struct ContinuityProof { - bytes32 lowerEndpointDigest; - bytes32[] roots; - } - - function verifyAndEmit( - uint64 chainKey, - uint64 height, - bytes calldata encodedTransaction, - MerkleProof calldata merkleProof, - ContinuityProof calldata continuityProof - ) external returns (bool); - - function calculateTxIndex( - MerkleProof calldata merkle_proof - ) external view returns (uint64); -} - -library NativeQueryVerifierLib { - address constant PRECOMPILE_ADDRESS = 0x0000000000000000000000000000000000000FD2; - - function getVerifier() internal pure returns (INativeQueryVerifier) { - return INativeQueryVerifier(PRECOMPILE_ADDRESS); - } -} diff --git a/foundry.toml b/foundry.toml index 79b45f03..853acf45 100644 --- a/foundry.toml +++ b/foundry.toml @@ -1,11 +1,19 @@ [profile.default] out = "out" -src = "contracts" - +src = "bridge/contracts/sol" libs = ["node_modules"] +# Used when forge create is run from the repo root (example CI workflows). +# via_ir required: ASCMinter + package ASCBase hit "stack too deep" without it. +remappings = [ + "@openzeppelin/=node_modules/@openzeppelin/", + "@gluwa/asc-contracts/=node_modules/@gluwa/asc-contracts/", +] solc_version = "0.8.30" optimizer = true optimizer_runs = 200 -via_ir = false +via_ir = true evm_version = "shanghai" verbosity = 2 + +[lint] +lint_on_build = false diff --git a/hello-bridge/submit_query.ts b/hello-bridge/submit_query.ts deleted file mode 100644 index a43aebc9..00000000 --- a/hello-bridge/submit_query.ts +++ /dev/null @@ -1,98 +0,0 @@ -import dotenv from 'dotenv'; -import { Contract, ethers, InterfaceAbi } from 'ethers'; - -import ASCMinterABI from '../contracts/abi/ASCMinter.json'; -import { - generateProofFor, - submitProofToMinterAndAwait, - computeGasLimitForMinter, - isValidPrivateKey, - isValidContractAddress, -} from '../utils'; - -dotenv.config({ override: true }); - -async function main() { - // Setup - const args = process.argv.slice(2); - - if (args.length !== 1) { - console.error(` - Usage: - yarn hello_bridge:submit_query - - Example: - yarn hello_bridge:submit_query 0x87c97c776a678941b5941ec0cb602a4467ff4a35f77264208575f137cb05b2a7 - `); - process.exit(1); - } - - const [transactionHash] = args; - - // Validate Transaction Hash - if (!transactionHash.startsWith('0x') || transactionHash.length !== 66) { - throw new Error('Invalid transaction hash provided'); - } - - // Validate Private Key - const ccNextPrivateKey = process.env.CREDITCOIN_WALLET_PRIVATE_KEY; - if (!isValidPrivateKey(ccNextPrivateKey)) { - throw new Error('CREDITCOIN_WALLET_PRIVATE_KEY environment variable is not configured or invalid'); - } - - const sourceChainKey = Number(process.env.SOURCE_CHAIN_KEY); - if (!sourceChainKey) { - throw new Error('SOURCE_CHAIN_KEY environment variable is not configured or invalid'); - } - - const proverApiUrl = process.env.PROOF_BUILDER_URL; - if (!proverApiUrl) { - throw new Error('PROOF_BUILDER_URL is not configured or invalid'); - } - - const creditcoinRpcUrl = process.env.CREDITCOIN_RPC_URL; - if (!creditcoinRpcUrl) { - throw new Error('CREDITCOIN_RPC_URL environment variable is not configured or invalid'); - } - - const ascMinterContractAddress = process.env.ASC_MINTER_CONTRACT_ADDRESS; - if (!isValidContractAddress(ascMinterContractAddress)) { - throw new Error('ASC_MINTER_CONTRACT_ADDRESS is not configured or invalid'); - } - - const sourceChainRpcUrl = process.env.SOURCE_CHAIN_RPC_URL; - if (!sourceChainRpcUrl) { - throw new Error('SOURCE_CHAIN_RPC_URL environment variable is not configured or invalid'); - } - - // 1. Initialize RPC providers - const ccProvider = new ethers.JsonRpcProvider(creditcoinRpcUrl); - const sourceChainProvider = new ethers.JsonRpcProvider(sourceChainRpcUrl); - - // 2. Validate transaction and generate proof once the block is attested - const proofResult = await generateProofFor( - transactionHash, - sourceChainKey, - proverApiUrl, - ccProvider, - sourceChainProvider - ); - - // 3. Using previously generated proof, submit to ASC minter and await for the minted event - if (proofResult.success) { - // Establish link with the ASC contract - const wallet = new ethers.Wallet(ccNextPrivateKey!, ccProvider); - const contractABI = ASCMinterABI as unknown as InterfaceAbi; - const minterContract = new Contract(ascMinterContractAddress!, contractABI, wallet); - - const proofData = proofResult.data!; - const gasLimit = await computeGasLimitForMinter(ccProvider, minterContract, proofData, wallet.address); - await submitProofToMinterAndAwait(minterContract, proofData, gasLimit); - } else { - throw new Error(`Failed to generate proof: ${proofResult.error}`); - } - - process.exit(0); -} - -main().catch(console.error); diff --git a/loan/.env.example b/loan/.env.example new file mode 100644 index 00000000..c3da0489 --- /dev/null +++ b/loan/.env.example @@ -0,0 +1,18 @@ +# Loan example environment +# Convenience split from root `.env` โ€” use the same CC3 Testnet values. + +SOURCE_CHAIN_KEY=1 +PROOF_BUILDER_URL="https://prover.cc3-testnet.creditcoin.network" +CREDITCOIN_RPC_URL="https://rpc.cc3-testnet.creditcoin.network" +EVM_V1_DECODER_LIBRARY_ADDRESS="0x04B9ae8562D8Cc5bbbBbBB759080dDC30B56D18B" + +## User-provided +SOURCE_CHAIN_RPC_URL="" +CREDITCOIN_WALLET_PRIVATE_KEY="" + +ASC_LOAN_MANAGER_CONTRACT_ADDRESS="" +SOURCE_CHAIN_LOAN_CONTRACT_ADDRESS="" +SOURCE_CHAIN_ERC20_CONTRACT_ADDRESS="" + +LENDER_WALLET_PRIVATE_KEY="" +BORROWER_WALLET_PRIVATE_KEY="" diff --git a/loan-flow/README.md b/loan/README.md similarity index 89% rename from loan-flow/README.md rename to loan/README.md index 8739fb60..1bc33780 100644 --- a/loan-flow/README.md +++ b/loan/README.md @@ -1,8 +1,7 @@ # Loan Flow > [!TIP] -> This tutorial builds on the previous [Bridge Offchain Worker] example -make sure to check it out -> before moving on! +> Complete [Custom Contract Bridging](../bridge/custom-contracts-bridging/README.md) (and optionally the offchain worker in ยง7) before this tutorial. Now that we know how to build an offchain worker to coordinate between two separate chains, we need a more advanced example that includes not only communication but also state tracking. @@ -29,6 +28,16 @@ yarn install The setup of this example is a bit more complex than previous ones. So let's go slowly: +## 1.0 .env fields + +First, we need to fill in our loan/.env with some fields used in prior tutorials: + +```env +## User-provided +SOURCE_CHAIN_RPC_URL="" +CREDITCOIN_WALLET_PRIVATE_KEY="" +``` + ## 1.1 Smart contracts Our system will have three contracts: @@ -40,7 +49,7 @@ Our system will have three contracts: Make sure to first load your `.env` file with: ```sh -source .env +source loan/.env ``` So, first of all we start with deploying our ERC20 contract: @@ -50,7 +59,7 @@ forge create \ --broadcast \ --rpc-url $SOURCE_CHAIN_RPC_URL \ --private-key $CREDITCOIN_WALLET_PRIVATE_KEY \ - contracts/sol/TestERC20.sol:TestERC20 + shared/contracts/sol/TestERC20.sol:TestERC20 ``` This should display some output containing the address of your test `ERC20` contract: @@ -61,46 +70,26 @@ Deployed to: 0x814Fd6EfA5E1cb49B623599Fd63Ab74142762A46 Transaction hash: 0xb234238d0da392063395ef59195b25827cdaf2009fe773fed1f8f7eda99fab7b ``` -Grab the contract address and add it to the `.env` file at the root of the repository with the address, like so: +Grab the contract address and add it to the `loan/.env` file at the root of the repository with the address, like so: ```env SOURCE_CHAIN_ERC20_CONTRACT_ADDRESS= ``` -Now we need to deploy a `EvmV1Decoder` library so that we can reference it in our -`ASCLoanManager`. We do so like this: - -```bash -forge create \ - --broadcast \ - --rpc-url $CREDITCOIN_RPC_URL \ - --private-key $CREDITCOIN_WALLET_PRIVATE_KEY \ - node_modules/@gluwa/usc-contracts/contracts/decoding/EvmV1Decoder.sol:EvmV1Decoder -``` - -You should get some output with the address of the library you just deployed: - -```bash -Deployer: 0x20dB67795C2AEb4De075986b0D4217A109FEF2B5 -Deployed to: 0xfE119359B96Bb1A32c14d32dD6b7E2f977Cd1060 -Transaction hash: 0xd1689ff2d26fd9e271d8c03e7933ce98f2f38836dd18441f6240e86a5816ec8d -``` - -Use the contract address shown in `Deployed to:` to deploy your `ASCLoanManager` using the following command: +Now deploy your `ASCLoanManager` using the following command: ```sh forge create \ --broadcast \ --rpc-url $CREDITCOIN_RPC_URL \ --private-key $CREDITCOIN_WALLET_PRIVATE_KEY \ - --libraries node_modules/@gluwa/usc-contracts/contracts/decoding/EvmV1Decoder.sol:EvmV1Decoder: \ - contracts/sol/ASCLoanManager.sol:ASCLoanManager + --libraries node_modules/@gluwa/asc-contracts/contracts/common/EvmV1Decoder.sol:EvmV1Decoder:$EVM_V1_DECODER_LIBRARY_ADDRESS \ + loan/contracts/sol/ASCLoanManager.sol:ASCLoanManager ``` -> [!IMPORTANT] -> Don't forget to replace `` with the address of your deployed decoder contract! +You should get deployment output with your `ASCLoanManager` address. -As before, grab the address and add it to the `.env` file, like so: +As before, grab the address and add it to the `loan/.env` file, like so: ```env ASC_LOAN_MANAGER_CONTRACT_ADDRESS= @@ -113,10 +102,10 @@ forge create \ --broadcast \ --rpc-url $SOURCE_CHAIN_RPC_URL \ --private-key $CREDITCOIN_WALLET_PRIVATE_KEY \ - contracts/sol/AuxiliaryLoanContract.sol:AuxiliaryLoanContract + loan/contracts/sol/AuxiliaryLoanContract.sol:AuxiliaryLoanContract ``` -Grab the address again (it gets repetitive huh?) and add it to the `.env` file, like so: +Grab the address again (it gets repetitive huh?) and add it to the `loan/.env` file, like so: ```env SOURCE_CHAIN_LOAN_CONTRACT_ADDRESS= @@ -126,7 +115,7 @@ Now that we have all three contracts we can move on to the actual participants. ## 1.2 Lender and borrower -Our system has both the lender account and the borrower fixed, much like before they are set in the `.env` file. +Our system has both the lender account and the borrower fixed, much like before they are set in the `loan/.env` file. But of course, you only have one account at this point, you can reuse the account in `CREDITCOIN_WALLET_PRIVATE_KEY` for the lender, for the borrower you can just create a new one like we did in [Hello Bridge], like so: @@ -161,7 +150,7 @@ Now that we've populated our .env file with all necessary variables, let's load those variables into our terminal. ```sh -source .env +source loan/.env ``` ## 1.3 Funding accounts @@ -266,7 +255,7 @@ Now that our worker is up and running, let's register a loan! Open a new terminal window and source the `.env` file once again ```sh -source .env +source loan/.env ``` Then run the following command to register a loan: @@ -320,7 +309,7 @@ Which will output something like this: ```sh Loan Details: - Loan ID: 1 + Loan ID: 5 From: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 To: 0x70997970C51812dc3A010C7d01b50e0d17dc79C8 With Token: 0x5FbDB2315678afecb367f032d93F642f64180aa3 # betterleaks:allow @@ -422,7 +411,7 @@ Marked loan 5 as repaid on Creditcoin, tx hash: 0xa1c22b43c37e51734d3d388fcd583a Loan 5 has been partially repaid on Creditcoin. Amount repaid: 1000 ``` -Wait a minute... what do you mean partially repaid!? The loan was for 1000 tokens, this is not fair! +Wait a minute... what do you mean partially repaid!? The expected repayment was 1050 tokens, this is not fair! Hmm... oh! The interest! I forgot about that! ```sh @@ -499,7 +488,6 @@ You've learned: If you haven't already, take a look at the [ASC Gitbook] for more information. -[Bridge Offchain Worker]: ../bridge-offchain-worker/README.md -[Hello Bridge]: ../hello-bridge/README.md#11-generate-a-new-wallet-address -[ASC Gitbook]: https://docs.attestcoin.org/ +[Hello Bridge]: ../bridge/hello-bridge/README.md#11-generate-a-new-wallet-address +[ASC Gitbook]: https://docs.attestcoin.org/attestcoin-protocol [๐Ÿšฐ testnet faucet]: https://cloud.google.com/application/web3/faucet/ethereum/sepolia diff --git a/loan/contracts/ABI_GENERATION.md b/loan/contracts/ABI_GENERATION.md new file mode 100644 index 00000000..5d8d063c --- /dev/null +++ b/loan/contracts/ABI_GENERATION.md @@ -0,0 +1,10 @@ +# ABI Generation + +After changing loan contracts under `loan/contracts/sol/`, regenerate ABIs from the repository root: + +```sh +yarn +./loan/contracts/abi-creator.sh +``` + +Loan contracts import readability `ASCBase` and `EvmV1Decoder` from `@gluwa/asc-contracts` (same base as the bridge examples). Shared source-chain `TestERC20` lives under `shared/contracts/`. ABI regeneration: see [bridge/contracts/CONTRIBUTOR_NOTES.md](../../bridge/contracts/CONTRIBUTOR_NOTES.md) and `./shared/contracts/abi-creator.sh`. diff --git a/loan/contracts/abi-creator.sh b/loan/contracts/abi-creator.sh new file mode 100755 index 00000000..af6f40ee --- /dev/null +++ b/loan/contracts/abi-creator.sh @@ -0,0 +1,30 @@ +#!/bin/bash + +# WARNING: execute this from the repository root. +set -euo pipefail + +sol_directory="loan/contracts/sol" +abi_directory="loan/contracts/abi" + +extract_abi() { + local path="$1" + local name="$2" + jq -c --arg path "$path" --arg name "$name" ' + (.contracts // {}) as $c + | ($c[$path + ":" + $name].abi + // (($c | to_entries | map(select(.key | endswith(":" + $name))) | .[0].value.abi) // null) + // {}) + ' +} + +for p in "$sol_directory"/*.sol; do + file=$(basename "$p") + contract_name="${file%.sol}" + file_with_extension="$contract_name.json" + solc --base-path . --include-path "node_modules" \ + "@openzeppelin/=node_modules/@openzeppelin/" \ + "@gluwa/asc-contracts/=node_modules/@gluwa/asc-contracts/" \ + "$p" \ + --combined-json abi --overwrite --json-indent 2 | \ + extract_abi "$p" "$contract_name" | jq --indent 2 . > "$abi_directory/$file_with_extension" +done diff --git a/contracts/abi/ASCLoanManager.json b/loan/contracts/abi/ASCLoanManager.json similarity index 99% rename from contracts/abi/ASCLoanManager.json rename to loan/contracts/abi/ASCLoanManager.json index 5c06b2e9..f0957848 100644 --- a/contracts/abi/ASCLoanManager.json +++ b/loan/contracts/abi/ASCLoanManager.json @@ -64,11 +64,6 @@ "name": "OwnableUnauthorizedAccount", "type": "error" }, - { - "inputs": [], - "name": "ReentrancyGuardReentrantCall", - "type": "error" - }, { "anonymous": false, "inputs": [ diff --git a/contracts/abi/AuxiliaryLoanContract.json b/loan/contracts/abi/AuxiliaryLoanContract.json similarity index 100% rename from contracts/abi/AuxiliaryLoanContract.json rename to loan/contracts/abi/AuxiliaryLoanContract.json diff --git a/contracts/abi/LoanTypes.json b/loan/contracts/abi/LoanTypes.json similarity index 100% rename from contracts/abi/LoanTypes.json rename to loan/contracts/abi/LoanTypes.json diff --git a/contracts/sol/ASCLoanManager.sol b/loan/contracts/sol/ASCLoanManager.sol similarity index 96% rename from contracts/sol/ASCLoanManager.sol rename to loan/contracts/sol/ASCLoanManager.sol index 58c4a271..cc3d85f7 100644 --- a/contracts/sol/ASCLoanManager.sol +++ b/loan/contracts/sol/ASCLoanManager.sol @@ -1,23 +1,20 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.20; -import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; - import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; import {MessageHashUtils} from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; import {LoanFlow, LoanStatus, LoanOrder, LoanTerms} from "./LoanTypes.sol"; -import {EvmV1Decoder} from "@gluwa/usc-contracts/contracts/decoding/EvmV1Decoder.sol"; -import {NativeQueryVerifierLib} from "./VerifierInterface.sol"; -import {ASCBase} from "./ASCBase.sol"; +import {EvmV1Decoder} from "@gluwa/asc-contracts/contracts/common/EvmV1Decoder.sol"; +import {ASCBase} from "@gluwa/asc-contracts/contracts/readability/ASCBase.sol"; /** * @title ASCLoanManager * @dev Main contract for managing cross-chain loan orders in the ASC system */ -contract ASCLoanManager is Ownable, ReentrancyGuard, ASCBase { +contract ASCLoanManager is Ownable, ASCBase { using ECDSA for bytes32; enum LoanManagerActions { @@ -62,8 +59,6 @@ contract ASCLoanManager is Ownable, ReentrancyGuard, ASCBase { constructor() Ownable(msg.sender) { nextLoanId = 1; - // Get the precompile instance using the helper library - VERIFIER = NativeQueryVerifierLib.getVerifier(); } /** diff --git a/contracts/sol/AuxiliaryLoanContract.sol b/loan/contracts/sol/AuxiliaryLoanContract.sol similarity index 100% rename from contracts/sol/AuxiliaryLoanContract.sol rename to loan/contracts/sol/AuxiliaryLoanContract.sol diff --git a/contracts/sol/LoanTypes.sol b/loan/contracts/sol/LoanTypes.sol similarity index 100% rename from contracts/sol/LoanTypes.sol rename to loan/contracts/sol/LoanTypes.sol diff --git a/loan/foundry.toml b/loan/foundry.toml new file mode 100644 index 00000000..d50b158c --- /dev/null +++ b/loan/foundry.toml @@ -0,0 +1,24 @@ +[profile.default] +out = "out" +src = "contracts/sol" +test = "test" +libs = ["../node_modules", "../lib"] +# Resolve @gluwa/asc-contracts from yarn/npm (readability ASCBase + common/EvmV1Decoder). +# Same package as bridge โ€” one ASCBase for both examples. +allow_paths = ["../node_modules", "../lib"] +remappings = [ + "@openzeppelin/=../node_modules/@openzeppelin/", + "@gluwa/asc-contracts/=../node_modules/@gluwa/asc-contracts/", + "forge-std/=../lib/forge-std/src/", +] +solc_version = "0.8.30" +optimizer = true +optimizer_runs = 200 +via_ir = true +evm_version = "shanghai" +verbosity = 2 + +# CI Foundry enables lint-on-build by default; remapped ../node_modules imports +# currently crash the lint engine after a successful compile. +[lint] +lint_on_build = false diff --git a/loan-flow/authorize.ts b/loan/scripts/authorize.ts similarity index 93% rename from loan-flow/authorize.ts rename to loan/scripts/authorize.ts index ea1f9cb7..eecaf59a 100644 --- a/loan-flow/authorize.ts +++ b/loan/scripts/authorize.ts @@ -1,10 +1,10 @@ -import dotenv from 'dotenv'; import { Contract, ethers } from 'ethers'; import loanHelperAbi from '../contracts/abi/AuxiliaryLoanContract.json'; -import { isValidContractAddress, isValidPrivateKey } from '../utils'; +import { isValidContractAddress, isValidPrivateKey } from '../../shared/utils'; +import { loadEnv } from '../../shared/env'; -dotenv.config({ override: true }); +loadEnv('loan'); const main = async () => { const args = process.argv.slice(2); diff --git a/loan-flow/fund_loan.ts b/loan/scripts/fund_loan.ts similarity index 91% rename from loan-flow/fund_loan.ts rename to loan/scripts/fund_loan.ts index 13316331..6a5b9b66 100644 --- a/loan-flow/fund_loan.ts +++ b/loan/scripts/fund_loan.ts @@ -1,11 +1,12 @@ -import dotenv from 'dotenv'; import { Contract, ethers } from 'ethers'; import loanHelperAbi from '../contracts/abi/AuxiliaryLoanContract.json'; -import ERC20Abi from '../contracts/abi/TestERC20.json'; -import { isValidContractAddress, isValidPrivateKey } from '../utils'; +import ERC20Abi from '../../shared/contracts/abi/TestERC20.json'; +import { isValidContractAddress, isValidPrivateKey } from '../../shared/utils'; +import { loadEnv } from '../../shared/env'; +import { submitLoanProofAfterTx } from './loan_proof'; -dotenv.config({ override: true }); +loadEnv('loan'); const main = async () => { const args = process.argv.slice(2); @@ -117,8 +118,10 @@ const main = async () => { console.log('Funding transaction submitted, waiting for it to be mined: ', tx.hash); await tx.wait(); console.log('Loan funded: ', tx.hash); + + await submitLoanProofAfterTx(loanId, tx.hash, 'fund'); } catch (error: any) { - console.error('Error funding loan: ', error.shortMessage); + console.error('Error funding loan: ', error.shortMessage ?? error.message ?? error); process.exit(1); } diff --git a/loan-flow/inspect.ts b/loan/scripts/inspect.ts similarity index 96% rename from loan-flow/inspect.ts rename to loan/scripts/inspect.ts index 5310c21e..63e4f5b6 100644 --- a/loan-flow/inspect.ts +++ b/loan/scripts/inspect.ts @@ -1,10 +1,10 @@ -import dotenv from 'dotenv'; import { Contract, ethers } from 'ethers'; import loanManagerAbi from '../contracts/abi/ASCLoanManager.json'; -import { isValidContractAddress } from '../utils'; +import { isValidContractAddress } from '../../shared/utils'; +import { loadEnv } from '../../shared/env'; -dotenv.config({ override: true }); +loadEnv('loan'); interface LoanFlow { from: string; diff --git a/loan/scripts/loan_proof.ts b/loan/scripts/loan_proof.ts new file mode 100644 index 00000000..7511751b --- /dev/null +++ b/loan/scripts/loan_proof.ts @@ -0,0 +1,190 @@ +import { Contract, ethers, JsonRpcApiProvider } from 'ethers'; + +import loanManagerAbi from '../contracts/abi/ASCLoanManager.json'; +import { + computeGasLimitForLoanManager, + generateProofFor, + isValidContractAddress, + isValidPrivateKey, + submitFundProofToLoanManager, + submitRepayProofToLoanManager, +} from '../../shared/utils'; +import { proofProvider } from '@gluwa/usc-sdk'; + +/** Matches LoanStatus in LoanTypes.sol */ +const LOAN_STATUS_CREATED = 0; +const LOAN_STATUS_FUNDED = 1; +const LOAN_STATUS_PARTLY_REPAID = 2; +const LOAN_STATUS_REPAID = 3; + +export type LoanProofKind = 'fund' | 'repay'; + +async function getLoanStatus(managerContract: Contract, loanId: number): Promise { + const order = await managerContract.getLoanOrder(loanId); + return Number(order[6]); +} + +function logFundedSuccess(loanId: number): void { + console.log(`Loan ${loanId} has been marked as funded on Creditcoin.`); +} + +function logRepaidSuccess(loanId: number): void { + console.log(`Loan ${loanId} has been marked as fully repaid on Creditcoin.`); +} + +function isFundedStatus(status: number): boolean { + return status === LOAN_STATUS_FUNDED || status === LOAN_STATUS_PARTLY_REPAID; +} + +async function submitProofExecuteWithRetry( + managerContract: Contract, + loanId: number, + kind: LoanProofKind, + proofData: proofProvider.ContinuityResponse, + ccProvider: JsonRpcApiProvider, + ccWalletAddress: string +): Promise { + const isRepayment = kind === 'repay'; + const maxAttempts = 3; + + for (let attempt = 1; attempt <= maxAttempts; attempt++) { + const status = await getLoanStatus(managerContract, loanId); + + if (kind === 'fund') { + if (status !== LOAN_STATUS_CREATED) { + if (isFundedStatus(status)) { + logFundedSuccess(loanId); + } + return; + } + } else if (status === LOAN_STATUS_REPAID) { + logRepaidSuccess(loanId); + return; + } else if (status !== LOAN_STATUS_FUNDED && status !== LOAN_STATUS_PARTLY_REPAID) { + throw new Error(`Loan ${loanId} is not in a repayable status on Creditcoin (status=${status})`); + } + + try { + const gasLimit = await computeGasLimitForLoanManager( + ccProvider, + managerContract, + proofData, + ccWalletAddress, + isRepayment + ); + + const response = isRepayment + ? await submitRepayProofToLoanManager(managerContract, proofData, gasLimit) + : await submitFundProofToLoanManager(managerContract, proofData, gasLimit); + + console.log(`Proof execute submitted for loan ${loanId}, tx hash: ${response.hash}`); + await response.wait(); + + if (kind === 'fund') { + console.log(`Marked loan ${loanId} as funded on Creditcoin, tx hash: ${response.hash}`); + logFundedSuccess(loanId); + return; + } + + console.log(`Marked loan ${loanId} as repaid on Creditcoin, tx hash: ${response.hash}`); + const newStatus = await getLoanStatus(managerContract, loanId); + if (newStatus === LOAN_STATUS_REPAID) { + logRepaidSuccess(loanId); + } else if (newStatus === LOAN_STATUS_PARTLY_REPAID) { + const order = await managerContract.getLoanOrder(loanId); + const repaidAmount = order[7]; + console.log(`Loan ${loanId} has been partially repaid on Creditcoin. Amount repaid: ${repaidAmount}`); + } + return; + } catch (error: any) { + const statusAfter = await getLoanStatus(managerContract, loanId); + + if (kind === 'fund' && isFundedStatus(statusAfter)) { + logFundedSuccess(loanId); + return; + } + if (kind === 'repay' && statusAfter === LOAN_STATUS_REPAID) { + logRepaidSuccess(loanId); + return; + } + + const message = error.shortMessage ?? error.message ?? String(error); + if (attempt === maxAttempts) { + throw new Error(`Proof execute failed after ${maxAttempts} attempts: ${message}`); + } + console.warn(`Proof execute attempt ${attempt} failed for loan ${loanId}: ${message}`); + await new Promise((resolve) => setTimeout(resolve, 5000)); + } + } +} + +/** + * Prove a source-chain fund/repay tx and submit to ASCLoanManager on Creditcoin. + * Uses the tx hash + proof builder (no wide eth_getLogs scans on Sepolia). + */ +export async function submitLoanProofAfterTx(loanId: number, txHash: string, kind: LoanProofKind): Promise { + const proofBuilderUrl = process.env.PROOF_BUILDER_URL; + const sourceChainRpcUrl = process.env.SOURCE_CHAIN_RPC_URL; + const ccNextRpcUrl = process.env.CREDITCOIN_RPC_URL; + const ccNextWalletPrivateKey = process.env.CREDITCOIN_WALLET_PRIVATE_KEY; + const loanManagerContractAddress = process.env.ASC_LOAN_MANAGER_CONTRACT_ADDRESS; + const sourceChainKey = Number(process.env.SOURCE_CHAIN_KEY); + + if (!proofBuilderUrl) { + throw new Error('PROOF_BUILDER_URL environment variable is not configured or invalid'); + } + if (!sourceChainRpcUrl) { + throw new Error('SOURCE_CHAIN_RPC_URL environment variable is not configured or invalid'); + } + if (!ccNextRpcUrl) { + throw new Error('CREDITCOIN_RPC_URL environment variable is not configured or invalid'); + } + if (!isValidPrivateKey(ccNextWalletPrivateKey)) { + throw new Error('CREDITCOIN_WALLET_PRIVATE_KEY environment variable is not configured or invalid'); + } + if (!isValidContractAddress(loanManagerContractAddress)) { + throw new Error('ASC_LOAN_MANAGER_CONTRACT_ADDRESS environment variable is not configured or invalid'); + } + if (isNaN(sourceChainKey)) { + throw new Error('SOURCE_CHAIN_KEY environment variable is not configured or invalid'); + } + + const ccProvider = new ethers.JsonRpcProvider(ccNextRpcUrl); + const sourceChainProvider = new ethers.JsonRpcProvider(sourceChainRpcUrl); + const ccWallet = new ethers.Wallet(ccNextWalletPrivateKey!, ccProvider); + const managerContract = new Contract(loanManagerContractAddress!, loanManagerAbi, ccWallet); + + const status = await getLoanStatus(managerContract, loanId); + + if (kind === 'fund') { + if (status !== LOAN_STATUS_CREATED) { + console.log( + `Loan ${loanId} is not in Created status on Creditcoin (status=${status}), skipping fund proof submission.` + ); + if (isFundedStatus(status)) { + logFundedSuccess(loanId); + } + return; + } + } else { + if (status === LOAN_STATUS_REPAID) { + logRepaidSuccess(loanId); + return; + } + if (status !== LOAN_STATUS_FUNDED && status !== LOAN_STATUS_PARTLY_REPAID) { + throw new Error(`Loan ${loanId} is not in a repayable status on Creditcoin (status=${status})`); + } + } + + console.log( + `Detected ${kind === 'fund' ? 'LoanFunded' : 'LoanRepaid'} event for loanId: ${loanId}, tx hash: ${txHash}` + ); + + const proofResult = await generateProofFor(txHash, sourceChainKey, proofBuilderUrl, ccProvider, sourceChainProvider); + + if (!proofResult.success) { + throw new Error(`Failed to generate proof: ${proofResult.error}`); + } + + await submitProofExecuteWithRetry(managerContract, loanId, kind, proofResult.data!, ccProvider, ccWallet.address); +} diff --git a/loan-flow/register.ts b/loan/scripts/register.ts similarity index 76% rename from loan-flow/register.ts rename to loan/scripts/register.ts index d6ac8a69..29cc182e 100644 --- a/loan-flow/register.ts +++ b/loan/scripts/register.ts @@ -1,10 +1,11 @@ -import dotenv from 'dotenv'; import { Contract, ethers, EventLog } from 'ethers'; import loanManagerAbi from '../contracts/abi/ASCLoanManager.json'; -import { isValidContractAddress, isValidPrivateKey } from '../utils'; +import loanHelperAbi from '../contracts/abi/AuxiliaryLoanContract.json'; +import { isValidContractAddress, isValidPrivateKey } from '../../shared/utils'; +import { loadEnv } from '../../shared/env'; -dotenv.config({ override: true }); +loadEnv('loan'); const main = async () => { const args = process.argv.slice(2); @@ -131,7 +132,7 @@ const main = async () => { const lenderSignature = await lenderWallet.signMessage(ethers.toBeArray(payloadToSign)); const borrowerSignature = await borrowerWallet.signMessage(ethers.toBeArray(payloadToSign)); - let loanRegistered = false; + let loanId: bigint | null = null; const registerBlock = await ccProvider.getBlockNumber(); @@ -146,7 +147,7 @@ const main = async () => { } // 5. Wait for the LoanRegistered event - while (!loanRegistered) { + while (loanId === null) { const currentBlock = await ccProvider.getBlockNumber(); console.log(`Waiting for LoanRegistered event... Current block: ${currentBlock}`); const events = await managerContract.queryFilter( @@ -157,12 +158,42 @@ const main = async () => { if (events.length > 0) { const eventData = events[0] as EventLog; - const loanId = eventData.args.loanId; - console.log(`Loan successfully registered with ID: ${loanId.toString()}`); - loanRegistered = true; + loanId = eventData.args.loanId; + console.log(`Loan successfully registered with ID: ${eventData.args.loanId.toString()}`); } - await new Promise((resolve) => setTimeout(resolve, 5000)); + if (loanId === null) { + await new Promise((resolve) => setTimeout(resolve, 5000)); + } + } + + if (loanId === null) { + throw new Error('LoanRegistered event was not observed'); + } + + if (process.env.LOAN_REGISTER_SOURCE_FUNDING_IN_REGISTER === '1') { + const sourceChainRpcUrl = process.env.SOURCE_CHAIN_RPC_URL; + const sourceChainLoanContractAddress = process.env.SOURCE_CHAIN_LOAN_CONTRACT_ADDRESS; + + if (!sourceChainRpcUrl) { + throw new Error('SOURCE_CHAIN_RPC_URL environment variable is not configured or invalid'); + } + if (!isValidContractAddress(sourceChainLoanContractAddress)) { + throw new Error('SOURCE_CHAIN_LOAN_CONTRACT_ADDRESS environment variable is not configured or invalid'); + } + + const sourceChainProvider = new ethers.JsonRpcProvider(sourceChainRpcUrl); + const sourceChainWallet = new ethers.Wallet(ccNextWalletPrivateKey!, sourceChainProvider); + const sourceChainLoanContract = new Contract(sourceChainLoanContractAddress!, loanHelperAbi, sourceChainWallet); + + const tx = await sourceChainLoanContract.registerLoanFund( + loanId, + fundFlow, + loanTerm.loanAmount, + loanTerm.expectedRepaymentAmount + ); + await tx.wait(); + console.log(`Registered loan ${loanId.toString()} for funding on source chain, tx hash: ${tx.hash}`); } process.exit(0); diff --git a/loan-flow/register_source_contract.ts b/loan/scripts/register_source_contract.ts similarity index 95% rename from loan-flow/register_source_contract.ts rename to loan/scripts/register_source_contract.ts index 7f4016e6..786c1724 100644 --- a/loan-flow/register_source_contract.ts +++ b/loan/scripts/register_source_contract.ts @@ -1,10 +1,10 @@ -import dotenv from 'dotenv'; import { Contract, ethers } from 'ethers'; import loanManagerAbi from '../contracts/abi/ASCLoanManager.json'; -import { isValidContractAddress, isValidPrivateKey } from '../utils'; +import { isValidContractAddress, isValidPrivateKey } from '../../shared/utils'; +import { loadEnv } from '../../shared/env'; -dotenv.config({ override: true }); +loadEnv('loan'); const main = async () => { const args = process.argv.slice(2); diff --git a/loan-flow/repay_loan.ts b/loan/scripts/repay_loan.ts similarity index 91% rename from loan-flow/repay_loan.ts rename to loan/scripts/repay_loan.ts index 40210109..57a9cba2 100644 --- a/loan-flow/repay_loan.ts +++ b/loan/scripts/repay_loan.ts @@ -1,11 +1,12 @@ -import dotenv from 'dotenv'; import { Contract, ethers } from 'ethers'; import loanHelperAbi from '../contracts/abi/AuxiliaryLoanContract.json'; -import ERC20Abi from '../contracts/abi/TestERC20.json'; -import { isValidContractAddress, isValidPrivateKey } from '../utils'; +import ERC20Abi from '../../shared/contracts/abi/TestERC20.json'; +import { isValidContractAddress, isValidPrivateKey } from '../../shared/utils'; +import { loadEnv } from '../../shared/env'; +import { submitLoanProofAfterTx } from './loan_proof'; -dotenv.config({ override: true }); +loadEnv('loan'); const main = async () => { const args = process.argv.slice(2); @@ -117,8 +118,10 @@ const main = async () => { console.log('Repayment transaction submitted, waiting for it to be mined: ', tx.hash); await tx.wait(); console.log('Loan repaid: ', tx.hash); + + await submitLoanProofAfterTx(loanId, tx.hash, 'repay'); } catch (error: any) { - console.error('Error repaying loan: ', error); + console.error('Error repaying loan: ', error.shortMessage ?? error.message ?? error); process.exit(1); } diff --git a/loan/scripts/wait_loan_status.ts b/loan/scripts/wait_loan_status.ts new file mode 100644 index 00000000..5e4a4364 --- /dev/null +++ b/loan/scripts/wait_loan_status.ts @@ -0,0 +1,86 @@ +import { Contract, ethers } from 'ethers'; + +import loanManagerAbi from '../contracts/abi/ASCLoanManager.json'; +import { isValidContractAddress } from '../../shared/utils'; +import { loadEnv } from '../../shared/env'; + +loadEnv('loan'); + +const STATUS_NAMES = ['Created', 'Funded', 'PartlyRepaid', 'Repaid', 'Expired'] as const; +type StatusName = (typeof STATUS_NAMES)[number]; + +function parseStatusName(name: string): number { + const index = STATUS_NAMES.indexOf(name as StatusName); + if (index < 0) { + throw new Error(`Invalid status "${name}". Expected one of: ${STATUS_NAMES.join(', ')}`); + } + return index; +} + +const main = async () => { + const args = process.argv.slice(2); + + if (args.length < 2 || args.length > 3) { + console.error(` + Usage: + yarn loan_flow:wait_loan_status [TimeoutSeconds] + + Example: + yarn loan_flow:wait_loan_status 1 Funded 1200 + `); + process.exit(1); + } + + const loanId = Number(args[0]); + const expectedStatus = parseStatusName(args[1]); + const timeoutSecs = args[2] ? Number(args[2]) : 1200; + + if (isNaN(loanId) || loanId < 0) { + throw new Error('Invalid Loan ID provided'); + } + if (isNaN(timeoutSecs) || timeoutSecs <= 0) { + throw new Error('Invalid timeout provided'); + } + + const ccNextRpcUrl = process.env.CREDITCOIN_RPC_URL; + const loanManagerContractAddress = process.env.ASC_LOAN_MANAGER_CONTRACT_ADDRESS; + + if (!ccNextRpcUrl) { + throw new Error('CREDITCOIN_RPC_URL environment variable is not configured or invalid'); + } + if (!isValidContractAddress(loanManagerContractAddress)) { + throw new Error('ASC_LOAN_MANAGER_CONTRACT_ADDRESS environment variable is not configured or invalid'); + } + + const ccProvider = new ethers.JsonRpcProvider(ccNextRpcUrl); + const managerContract = new Contract(loanManagerContractAddress!, loanManagerAbi, ccProvider); + + const deadline = Date.now() + timeoutSecs * 1000; + const expectedName = STATUS_NAMES[expectedStatus]; + + while (Date.now() < deadline) { + const currentOrder = await managerContract.getLoanOrder(loanId); + const status = Number(currentOrder[6]); + const statusName = STATUS_NAMES[status] ?? String(status); + + if (status === expectedStatus) { + console.log(`Loan ${loanId} reached status ${statusName} on Creditcoin.`); + process.exit(0); + } + + console.log(`Waiting for loan ${loanId} status ${expectedName} (current: ${statusName})...`); + await new Promise((resolve) => setTimeout(resolve, 15000)); + } + + const finalOrder = await managerContract.getLoanOrder(loanId); + const finalStatus = Number(finalOrder[6]); + console.error( + `Timed out after ${timeoutSecs}s waiting for loan ${loanId} status ${expectedName} (last status: ${STATUS_NAMES[finalStatus] ?? finalStatus}).` + ); + process.exit(1); +}; + +main().catch((error) => { + console.error(error); + process.exit(1); +}); diff --git a/loan-flow/worker.ts b/loan/scripts/worker.ts similarity index 80% rename from loan-flow/worker.ts rename to loan/scripts/worker.ts index 092f43eb..7703e1c3 100644 --- a/loan-flow/worker.ts +++ b/loan/scripts/worker.ts @@ -1,4 +1,3 @@ -import dotenv from 'dotenv'; import { Contract, ethers } from 'ethers'; import loanManagerAbi from '../contracts/abi/ASCLoanManager.json'; @@ -13,9 +12,10 @@ import { POLLING_INTERVAL_MS, submitFundProofToLoanManager, submitRepayProofToLoanManager, -} from '../utils'; +} from '../../shared/utils'; +import { loadEnv } from '../../shared/env'; -dotenv.config({ override: true }); +loadEnv('loan'); // Graceful shutdown flag let isShuttingDown = false; @@ -218,7 +218,23 @@ const main = async () => { return; } + const onChainOrder = await managerContract.getLoanOrder(loanId); + const onChainStatus = Number(onChainOrder[6]); + if (onChainStatus !== 0) { + console.log(`Loan ${loanId} already marked on Creditcoin (status=${onChainStatus}), skipping fund proof.`); + loanInfo.funded = onChainStatus >= 1 && onChainStatus !== 4; + processedTxs.add(txHash); + return; + } + // 6.1 Validate transaction and generate proof once the block is attested + if (process.env.LOAN_WORKER_SKIP_FUND_REPAY_PROOF === '1') { + console.log(`Skipping fund proof in worker (scripts submit proofs); loan ${loanId} funded on source chain.`); + loanInfo.funded = true; + processedTxs.add(txHash); + return; + } + const proofResult = await generateProofFor( txHash, sourceChainKey, @@ -228,6 +244,16 @@ const main = async () => { ); if (proofResult.success) { + const statusAfterProof = Number((await managerContract.getLoanOrder(loanId))[6]); + if (statusAfterProof !== 0) { + console.log( + `Loan ${loanId} already marked on Creditcoin after proof wait (status=${statusAfterProof}), skipping fund proof.` + ); + loanInfo.funded = statusAfterProof >= 1 && statusAfterProof !== 4; + processedTxs.add(txHash); + return; + } + try { // 6.2 Mark loan as funded on Creditcoin const gasLimit = await computeGasLimitForLoanManager( @@ -247,7 +273,7 @@ const main = async () => { console.error(`Error on LoanFunded handle for ${loanId}: `, error.shortMessage); } } else { - throw new Error(`Failed to generate proof: ${proofResult.error}`); + console.error(`Failed to generate proof for LoanFunded ${loanId}: ${proofResult.error}`); } }), // 7. Poll LoanRepaid events on source chain loan contract @@ -268,11 +294,40 @@ const main = async () => { return; } - if (loanInfo.expired || loanInfo.repaid || !loanInfo.funded) { + const onChainOrder = await managerContract.getLoanOrder(loanId); + const onChainStatus = Number(onChainOrder[6]); + + if (loanInfo.expired || loanInfo.repaid) { console.warn(`Loan ${loanId} is not in a valid state for repayment. Skipping.`); return; } + if (!loanInfo.funded && onChainStatus >= 1 && onChainStatus !== 4) { + loanInfo.funded = true; + } + + if (!loanInfo.funded) { + console.warn(`Loan ${loanId} is not funded in worker tracker yet. Skipping repay proof.`); + return; + } + + if (onChainStatus === 3) { + console.log(`Loan ${loanId} already fully repaid on Creditcoin, skipping repay proof.`); + loanInfo.repaid = true; + processedTxs.add(txHash); + return; + } + if (onChainStatus !== 1 && onChainStatus !== 2) { + console.warn(`Loan ${loanId} not repayable on Creditcoin (status=${onChainStatus}). Skipping.`); + return; + } + + if (process.env.LOAN_WORKER_SKIP_FUND_REPAY_PROOF === '1') { + console.log(`Skipping repay proof in worker (scripts submit proofs); loan ${loanId} repaid on source chain.`); + processedTxs.add(txHash); + return; + } + // 7.1 Validate transaction and generate proof once the block is attested const proofResult = await generateProofFor( txHash, @@ -283,6 +338,20 @@ const main = async () => { ); if (proofResult.success) { + const statusAfterProof = Number((await managerContract.getLoanOrder(loanId))[6]); + if (statusAfterProof === 3) { + console.log(`Loan ${loanId} already fully repaid on Creditcoin after proof wait, skipping repay proof.`); + loanInfo.repaid = true; + processedTxs.add(txHash); + return; + } + if (statusAfterProof !== 1 && statusAfterProof !== 2) { + console.warn( + `Loan ${loanId} not repayable on Creditcoin after proof wait (status=${statusAfterProof}). Skipping.` + ); + return; + } + try { // 7.2 Note loan repayment on Creditcoin, depending on whether the loan is fully repaid or not // will trigger either partial or full repayment events @@ -308,7 +377,7 @@ const main = async () => { console.error(`Error on LoanRepaid handle for ${loanId}: `, error.shortMessage); } } else { - throw new Error(`Failed to generate proof: ${proofResult.error}`); + console.error(`Failed to generate proof for LoanRepaid ${loanId}: ${proofResult.error}`); } }), // 8. Polling LoanFunded, LoanExpired, LoanPartiallyRepaid and LoanRepaid events on manager contract diff --git a/loan/test/ASCLoanManagerSourceBinding.t.sol b/loan/test/ASCLoanManagerSourceBinding.t.sol new file mode 100644 index 00000000..3e810b49 --- /dev/null +++ b/loan/test/ASCLoanManagerSourceBinding.t.sol @@ -0,0 +1,73 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {Test} from "forge-std/Test.sol"; +import {ASCLoanManagerHarness} from "./harness/ASCLoanManagerHarness.sol"; +import {EvmV1Decoder} from "@gluwa/asc-contracts/contracts/common/EvmV1Decoder.sol"; + +/// @notice Regression tests for loan source-contract binding โ€” fund/repay logs must come from the registered source contract. +contract ASCLoanManagerSourceBindingTest is Test { + ASCLoanManagerHarness internal manager; + + bytes32 internal constant FUND_EVENT_SIGNATURE = + 0x9e71d2fb732e68272b7e74ecfd14638673c1d77e19a5d390a3ffff054d57c44b; + bytes32 internal constant REPAY_EVENT_SIGNATURE = + 0x040cee90ee4799897c30ca04e5feb6fa43dbba9b6d084b4b257cdafd84ba013e; + + address internal registeredSource = address(0xA11CE); + address internal spoofedSource = address(0xBAD); + + function setUp() public { + manager = new ASCLoanManagerHarness(); + manager.registerSourceLoanContract(registeredSource); + } + + function testFundLog_acceptsRegisteredEmitter() public view { + uint256 loanId = manager.exposeProcessFundLogs(_fundLog(registeredSource, 42)); + assertEq(loanId, 42); + } + + function testFundLog_rejectsUnregisteredSource() public { + ASCLoanManagerHarness fresh = new ASCLoanManagerHarness(); + vm.expectRevert("Source loan contract not registered!"); + fresh.exposeProcessFundLogs(_fundLog(registeredSource, 1)); + } + + function testFundLog_rejectsWrongEmitter() public { + vm.expectRevert("LoanFunded event not emitted by registered source loan contract!"); + manager.exposeProcessFundLogs(_fundLog(spoofedSource, 1)); + } + + function testRepayLog_acceptsRegisteredEmitter() public view { + (uint256 loanId, uint256 amount) = manager.exposeProcessRepayLogs(_repayLog(registeredSource, 7, 1000)); + assertEq(loanId, 7); + assertEq(amount, 1000); + } + + function testRepayLog_rejectsWrongEmitter() public { + vm.expectRevert("LoanRepaid event not emitted by registered source loan contract!"); + manager.exposeProcessRepayLogs(_repayLog(spoofedSource, 7, 1000)); + } + + function _fundLog(address emitter, uint256 loanId) internal pure returns (EvmV1Decoder.LogEntry[] memory logs) { + logs = new EvmV1Decoder.LogEntry[](1); + logs[0].address_ = emitter; + logs[0].topics = new bytes32[](2); + logs[0].topics[0] = FUND_EVENT_SIGNATURE; + logs[0].topics[1] = bytes32(loanId); + logs[0].data = ""; + } + + function _repayLog(address emitter, uint256 loanId, uint256 amount) + internal + pure + returns (EvmV1Decoder.LogEntry[] memory logs) + { + logs = new EvmV1Decoder.LogEntry[](1); + logs[0].address_ = emitter; + logs[0].topics = new bytes32[](2); + logs[0].topics[0] = REPAY_EVENT_SIGNATURE; + logs[0].topics[1] = bytes32(loanId); + logs[0].data = abi.encode(amount); + } +} diff --git a/loan/test/harness/ASCLoanManagerHarness.sol b/loan/test/harness/ASCLoanManagerHarness.sol new file mode 100644 index 00000000..efe93c6e --- /dev/null +++ b/loan/test/harness/ASCLoanManagerHarness.sol @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {ASCLoanManager} from "../../contracts/sol/ASCLoanManager.sol"; +import {EvmV1Decoder} from "@gluwa/asc-contracts/contracts/common/EvmV1Decoder.sol"; + +/// @dev Test harness exposing internal log validators for source-contract binding tests. +contract ASCLoanManagerHarness is ASCLoanManager { + function exposeProcessFundLogs(EvmV1Decoder.LogEntry[] memory fundLogs) external view returns (uint256) { + return _processFundLogs(fundLogs); + } + + function exposeProcessRepayLogs(EvmV1Decoder.LogEntry[] memory repayLogs) + external + view + returns (uint256 loanId, uint256 amount) + { + return _processRepayLogs(repayLogs); + } +} diff --git a/package.json b/package.json index a80e0a1c..2f6f46b4 100644 --- a/package.json +++ b/package.json @@ -3,21 +3,26 @@ "name": "asc-testnet-bridge-examples", "version": "0.2.0", "scripts": { - "hello_bridge:submit_query": "tsx hello-bridge/submit_query.ts", - "custom_bridge:submit_query": "tsx custom-contracts-bridging/submit_query.ts", - "offchain:start_worker": "tsx bridge-offchain-worker/worker.ts", - "loan_flow:authorize_token": "tsx loan-flow/authorize.ts", - "loan_flow:register_source_contract": "tsx loan-flow/register_source_contract.ts", - "loan_flow:register_loan": "tsx loan-flow/register.ts", - "loan_flow:fund_loan": "tsx loan-flow/fund_loan.ts", - "loan_flow:repay_loan": "tsx loan-flow/repay_loan.ts", - "loan_flow:inspect_loan": "tsx loan-flow/inspect.ts", - "loan_flow:start_worker": "tsx loan-flow/worker.ts", - "utils:check_balance": "node utils/check_balance.js", + "hello_bridge:submit_query": "tsx bridge/hello-bridge/submit_query.ts", + "custom_bridge:submit_query": "tsx bridge/custom-contracts-bridging/submit_query.ts", + "offchain:start_worker": "tsx bridge/bridge-offchain-worker/worker.ts", + "loan_flow:authorize_token": "tsx loan/scripts/authorize.ts", + "loan_flow:register_source_contract": "tsx loan/scripts/register_source_contract.ts", + "loan_flow:register_loan": "tsx loan/scripts/register.ts", + "loan_flow:fund_loan": "tsx loan/scripts/fund_loan.ts", + "loan_flow:repay_loan": "tsx loan/scripts/repay_loan.ts", + "loan_flow:inspect_loan": "tsx loan/scripts/inspect.ts", + "loan_flow:wait_loan_status": "tsx loan/scripts/wait_loan_status.ts", + "loan_flow:start_worker": "tsx loan/scripts/worker.ts", + "utils:check_balance": "tsx shared/utils/check_balance.js", + "utils:check_setup": "tsx shared/check_setup.ts", "check-format": "prettier --check .", - "eslint": "eslint -c .eslintrc.js --ignore-pattern .eslintrc.js --ignore-pattern check_balance.js --max-warnings 0 --ext .ts --ext .js .", + "eslint": "eslint -c .eslintrc.js --ignore-pattern .eslintrc.js --ignore-pattern check_balance.js --ignore-pattern vendor/** --max-warnings 0 --ext .ts --ext .js .", "format": "prettier --write .", - "build": "forge build", + "build": "forge build --root bridge && forge build --root loan", + "test": "forge test --root bridge && forge test --root loan", + "test:contracts": "forge test --root bridge && forge test --root loan", + "verify": "yarn typecheck && yarn eslint && yarn build && yarn test:contracts", "typecheck": "tsc --noEmit" }, "devDependencies": { @@ -26,11 +31,11 @@ "eslint": "^8.57.0", "eslint-config-prettier": "^10.1.8", "prettier": "^3.9.4", - "typescript": "6.0.2", - "tsx": "^4.22.4" + "tsx": "^4.22.4", + "typescript": "6.0.2" }, "dependencies": { - "@gluwa/usc-contracts": "0.1.2", + "@gluwa/asc-contracts": "0.2.1", "@gluwa/usc-sdk": "0.18.0", "@openzeppelin/contracts": "5.4.0", "@types/node": "26.1.0", diff --git a/shared/check_setup.ts b/shared/check_setup.ts new file mode 100644 index 00000000..48ef3d27 --- /dev/null +++ b/shared/check_setup.ts @@ -0,0 +1,360 @@ +import { ethers } from 'ethers'; + +import { chainInfo } from '@gluwa/usc-sdk'; +import { isValidContractAddress, isValidPrivateKey } from './utils'; +import { loadEnv } from './env'; + +const VERIFIER_PRECOMPILE = '0x0000000000000000000000000000000000000FD2'; + +type SetupMode = 'hello' | 'hello-bridge' | 'bridge' | 'custom' | 'loan' | 'network'; + +/** Loads the same files the tutorial scripts do, so a passing check reflects what they will see. */ +function loadTutorialEnv(mode: SetupMode): void { + if (mode === 'loan') { + loadEnv('loan'); + } else if (mode === 'network') { + loadEnv(); + } else { + loadEnv('bridge'); + } +} + +function normalizeMode(raw: string | undefined): SetupMode { + const mode = (raw ?? 'hello').toLowerCase(); + switch (mode) { + case 'hello': + case 'hello-bridge': + case 'bridge': + case 'custom': + case 'loan': + case 'network': + return mode; + default: + throw new Error(`Unknown mode "${mode}". Use: hello | bridge | loan | network`); + } +} + +type CheckResult = { ok: boolean; message: string }; + +function pass(message: string): CheckResult { + return { ok: true, message }; +} + +function fail(message: string): CheckResult { + return { ok: false, message }; +} + +async function checkRpc(label: string, url: string | undefined): Promise { + if (!url?.trim()) { + return fail(`${label}: not set`); + } + try { + const provider = new ethers.JsonRpcProvider(url); + const network = await provider.getNetwork(); + const block = await provider.getBlockNumber(); + return pass(`${label}: reachable (chainId ${network.chainId}, block ${block})`); + } catch (error: unknown) { + const msg = error instanceof Error ? error.message : String(error); + return fail(`${label}: unreachable (${msg})`); + } +} + +async function checkProofBuilder(url: string | undefined): Promise { + if (!url?.trim()) { + return fail('PROOF_BUILDER_URL: not set'); + } + try { + const response = await fetch(url.replace(/\/$/, ''), { method: 'GET' }); + if (response.ok || response.status === 404 || response.status === 405) { + return pass(`PROOF_BUILDER_URL: reachable (${response.status})`); + } + return fail(`PROOF_BUILDER_URL: HTTP ${response.status}`); + } catch (error: unknown) { + const msg = error instanceof Error ? error.message : String(error); + return fail(`PROOF_BUILDER_URL: unreachable (${msg})`); + } +} + +/** ethers surfaces an on-chain revert as CALL_EXCEPTION; anything else is a transport failure. */ +function isExecutionRevert(error: unknown): boolean { + return typeof error === 'object' && error !== null && (error as { code?: unknown }).code === 'CALL_EXCEPTION'; +} + +async function checkVerifierPrecompile(creditcoinRpc: string | undefined): Promise { + if (!creditcoinRpc?.trim()) { + return fail('Verifier precompile: skipped (no CREDITCOIN_RPC_URL)'); + } + try { + const provider = new ethers.JsonRpcProvider(creditcoinRpc); + // Frontier precompiles are runtime PrecompileSet entries rather than accounts holding + // bytecode, so `getCode` returns '0x' for them even on Creditcoin. Probe instead: an + // empty call makes the precompile revert on the absent selector, while calling an + // address with nothing behind it succeeds and returns '0x'. + const result = await provider.call({ to: VERIFIER_PRECOMPILE, data: '0x' }); + return fail(`Native verifier precompile (0xFD2): nothing at address, call returned ${result} (wrong network?)`); + } catch (error: unknown) { + if (isExecutionRevert(error)) { + return pass('Native verifier precompile (0xFD2): present'); + } + const msg = error instanceof Error ? error.message : String(error); + return fail(`Verifier precompile check failed (${msg})`); + } +} + +async function checkContractCode( + label: string, + rpcUrl: string | undefined, + address: string | undefined +): Promise { + if (!isValidContractAddress(address)) { + return fail(`${label}: not configured`); + } + if (!rpcUrl?.trim()) { + return fail(`${label}: skipped (no RPC URL)`); + } + try { + const provider = new ethers.JsonRpcProvider(rpcUrl); + const code = await provider.getCode(address!); + if (code && code !== '0x') { + return pass(`${label}: contract code found at ${address}`); + } + return fail(`${label}: no code at ${address}`); + } catch (error: unknown) { + const msg = error instanceof Error ? error.message : String(error); + return fail(`${label}: check failed (${msg})`); + } +} + +function printResults(results: CheckResult[]): boolean { + let allOk = true; + for (const result of results) { + console.log(`${result.ok ? 'โœ“' : 'โœ—'} ${result.message}`); + if (!result.ok) { + allOk = false; + } + } + return allOk; +} + +async function checkChainKeyAttestation(creditcoinRpc: string | undefined, chainKey: number): Promise { + if (!creditcoinRpc?.trim()) { + return fail('Chain attestation: skipped (no CREDITCOIN_RPC_URL)'); + } + if (!chainKey) { + return fail('Chain attestation: SOURCE_CHAIN_KEY missing'); + } + try { + const provider = new ethers.JsonRpcProvider(creditcoinRpc); + const info = new chainInfo.PrecompileChainInfoProvider(provider); + const latest = await info.getLatestAttestedHeightAndHash(chainKey); + return pass(`Chain key ${chainKey} attestation: latest height ${latest.height}`); + } catch (error: unknown) { + const msg = error instanceof Error ? error.message : String(error); + return fail(`Chain attestation failed (${msg})`); + } +} + +async function runNetworkChecks(): Promise { + const sourceChainKey = Number(process.env.SOURCE_CHAIN_KEY); + const results: CheckResult[] = [ + sourceChainKey > 0 ? pass(`SOURCE_CHAIN_KEY: ${sourceChainKey}`) : fail('SOURCE_CHAIN_KEY: missing or invalid'), + await checkRpc('CREDITCOIN_RPC_URL', process.env.CREDITCOIN_RPC_URL), + await checkProofBuilder(process.env.PROOF_BUILDER_URL), + await checkVerifierPrecompile(process.env.CREDITCOIN_RPC_URL), + await checkChainKeyAttestation(process.env.CREDITCOIN_RPC_URL, sourceChainKey), + ]; + return printResults(results); +} + +async function runHelloBridgeChecks(): Promise { + const results: CheckResult[] = []; + + const sourceChainKey = Number(process.env.SOURCE_CHAIN_KEY); + results.push( + sourceChainKey > 0 ? pass(`SOURCE_CHAIN_KEY: ${sourceChainKey}`) : fail('SOURCE_CHAIN_KEY: missing or invalid') + ); + + results.push( + isValidPrivateKey(process.env.CREDITCOIN_WALLET_PRIVATE_KEY) + ? pass('CREDITCOIN_WALLET_PRIVATE_KEY: set') + : fail('CREDITCOIN_WALLET_PRIVATE_KEY: missing or invalid') + ); + + results.push(await checkRpc('CREDITCOIN_RPC_URL', process.env.CREDITCOIN_RPC_URL)); + results.push(await checkRpc('SOURCE_CHAIN_RPC_URL', process.env.SOURCE_CHAIN_RPC_URL)); + results.push(await checkProofBuilder(process.env.PROOF_BUILDER_URL)); + results.push(await checkVerifierPrecompile(process.env.CREDITCOIN_RPC_URL)); + results.push( + await checkContractCode( + 'ASC_MINTER_CONTRACT_ADDRESS', + process.env.CREDITCOIN_RPC_URL, + process.env.ASC_MINTER_CONTRACT_ADDRESS + ) + ); + results.push( + await checkContractCode( + 'SOURCE_CHAIN_CONTRACT_ADDRESS (Sepolia burner)', + process.env.SOURCE_CHAIN_RPC_URL, + process.env.SOURCE_CHAIN_CONTRACT_ADDRESS + ) + ); + + results.push(await checkChainKeyAttestation(process.env.CREDITCOIN_RPC_URL, sourceChainKey)); + + return printResults(results); +} + +async function runBridgeChecks(): Promise { + const results: CheckResult[] = []; + + const sourceChainKey = Number(process.env.SOURCE_CHAIN_KEY); + results.push( + sourceChainKey > 0 ? pass(`SOURCE_CHAIN_KEY: ${sourceChainKey}`) : fail('SOURCE_CHAIN_KEY: missing or invalid') + ); + + results.push(await checkRpc('CREDITCOIN_RPC_URL', process.env.CREDITCOIN_RPC_URL)); + results.push(await checkRpc('SOURCE_CHAIN_RPC_URL', process.env.SOURCE_CHAIN_RPC_URL)); + results.push(await checkProofBuilder(process.env.PROOF_BUILDER_URL)); + results.push(await checkVerifierPrecompile(process.env.CREDITCOIN_RPC_URL)); + results.push(await checkChainKeyAttestation(process.env.CREDITCOIN_RPC_URL, sourceChainKey)); + + results.push( + isValidPrivateKey(process.env.CREDITCOIN_WALLET_PRIVATE_KEY) + ? pass('CREDITCOIN_WALLET_PRIVATE_KEY: set') + : fail('CREDITCOIN_WALLET_PRIVATE_KEY: missing or invalid') + ); + + if (isValidContractAddress(process.env.EVM_V1_DECODER_LIBRARY_ADDRESS)) { + results.push( + pass(`EVM_V1_DECODER_LIBRARY_ADDRESS: ${process.env.EVM_V1_DECODER_LIBRARY_ADDRESS} (reuse for loan deploy)`) + ); + } else { + results.push( + pass('EVM_V1_DECODER_LIBRARY_ADDRESS: not set (deploy EvmV1Decoder once in custom-contracts tutorial)') + ); + } + + return printResults(results); +} + +async function runLoanChecks(): Promise { + const results: CheckResult[] = []; + + const sourceChainKey = Number(process.env.SOURCE_CHAIN_KEY); + results.push( + sourceChainKey > 0 ? pass(`SOURCE_CHAIN_KEY: ${sourceChainKey}`) : fail('SOURCE_CHAIN_KEY: missing or invalid') + ); + + results.push(await checkRpc('CREDITCOIN_RPC_URL', process.env.CREDITCOIN_RPC_URL)); + results.push(await checkRpc('SOURCE_CHAIN_RPC_URL', process.env.SOURCE_CHAIN_RPC_URL)); + results.push(await checkProofBuilder(process.env.PROOF_BUILDER_URL)); + results.push(await checkVerifierPrecompile(process.env.CREDITCOIN_RPC_URL)); + results.push(await checkChainKeyAttestation(process.env.CREDITCOIN_RPC_URL, sourceChainKey)); + + results.push( + isValidPrivateKey(process.env.CREDITCOIN_WALLET_PRIVATE_KEY) + ? pass('CREDITCOIN_WALLET_PRIVATE_KEY: set') + : fail('CREDITCOIN_WALLET_PRIVATE_KEY: missing or invalid') + ); + results.push( + isValidPrivateKey(process.env.LENDER_WALLET_PRIVATE_KEY) + ? pass('LENDER_WALLET_PRIVATE_KEY: set') + : fail('LENDER_WALLET_PRIVATE_KEY: missing or invalid') + ); + results.push( + isValidPrivateKey(process.env.BORROWER_WALLET_PRIVATE_KEY) + ? pass('BORROWER_WALLET_PRIVATE_KEY: set') + : fail('BORROWER_WALLET_PRIVATE_KEY: missing or invalid') + ); + + if (isValidContractAddress(process.env.EVM_V1_DECODER_LIBRARY_ADDRESS)) { + results.push(pass(`EVM_V1_DECODER_LIBRARY_ADDRESS: ${process.env.EVM_V1_DECODER_LIBRARY_ADDRESS}`)); + } else { + results.push( + pass('EVM_V1_DECODER_LIBRARY_ADDRESS: not set (reuse from bridge tutorial or deploy once on Creditcoin)') + ); + } + + if (isValidContractAddress(process.env.ASC_LOAN_MANAGER_CONTRACT_ADDRESS)) { + results.push( + await checkContractCode( + 'ASC_LOAN_MANAGER_CONTRACT_ADDRESS', + process.env.CREDITCOIN_RPC_URL, + process.env.ASC_LOAN_MANAGER_CONTRACT_ADDRESS + ) + ); + } else { + results.push(fail('ASC_LOAN_MANAGER_CONTRACT_ADDRESS: not configured')); + } + + if (isValidContractAddress(process.env.SOURCE_CHAIN_LOAN_CONTRACT_ADDRESS)) { + results.push( + await checkContractCode( + 'SOURCE_CHAIN_LOAN_CONTRACT_ADDRESS', + process.env.SOURCE_CHAIN_RPC_URL, + process.env.SOURCE_CHAIN_LOAN_CONTRACT_ADDRESS + ) + ); + } else { + results.push(fail('SOURCE_CHAIN_LOAN_CONTRACT_ADDRESS: not configured')); + } + + if (isValidContractAddress(process.env.SOURCE_CHAIN_ERC20_CONTRACT_ADDRESS)) { + results.push( + await checkContractCode( + 'SOURCE_CHAIN_ERC20_CONTRACT_ADDRESS', + process.env.SOURCE_CHAIN_RPC_URL, + process.env.SOURCE_CHAIN_ERC20_CONTRACT_ADDRESS + ) + ); + } else { + results.push(fail('SOURCE_CHAIN_ERC20_CONTRACT_ADDRESS: not configured')); + } + + return printResults(results); +} + +async function main(): Promise { + let mode: SetupMode; + try { + mode = normalizeMode(process.argv[2]); + } catch (error: unknown) { + const msg = error instanceof Error ? error.message : String(error); + console.error(msg); + process.exit(1); + } + + loadTutorialEnv(mode); + + console.log(`Tutorial setup check (${mode})\n`); + + let ok: boolean; + switch (mode) { + case 'hello': + case 'hello-bridge': + ok = await runHelloBridgeChecks(); + break; + case 'bridge': + case 'custom': + ok = await runBridgeChecks(); + break; + case 'loan': + ok = await runLoanChecks(); + break; + case 'network': + ok = await runNetworkChecks(); + break; + } + + if (!ok) { + console.error(`\nFix the items above, then re-run: yarn utils:check_setup ${mode}`); + process.exit(1); + } + + console.log('\nSetup looks good. You can continue the tutorial.'); +} + +main().catch((error) => { + console.error(error); + process.exit(1); +}); diff --git a/shared/contracts/abi-creator.sh b/shared/contracts/abi-creator.sh new file mode 100755 index 00000000..32eaee5c --- /dev/null +++ b/shared/contracts/abi-creator.sh @@ -0,0 +1,31 @@ +#!/bin/bash + +# WARNING: execute this from the repository root. +set -euo pipefail + +sol_directory="shared/contracts/sol" +abi_directory="shared/contracts/abi" + +extract_abi() { + local path="$1" + local name="$2" + jq -c --arg path "$path" --arg name "$name" ' + (.contracts // {}) as $c + | ($c[$path + ":" + $name].abi + // (($c | to_entries | map(select(.key | endswith(":" + $name))) | .[0].value.abi) // null) + // {}) + ' +} + +mkdir -p "$abi_directory" + +for p in "$sol_directory"/*.sol; do + file=$(basename "$p") + contract_name="${file%.sol}" + file_with_extension="$contract_name.json" + solc --base-path . --include-path "node_modules" \ + "@openzeppelin/=node_modules/@openzeppelin/" \ + "$p" \ + --combined-json abi --overwrite --json-indent 2 | \ + extract_abi "$p" "$contract_name" | jq --indent 2 . > "$abi_directory/$file_with_extension" +done diff --git a/contracts/abi/TestERC20.json b/shared/contracts/abi/TestERC20.json similarity index 100% rename from contracts/abi/TestERC20.json rename to shared/contracts/abi/TestERC20.json diff --git a/contracts/sol/TestERC20.sol b/shared/contracts/sol/TestERC20.sol similarity index 87% rename from contracts/sol/TestERC20.sol rename to shared/contracts/sol/TestERC20.sol index 3ce1b133..b8c1c791 100644 --- a/contracts/sol/TestERC20.sol +++ b/shared/contracts/sol/TestERC20.sol @@ -3,6 +3,7 @@ pragma solidity ^0.8.20; import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +/// @notice Minimal Sepolia burner ERC20 shared by bridge and loan tutorials. contract TestERC20 is ERC20 { address public constant BURN_ADDRESS = address(1); // 0x...01 @@ -12,7 +13,6 @@ contract TestERC20 is ERC20 { event TokensBurnedForBridging(address indexed from, uint256 value); constructor() ERC20("Burn Test", "TEST") { - // Mint sender initial supply _mint(msg.sender, 1_000_000 ether); } @@ -24,8 +24,7 @@ contract TestERC20 is ERC20 { return true; } - /// @notice Mint new tokens to a specified address. - /// @param amount The amount of tokens to mint + /// @notice Mint new tokens to the caller. function mint(uint256 amount) external returns (bool) { _mint(msg.sender, amount); return true; diff --git a/shared/env.ts b/shared/env.ts new file mode 100644 index 00000000..3e0d9587 --- /dev/null +++ b/shared/env.ts @@ -0,0 +1,43 @@ +import fs from 'fs'; +import path from 'path'; +import dotenv from 'dotenv'; + +const REPO_ROOT = path.resolve(__dirname, '..'); + +export type Tutorial = 'bridge' | 'loan'; + +/** + * Loads tutorial configuration into `process.env`. + * + * Config lives in the per-tutorial `bridge/.env` and `loan/.env`; the root `.env` is + * optional and only fills in whatever the tutorial file leaves unset. A bare + * `dotenv.config()` would instead resolve `.env` against `process.cwd()`, which misses + * both tutorial files and depends on the directory the script happens to be run from. + * + * Precedence, highest first: shell/exported env, the tutorial's `.env`, the root `.env`. + * + * CI writes only the root `.env` and never creates the per-tutorial files, so passing a + * tutorial stays correct there โ€” the missing file is skipped. + * + * @param tutorials Which tutorials' `.env` to layer on top of the root one, lowest precedence + * first. Omit to load only the root; pass several for utilities shared by + * more than one tutorial. + */ +export function loadEnv(...tutorials: Tutorial[]): void { + const shellEnv = { ...process.env }; + + const files = [path.join(REPO_ROOT, '.env'), ...tutorials.map((t) => path.join(REPO_ROOT, t, '.env'))]; + + for (const file of files) { + if (fs.existsSync(file)) { + dotenv.config({ path: file, override: true }); + } + } + + // Anything the caller already exported outranks both files. + for (const [key, value] of Object.entries(shellEnv)) { + if (value !== undefined) { + process.env[key] = value; + } + } +} diff --git a/custom-contracts-bridging/submit_query.ts b/shared/submit_bridge_query.ts similarity index 54% rename from custom-contracts-bridging/submit_query.ts rename to shared/submit_bridge_query.ts index a4356600..dc7decbb 100644 --- a/custom-contracts-bridging/submit_query.ts +++ b/shared/submit_bridge_query.ts @@ -1,40 +1,38 @@ -import dotenv from 'dotenv'; import { Contract, ethers, InterfaceAbi } from 'ethers'; -import ASCMinterABI from '../contracts/abi/ASCMinter.json'; +import ASCMinterABI from '../bridge/contracts/abi/ASCMinter.json'; import { generateProofFor, - computeGasLimitForMinter, submitProofToMinterAndAwait, + computeGasLimitForMinter, isValidPrivateKey, isValidContractAddress, -} from '../utils'; +} from './utils'; +import { loadEnv } from './env'; -dotenv.config({ override: true }); +loadEnv('bridge'); -async function main() { - // Setup - const args = process.argv.slice(2); +export type BridgeSubmitConfig = { + usageCommand: string; + exampleTxHash: string; + minterAddressEnvVar: 'ASC_MINTER_CONTRACT_ADDRESS' | 'ASC_CUSTOM_MINTER_CONTRACT_ADDRESS'; +}; - if (args.length !== 1) { - console.error(` +export function printBridgeSubmitUsage(config: BridgeSubmitConfig): void { + console.error(` Usage: - yarn custom_bridge:submit_query + ${config.usageCommand} Example: - yarn custom_bridge:submit_query 0x87c97c776a678941b5941ec0cb602a4467ff4a35f77264208575f137cb05b2a7 + ${config.usageCommand} ${config.exampleTxHash} `); - process.exit(1); - } - - const [transactionHash] = args; +} - // Validate Transaction Hash +export async function runBridgeSubmitQuery(config: BridgeSubmitConfig, transactionHash: string): Promise { if (!transactionHash.startsWith('0x') || transactionHash.length !== 66) { throw new Error('Invalid transaction hash provided'); } - // Validate Private Key const ccNextPrivateKey = process.env.CREDITCOIN_WALLET_PRIVATE_KEY; if (!isValidPrivateKey(ccNextPrivateKey)) { throw new Error('CREDITCOIN_WALLET_PRIVATE_KEY environment variable is not configured or invalid'); @@ -55,20 +53,19 @@ async function main() { throw new Error('CREDITCOIN_RPC_URL environment variable is not configured or invalid'); } - const minterContractAddress = process.env.ASC_CUSTOM_MINTER_CONTRACT_ADDRESS; + const minterContractAddress = process.env[config.minterAddressEnvVar]; if (!isValidContractAddress(minterContractAddress)) { - throw new Error('ASC_CUSTOM_MINTER_CONTRACT_ADDRESS is not configured or invalid'); + throw new Error(`${config.minterAddressEnvVar} is not configured or invalid`); } + const sourceChainRpcUrl = process.env.SOURCE_CHAIN_RPC_URL; if (!sourceChainRpcUrl) { throw new Error('SOURCE_CHAIN_RPC_URL environment variable is not configured or invalid'); } - // 1. Initialize RPC providers const ccProvider = new ethers.JsonRpcProvider(creditcoinRpcUrl); const sourceChainProvider = new ethers.JsonRpcProvider(sourceChainRpcUrl); - // 2. Validate transaction and generate proof once the block is attested const proofResult = await generateProofFor( transactionHash, sourceChainKey, @@ -77,21 +74,15 @@ async function main() { sourceChainProvider ); - // 3. Using previously generated proof, submit to ASC minter and await for the minted event - if (proofResult.success) { - // Establish link with the ASC contract - const wallet = new ethers.Wallet(ccNextPrivateKey!, ccProvider); - const contractABI = ASCMinterABI as unknown as InterfaceAbi; - const minterContract = new Contract(minterContractAddress!, contractABI, wallet); - - const proofData = proofResult.data!; - const gasLimit = await computeGasLimitForMinter(ccProvider, minterContract, proofData, wallet.address); - await submitProofToMinterAndAwait(minterContract, proofData, gasLimit); - } else { + if (!proofResult.success) { throw new Error(`Failed to generate proof: ${proofResult.error}`); } - process.exit(0); -} + const wallet = new ethers.Wallet(ccNextPrivateKey!, ccProvider); + const contractABI = ASCMinterABI as unknown as InterfaceAbi; + const minterContract = new Contract(minterContractAddress!, contractABI, wallet); -main().catch(console.error); + const proofData = proofResult.data!; + const gasLimit = await computeGasLimitForMinter(ccProvider, minterContract, proofData, wallet.address); + await submitProofToMinterAndAwait(minterContract, proofData, gasLimit); +} diff --git a/utils/check_balance.js b/shared/utils/check_balance.js similarity index 92% rename from utils/check_balance.js rename to shared/utils/check_balance.js index 06fc540f..f1573c65 100644 --- a/utils/check_balance.js +++ b/shared/utils/check_balance.js @@ -1,6 +1,9 @@ const { ethers } = require('ethers'); -require('dotenv').config({ override: true }); +const { loadEnv } = require('../env'); + +// Shared by both tutorials, so read whichever per-tutorial .env files exist. +loadEnv('bridge', 'loan'); // === Check for arguments === if (process.argv.length < 4 || process.argv.length > 5) { diff --git a/utils/index.ts b/shared/utils/index.ts similarity index 88% rename from utils/index.ts rename to shared/utils/index.ts index e0007a1f..f65c4d25 100644 --- a/utils/index.ts +++ b/shared/utils/index.ts @@ -21,18 +21,14 @@ export async function generateProofFor( creditcoinRpc: JsonRpcApiProvider, sourceChainRpc: JsonRpcApiProvider ): Promise { - // First, we need to ensure that the transaction exists on the source chain - const transaction = await sourceChainRpc.getTransaction(txHash); - if (!transaction) { - throw new Error(`Transaction ${txHash} does not exist on source chain`); - } - - // Next, we need to ensure that the block is mined - const blockNumber = transaction.blockNumber; - if (!blockNumber) { + // Wait until the burn tx is mined (CI submits immediately after cast send). + console.log(`Waiting for transaction ${txHash} to be mined on source chain...`); + const receipt = await sourceChainRpc.waitForTransaction(txHash, 1, 120_000); + if (!receipt || receipt.blockNumber == null) { throw new Error(`Transaction ${txHash} is not yet mined on source chain`); } + const blockNumber = receipt.blockNumber; console.log(`Transaction ${txHash} found in block ${blockNumber}`); // Now that we have the block number, we can listen for the required attestation @@ -143,7 +139,7 @@ export async function computeGasLimitForMinter( proofData: proofProvider.ContinuityResponse, signerAddress: string ): Promise { - const action = 0; // Mint action (see MinterActions in ASCMinter) + const action = 0; // Mint action (see MinterActions in ASCMinter: Mint=0) const chainKey = proofData.chainKey; const height = proofData.headerNumber; const encodedTransaction = proofData.txBytes; @@ -250,7 +246,7 @@ export async function submitProofToMinter( proofData: proofProvider.ContinuityResponse, gasLimit: bigint ): Promise { - const action = 0; // Mint action (see MinterActions in ASCMinter) + const action = 0; // Mint action (see MinterActions in ASCMinter: Mint=0) const chainKey = proofData.chainKey; const height = proofData.headerNumber; const encodedTransaction = proofData.txBytes; @@ -336,6 +332,8 @@ export const POLLING_INTERVAL_MS = 5000; export const ERROR_BACKOFF_MS = 10000; // Maximum number of processed transactions to track before clearing export const MAX_PROCESSED_TXS = 1000; +// Hosted Sepolia RPCs often cap eth_getLogs (Google 400, others ~50 blocks). +export const MAX_LOG_BLOCK_RANGE = 50; // Helper function to poll for events using queryFilter (avoids filter expiration issues) export async function pollEvents( @@ -350,20 +348,41 @@ export async function pollEvents( return fromBlock; } - const events = await contract.queryFilter(eventName, fromBlock, currentBlock); - for (const event of events) { - if (event instanceof EventLog) { - await handler(event); + let start = fromBlock; + let chunkSize = MAX_LOG_BLOCK_RANGE; + + while (start <= currentBlock) { + const end = Math.min(start + chunkSize - 1, currentBlock); + try { + const events = await contract.queryFilter(eventName, start, end); + for (const event of events) { + if (event instanceof EventLog) { + await handler(event); + } + } + start = end + 1; + chunkSize = MAX_LOG_BLOCK_RANGE; + } catch (chunkError: any) { + const message = chunkError.shortMessage ?? chunkError.message ?? String(chunkError); + console.error(`Error polling ${eventName} events (blocks ${start}-${end}):`, message); + + if (chunkSize > 1) { + chunkSize = Math.max(1, Math.floor(chunkSize / 2)); + await new Promise((resolve) => setTimeout(resolve, ERROR_BACKOFF_MS)); + } else { + // Skip a bad block instead of retrying forever on the same range. + start = end + 1; + chunkSize = MAX_LOG_BLOCK_RANGE; + await new Promise((resolve) => setTimeout(resolve, ERROR_BACKOFF_MS)); + } } } - // Return next block to query from return currentBlock + 1; } catch (error: any) { - console.error(`Error polling ${eventName} events:`, error.shortMessage); - // Add backoff delay on error to avoid hammering the RPC + console.error(`Error polling ${eventName} events:`, error.shortMessage ?? error.message); await new Promise((resolve) => setTimeout(resolve, ERROR_BACKOFF_MS)); - return fromBlock; // Retry from same block on error + return fromBlock; } } diff --git a/tsconfig.json b/tsconfig.json index b3ec2195..7f6ab8de 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -10,7 +10,7 @@ "allowSyntheticDefaultImports": true, "resolveJsonModule": true }, - "exclude": ["node_modules", "**/__tests__/*", "dist"], + "exclude": ["node_modules", "**/__tests__/*", "dist", "vendor"], "ts-node": { "require": ["tsconfig-paths/register"], diff --git a/yarn.lock b/yarn.lock index 700ad763..109d1726 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4,154 +4,154 @@ "@adraffy/ens-normalize@1.11.1": version "1.11.1" - resolved "https://registry.yarnpkg.com/@adraffy/ens-normalize/-/ens-normalize-1.11.1.tgz#6c2d657d4b2dfb37f8ea811dcb3e60843d4ac24a" + resolved "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.11.1.tgz" integrity sha512-nhCBV3quEgesuf7c7KYfperqSS14T8bYuvJ8PcLJp6znkZpFc0AuW4qBtr8eKVyPPe/8RSr7sglCWPU5eaxwKQ== -"@esbuild/aix-ppc64@0.28.1": - version "0.28.1" - resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz#7a01a8d2ec2fbb2dac78adad09b0fa781e4082be" - integrity sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ== - -"@esbuild/android-arm64@0.28.1": - version "0.28.1" - resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz#b540a27d14e4afd058496a4dbec4d3f414db110a" - integrity sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg== - -"@esbuild/android-arm@0.28.1": - version "0.28.1" - resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.28.1.tgz#704bd297de6d762de54eabbeafbf55f6756abe2f" - integrity sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ== - -"@esbuild/android-x64@0.28.1": - version "0.28.1" - resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.28.1.tgz#d1cb166d34b0fbf0fe8ab460a5594f24a378701e" - integrity sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng== - -"@esbuild/darwin-arm64@0.28.1": - version "0.28.1" - resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz#1034b26457fc886368fe61bbd09f653f6afa8e54" - integrity sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q== - -"@esbuild/darwin-x64@0.28.1": - version "0.28.1" - resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz#65556a432a1e4d72032d8218c1932fcca1a49772" - integrity sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ== - -"@esbuild/freebsd-arm64@0.28.1": - version "0.28.1" - resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz#2e61e0592f9030d7e3dae18ee25ebc535918aef6" - integrity sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw== - -"@esbuild/freebsd-x64@0.28.1": - version "0.28.1" - resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz#c95ec289959ef8079c4dca817a1e2c4be66b9bd3" - integrity sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ== - -"@esbuild/linux-arm64@0.28.1": - version "0.28.1" - resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz#40b22175dda06182f3ee8141186c5ff304c4a717" - integrity sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g== - -"@esbuild/linux-arm@0.28.1": - version "0.28.1" - resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz#c09a0f67917592ac0de892a9be4d3814debd2a6c" - integrity sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ== - -"@esbuild/linux-ia32@0.28.1": - version "0.28.1" - resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz#a580f9c676797833891e519fc7a1337c8afd8db3" - integrity sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w== - -"@esbuild/linux-loong64@0.28.1": - version "0.28.1" - resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz#46452cf321dc7f9e91c2fa780a56bb56e79cd68b" - integrity sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg== - -"@esbuild/linux-mips64el@0.28.1": - version "0.28.1" - resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz#4211b3184dd6608f53dcb22e39f5d34ee08852c8" - integrity sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ== - -"@esbuild/linux-ppc64@0.28.1": - version "0.28.1" - resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz#697857c2a61cb9b0b6bb6652e40c1dc5e1ca8e5d" - integrity sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ== - -"@esbuild/linux-riscv64@0.28.1": - version "0.28.1" - resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz#d192943eb146a40ac4c6497d0cf7be35b986bf08" - integrity sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ== - -"@esbuild/linux-s390x@0.28.1": - version "0.28.1" - resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz#acea0356da0e0ebc08f97cf7b9c2e401e1e648dc" - integrity sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag== - -"@esbuild/linux-x64@0.28.1": - version "0.28.1" - resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz#6f0c3ce0cb64c534b70c4c45ecb2c16d34e35dfd" - integrity sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA== - -"@esbuild/netbsd-arm64@0.28.1": - version "0.28.1" - resolved "https://registry.yarnpkg.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz#8bcd77077a0dce3378b574fedb26d2a253b73d36" - integrity sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw== - -"@esbuild/netbsd-x64@0.28.1": - version "0.28.1" - resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz#e7fb2a01e99c830c94e6623cd9fefb4c8fb58347" - integrity sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg== - -"@esbuild/openbsd-arm64@0.28.1": - version "0.28.1" - resolved "https://registry.yarnpkg.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz#c52909372db8b86e2c55e05a8940033b5660a3b2" - integrity sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q== - -"@esbuild/openbsd-x64@0.28.1": - version "0.28.1" - resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz#c427b9be5a64c262ff9a7eb70b5fbbaadf446c6c" - integrity sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw== - -"@esbuild/openharmony-arm64@0.28.1": - version "0.28.1" - resolved "https://registry.yarnpkg.com/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz#dc9b147baca2e6c4b3c85571741ef4860a489097" - integrity sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg== - -"@esbuild/sunos-x64@0.28.1": - version "0.28.1" - resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz#ce866d12df13c15e4c99f073a3d466f6e0649b3a" - integrity sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ== - -"@esbuild/win32-arm64@0.28.1": - version "0.28.1" - resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz#7468e3692d01d629d5941e5d83817bb80f9e39b4" - integrity sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA== - -"@esbuild/win32-ia32@0.28.1": - version "0.28.1" - resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz#a5bc0063fb2bcab6d0ed63f2a1537958bc269ec6" - integrity sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg== - -"@esbuild/win32-x64@0.28.1": - version "0.28.1" - resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz#10064ee44f4347b90c9a02b446bbf80a91632b12" - integrity sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A== +"@esbuild/aix-ppc64@0.28.2": + version "0.28.2" + resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz#bf6e10303bcf2e7c686975fa52f937ec2728d8bc" + integrity sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ== + +"@esbuild/android-arm64@0.28.2": + version "0.28.2" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz#0c6246bc8d2c4d172aac2db3fb1190d72bd65504" + integrity sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A== + +"@esbuild/android-arm@0.28.2": + version "0.28.2" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.28.2.tgz#2d84ece6a4e2684d92be26ee13d42757d831c381" + integrity sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg== + +"@esbuild/android-x64@0.28.2": + version "0.28.2" + resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.28.2.tgz#fc38d4d6358d8dc1cf53f09f7589fe436eb64801" + integrity sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q== + +"@esbuild/darwin-arm64@0.28.2": + version "0.28.2" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz#f83afeeac1d7dac01c7a2fd012b3e451a0591fcc" + integrity sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw== + +"@esbuild/darwin-x64@0.28.2": + version "0.28.2" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz#510147c055a795588dbbe14fd6b1b8ad0a2f30de" + integrity sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw== + +"@esbuild/freebsd-arm64@0.28.2": + version "0.28.2" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz#093b9200ecf0b115ba4e5e248a7485c9c5f8bd5e" + integrity sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw== + +"@esbuild/freebsd-x64@0.28.2": + version "0.28.2" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz#0be22b6df925d213e841ea87123af5df80b0faf7" + integrity sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg== + +"@esbuild/linux-arm64@0.28.2": + version "0.28.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz#1bdbc651cda9ba9995c53ed9c71ceaa65094762d" + integrity sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug== + +"@esbuild/linux-arm@0.28.2": + version "0.28.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz#beb12ad72b84f72d28488cc1b8ee9f7eb141d753" + integrity sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w== + +"@esbuild/linux-ia32@0.28.2": + version "0.28.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz#b81f9d55529b45c206a46a138214b1aa6879696b" + integrity sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ== + +"@esbuild/linux-loong64@0.28.2": + version "0.28.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz#598667241a04c99b76ed6ef940ac50038c419f98" + integrity sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ== + +"@esbuild/linux-mips64el@0.28.2": + version "0.28.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz#1c51eb9cea903f53d97b5af3b1841db70f5596ca" + integrity sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA== + +"@esbuild/linux-ppc64@0.28.2": + version "0.28.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz#63dd61f17ceb31a81227f413feac8a71bc2c51f2" + integrity sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ== + +"@esbuild/linux-riscv64@0.28.2": + version "0.28.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz#3763b08fde5cf25ab1facb8e7752edfe45fbfc27" + integrity sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA== + +"@esbuild/linux-s390x@0.28.2": + version "0.28.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz#1a137ff293a82906eb3176385bd7e8e0e5cfb7cb" + integrity sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg== + +"@esbuild/linux-x64@0.28.2": + version "0.28.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz#268b36211c146ca54f8fe12c578a8d6ef8979485" + integrity sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ== + +"@esbuild/netbsd-arm64@0.28.2": + version "0.28.2" + resolved "https://registry.yarnpkg.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz#22571ad951d62bb6accc82d8d1fad5c8c1ac0ba1" + integrity sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw== + +"@esbuild/netbsd-x64@0.28.2": + version "0.28.2" + resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz#42fcc57297eb0a0ca3f5fc475291f4c1a3f7c0de" + integrity sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw== + +"@esbuild/openbsd-arm64@0.28.2": + version "0.28.2" + resolved "https://registry.yarnpkg.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz#9eb32af104ac3dacf4edca01f596664aab0c73ef" + integrity sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ== + +"@esbuild/openbsd-x64@0.28.2": + version "0.28.2" + resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz#febed2402d6088225e91f20fb4ce2522ad0a4efd" + integrity sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw== + +"@esbuild/openharmony-arm64@0.28.2": + version "0.28.2" + resolved "https://registry.yarnpkg.com/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz#85641c3d466428bfbccea5f21c26836663fef5ce" + integrity sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q== + +"@esbuild/sunos-x64@0.28.2": + version "0.28.2" + resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz#a736f9d8962481045fc4c3e54f5479f22c870fb4" + integrity sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g== + +"@esbuild/win32-arm64@0.28.2": + version "0.28.2" + resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz#ee5ab40fad186201b652a33f8a5eb149e9e42532" + integrity sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ== + +"@esbuild/win32-ia32@0.28.2": + version "0.28.2" + resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz#c40d28a6d99a127da6711f2afd74b11cb63b06a7" + integrity sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA== + +"@esbuild/win32-x64@0.28.2": + version "0.28.2" + resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz#b21affb804cc167c133d95f45b3a1dc1323b9a87" + integrity sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g== "@eslint-community/eslint-utils@^4.2.0", "@eslint-community/eslint-utils@^4.9.1": version "4.9.1" - resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz#4e90af67bc51ddee6cdef5284edf572ec376b595" + resolved "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz" integrity sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ== dependencies: eslint-visitor-keys "^3.4.3" "@eslint-community/regexpp@^4.12.2", "@eslint-community/regexpp@^4.6.1": version "4.12.2" - resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.12.2.tgz#bccdf615bcf7b6e8db830ec0b8d21c9a25de597b" + resolved "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz" integrity sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew== "@eslint/eslintrc@^2.1.4": version "2.1.4" - resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-2.1.4.tgz#388a269f0f25c1b6adc317b5a2c55714894c70ad" + resolved "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz" integrity sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ== dependencies: ajv "^6.12.4" @@ -166,17 +166,19 @@ "@eslint/js@8.57.1": version "8.57.1" - resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.57.1.tgz#de633db3ec2ef6a3c89e2f19038063e8a122e2c2" + resolved "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz" integrity sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q== -"@gluwa/usc-contracts@0.1.2": - version "0.1.2" - resolved "https://registry.yarnpkg.com/@gluwa/usc-contracts/-/usc-contracts-0.1.2.tgz#480ba30d2eb6f2b96f884ee6f20963c677b94d64" - integrity sha512-6GbBApjqhb6uWGzo6mLOZxH2RNq8iCOoRuYXn97gIQIaIHMk/pjQdDk2vJolX11TnhQbcrtoE/em/RDQyrJJdQ== +"@gluwa/asc-contracts@0.2.1": + version "0.2.1" + resolved "https://registry.yarnpkg.com/@gluwa/asc-contracts/-/asc-contracts-0.2.1.tgz#f88e9355c613f46073f626b7b04f8f3223480097" + integrity sha512-3/zVx8ElFewTB0Xy06xJ/uPYfvFI1usOe/ilxoM5SNT9Cf47ML/YW41Pq00NttxuOPLkh8HnRlPgD1n66ritLA== + dependencies: + "@openzeppelin/contracts" "5.1.0" "@gluwa/usc-sdk@0.18.0": version "0.18.0" - resolved "https://registry.yarnpkg.com/@gluwa/usc-sdk/-/usc-sdk-0.18.0.tgz#03d0bbcd00a9a9a6b91eb4e9565fc861ee8978da" + resolved "https://registry.npmjs.org/@gluwa/usc-sdk/-/usc-sdk-0.18.0.tgz" integrity sha512-iUEXPAp1gB/HDYkXad+StVJvs0Yz2ZIbzijc8SaBflGJ95hPnVahyY+tsP2PApswSQaX2uIBM7Cb0jpZ+KBzTQ== dependencies: axios "^1.13.2" @@ -186,7 +188,7 @@ "@humanwhocodes/config-array@^0.13.0": version "0.13.0" - resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.13.0.tgz#fb907624df3256d04b9aa2df50d7aa97ec648748" + resolved "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz" integrity sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw== dependencies: "@humanwhocodes/object-schema" "^2.0.3" @@ -195,29 +197,29 @@ "@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" + resolved "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz" integrity sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA== "@humanwhocodes/object-schema@^2.0.3": version "2.0.3" - resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz#4a2868d75d6d6963e423bcf90b7fd1be343409d3" + resolved "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz" integrity sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA== "@noble/curves@1.2.0": version "1.2.0" - resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.2.0.tgz#92d7e12e4e49b23105a2555c6984d41733d65c35" + resolved "https://registry.npmjs.org/@noble/curves/-/curves-1.2.0.tgz" integrity sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw== dependencies: "@noble/hashes" "1.3.2" "@noble/hashes@1.3.2": version "1.3.2" - resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.3.2.tgz#6f26dbc8fbc7205873ce3cee2f690eba0d421b39" + resolved "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.2.tgz" integrity sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ== "@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" + resolved "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz" integrity sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g== dependencies: "@nodelib/fs.stat" "2.0.5" @@ -225,39 +227,44 @@ "@nodelib/fs.stat@2.0.5": version "2.0.5" - resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz#5bd262af94e9d25bd1e71b05deed44876a222e8b" + resolved "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz" integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A== "@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" + resolved "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz" integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg== dependencies: "@nodelib/fs.scandir" "2.1.5" fastq "^1.6.0" +"@openzeppelin/contracts@5.1.0": + version "5.1.0" + resolved "https://registry.yarnpkg.com/@openzeppelin/contracts/-/contracts-5.1.0.tgz#4e61162f2a2bf414c4e10c45eca98ce5f1aadbd4" + integrity sha512-p1ULhl7BXzjjbha5aqst+QMLY+4/LCWADXOCsmLHRM77AqiPjnd9vvUN9sosUfhL9JGKpZ0TjEGxgvnizmWGSA== + "@openzeppelin/contracts@5.4.0": version "5.4.0" - resolved "https://registry.yarnpkg.com/@openzeppelin/contracts/-/contracts-5.4.0.tgz#177594bdb2d86c71f5d1052fe40cb4edb95fb20f" + resolved "https://registry.npmjs.org/@openzeppelin/contracts/-/contracts-5.4.0.tgz" integrity sha512-eCYgWnLg6WO+X52I16TZt8uEjbtdkgLC0SUX/xnAksjjrQI4Xfn4iBRoI5j55dmlOhDv1Y7BoR3cU7e3WWhC6A== "@types/node@22.7.5": version "22.7.5" - resolved "https://registry.yarnpkg.com/@types/node/-/node-22.7.5.tgz#cfde981727a7ab3611a481510b473ae54442b92b" + resolved "https://registry.npmjs.org/@types/node/-/node-22.7.5.tgz" integrity sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ== dependencies: undici-types "~6.19.2" "@types/node@26.1.0": version "26.1.0" - resolved "https://registry.yarnpkg.com/@types/node/-/node-26.1.0.tgz#aa85f0727fc5611347091c478341c63650903439" + resolved "https://registry.npmjs.org/@types/node/-/node-26.1.0.tgz" integrity sha512-O0A1G3xPGy4w7AgQdAQYUlQ+BKk2Oovw8eRpofyp5KdBZULnbe+WqaOVNrm705SHphCiG4XHsACrSmPu1f+Kgw== dependencies: undici-types "~8.3.0" "@typescript-eslint/eslint-plugin@8.62.1": version "8.62.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.62.1.tgz#1736dcdca6cae3359d818456a47d18b674761f7f" + resolved "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.62.1.tgz" integrity sha512-4EQM77WgVNxj7OkL/5b/D/xZsw00G577+UriYTC7JF5opcF3T2AuoeY7ueLaZgSVjSgCS6yOAJB5bRGLPSJUzA== dependencies: "@eslint-community/regexpp" "^4.12.2" @@ -271,7 +278,7 @@ "@typescript-eslint/parser@8.62.1": version "8.62.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.62.1.tgz#d3f7ba18f1bf78bfb7256fea021d1927b48e7080" + resolved "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.62.1.tgz" integrity sha512-sPhE4iHuJDSvoAiec+Ro8JyXw8f0ql13HFR82P99nCm9GwTEKG0KYLvDe6REk8BCXuit6vJAv/Yxg5ABaNS2rA== dependencies: "@typescript-eslint/scope-manager" "8.62.1" @@ -282,7 +289,7 @@ "@typescript-eslint/project-service@8.62.1": version "8.62.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/project-service/-/project-service-8.62.1.tgz#78d880eb1cf6859b5ec263d04f95403e9f90ae47" + resolved "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.62.1.tgz" integrity sha512-yQ3RgY5RkSBpsNS1Bx/JQEcA24FOSdfGktoyprAr5u18390UQdtVcfnEv4nIrIshNnavlVyZBKxQwT1fIAE6cg== dependencies: "@typescript-eslint/tsconfig-utils" "^8.62.1" @@ -291,7 +298,7 @@ "@typescript-eslint/scope-manager@8.62.1": version "8.62.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.62.1.tgz#7ee65e9a6eb3ccdc4816593a4ff38840306de88a" + resolved "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.62.1.tgz" integrity sha512-r4d249KbQ1SFdpeStvob8Ih6aPPIzfqllPVOtvhve6ZcpuVcYo5/7zUWckKpHE7StASX4kTKZTLf0WQm/wPkcg== dependencies: "@typescript-eslint/types" "8.62.1" @@ -299,12 +306,12 @@ "@typescript-eslint/tsconfig-utils@8.62.1", "@typescript-eslint/tsconfig-utils@^8.62.1": version "8.62.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.62.1.tgz#e2b5f24fe721044189cb7e81117c96d75979d627" + resolved "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.62.1.tgz" integrity sha512-xadytJqX9vJVQ2fdQjkcIVigwaOJNWkpjdLt6cEQ+xPnrI1fkp+/jZE/I97k9KUjqtpd25i0HeyZf3T6dutv2g== "@typescript-eslint/type-utils@8.62.1": version "8.62.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.62.1.tgz#ebd30b13bacb13070917259a23309cf644121f9a" + resolved "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.62.1.tgz" integrity sha512-aXM5xlqXiTxPibXB93cLAURfT3rlizf7uMXISCXy66Isr/9hISJx3yDsKl0L7lKa51b8JpFuNKby0/O0pEm9jg== dependencies: "@typescript-eslint/types" "8.62.1" @@ -315,12 +322,12 @@ "@typescript-eslint/types@8.62.1", "@typescript-eslint/types@^8.62.1": version "8.62.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.62.1.tgz#c58be954e483b2fc98275374d5bcb40b99842dc1" + resolved "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.62.1.tgz" integrity sha512-ooCzJFaf+Hg+uG6fA3NRFGuFjlfNlDhBthbv4ZPU/0elCAFUfnyXUvf/WOpHz/jYwSmvU2GkR2LtyUfy1AxZ1Q== "@typescript-eslint/typescript-estree@8.62.1": version "8.62.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.62.1.tgz#98c1bb17635d5b026b24193a8d29188ac64380ff" + resolved "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.62.1.tgz" integrity sha512-xMcW9oP9u7fAMXYs9A65CVmtLQe2r//oXINHfi8HV+oiqhih17sbLdhXr4540YWlgpDKQdY854OL5ZrdCiQsAA== dependencies: "@typescript-eslint/project-service" "8.62.1" @@ -335,7 +342,7 @@ "@typescript-eslint/utils@8.62.1": version "8.62.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.62.1.tgz#1622b75c7e6df308181dd0b44855dc4228da0457" + resolved "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.62.1.tgz" integrity sha512-sHtbPfuKNZCG+ih8SyjjucqRntSVmp8XgL5u6o9mAhiSn8ds5o/M/XdM0abweme2Tln3szOstOrZ9OXitvPh0g== dependencies: "@eslint-community/eslint-utils" "^4.9.1" @@ -345,7 +352,7 @@ "@typescript-eslint/visitor-keys@8.62.1": version "8.62.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.62.1.tgz#499657d77ffafb8a99eb1d6c97847ca430234722" + resolved "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.62.1.tgz" integrity sha512-4g3BLxfdTMy8iZG0MaBkadnlRrCJ74cQiFbyEVMrkwIoqdyaXXQM22cotDvrl4x28wgIZ9rEJRoM+mmhSJpJ1g== dependencies: "@typescript-eslint/types" "8.62.1" @@ -353,27 +360,27 @@ "@ungap/structured-clone@^1.2.0": version "1.3.0" - resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.3.0.tgz#d06bbb384ebcf6c505fde1c3d0ed4ddffe0aaff8" + resolved "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz" integrity sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g== acorn-jsx@^5.3.2: version "5.3.2" - resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" + resolved "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz" integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== acorn@^8.9.0: version "8.15.0" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.15.0.tgz#a360898bc415edaac46c8241f6383975b930b816" + resolved "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz" integrity sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg== 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" + resolved "https://registry.npmjs.org/aes-js/-/aes-js-4.0.0-beta.5.tgz" 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" + resolved "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz" integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== dependencies: fast-deep-equal "^3.1.1" @@ -383,29 +390,29 @@ ajv@^6.12.4: ansi-regex@^5.0.1: version "5.0.1" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" + resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz" integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== ansi-styles@^4.1.0: version "4.3.0" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" + resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz" integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== dependencies: color-convert "^2.0.1" argparse@^2.0.1: version "2.0.1" - resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" + resolved "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz" integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== asynckit@^0.4.0: version "0.4.0" - resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" + resolved "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz" integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q== axios@^1.13.2: version "1.16.0" - resolved "https://registry.yarnpkg.com/axios/-/axios-1.16.0.tgz#f8e5dd931cef2a5f8c32216d5784eda2f8750eb7" + resolved "https://registry.npmjs.org/axios/-/axios-1.16.0.tgz" integrity sha512-6hp5CwvTPlN2A31g5dxnwAX0orzM7pmCRDLnZSX772mv8WDqICwFjowHuPs04Mc8deIld1+ejhtaMn5vp6b+1w== dependencies: follow-redirects "^1.16.0" @@ -414,32 +421,32 @@ axios@^1.13.2: balanced-match@^1.0.0: version "1.0.2" - resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" + resolved "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz" integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== balanced-match@^4.0.2: version "4.0.4" - resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-4.0.4.tgz#bfb10662feed8196a2c62e7c68e17720c274179a" + resolved "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz" integrity sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA== brace-expansion@^1.1.7: version "1.1.12" - resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.12.tgz#ab9b454466e5a8cc3a187beaad580412a9c5b843" + resolved "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz" integrity sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg== dependencies: balanced-match "^1.0.0" concat-map "0.0.1" -brace-expansion@^5.0.2: - version "5.0.4" - resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-5.0.4.tgz#614daaecd0a688f660bbbc909a8748c3d80d4336" - integrity sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg== +brace-expansion@^5.0.8: + version "5.0.9" + resolved "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz" + integrity sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg== dependencies: balanced-match "^4.0.2" call-bind-apply-helpers@^1.0.1, call-bind-apply-helpers@^1.0.2: version "1.0.2" - resolved "https://registry.yarnpkg.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz#4b5428c222be985d79c3d82657479dbe0b59b2d6" + resolved "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz" integrity sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ== dependencies: es-errors "^1.3.0" @@ -447,12 +454,12 @@ call-bind-apply-helpers@^1.0.1, call-bind-apply-helpers@^1.0.2: callsites@^3.0.0: version "3.1.0" - resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" + resolved "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz" integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== chalk@^4.0.0: version "4.1.2" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" + resolved "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz" integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== dependencies: ansi-styles "^4.1.0" @@ -460,31 +467,31 @@ chalk@^4.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" + 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" color-name@~1.1.4: version "1.1.4" - resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" + resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== combined-stream@^1.0.8: version "1.0.8" - resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" + resolved "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz" integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== dependencies: delayed-stream "~1.0.0" concat-map@0.0.1: version "0.0.1" - resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + resolved "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz" integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== cross-spawn@^7.0.2: version "7.0.6" - resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f" + resolved "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz" integrity sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA== dependencies: path-key "^3.1.0" @@ -493,36 +500,36 @@ cross-spawn@^7.0.2: debug@^4.3.1, debug@^4.3.2, debug@^4.4.3: version "4.4.3" - resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.3.tgz#c6ae432d9bd9662582fce08709b038c58e9e3d6a" + resolved "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz" integrity sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA== dependencies: ms "^2.1.3" deep-is@^0.1.3: version "0.1.4" - resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831" + resolved "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz" integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ== delayed-stream@~1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" + resolved "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz" integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== doctrine@^3.0.0: version "3.0.0" - resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-3.0.0.tgz#addebead72a6574db783639dc87a121773973961" + resolved "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz" integrity sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w== dependencies: esutils "^2.0.2" dotenv@17.4.2, dotenv@^17.2.3: version "17.4.2" - resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-17.4.2.tgz#c07e54a746e11eba021dd9e1047ced5afdc1c034" + resolved "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz" integrity sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw== dunder-proto@^1.0.1: version "1.0.1" - resolved "https://registry.yarnpkg.com/dunder-proto/-/dunder-proto-1.0.1.tgz#d7ae667e1dc83482f8b70fd0f6eefc50da30f58a" + resolved "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz" integrity sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A== dependencies: call-bind-apply-helpers "^1.0.1" @@ -531,24 +538,24 @@ dunder-proto@^1.0.1: es-define-property@^1.0.1: version "1.0.1" - resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.1.tgz#983eb2f9a6724e9303f61addf011c72e09e0b0fa" + resolved "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz" integrity sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g== es-errors@^1.3.0: version "1.3.0" - resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f" + resolved "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz" integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== es-object-atoms@^1.0.0, es-object-atoms@^1.1.1: version "1.1.1" - resolved "https://registry.yarnpkg.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz#1c4f2c4837327597ce69d2ca190a7fdd172338c1" + resolved "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz" integrity sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA== dependencies: es-errors "^1.3.0" es-set-tostringtag@^2.1.0: version "2.1.0" - resolved "https://registry.yarnpkg.com/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz#f31dbbe0c183b00a6d26eb6325c810c0fd18bd4d" + resolved "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz" integrity sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA== dependencies: es-errors "^1.3.0" @@ -557,50 +564,50 @@ es-set-tostringtag@^2.1.0: hasown "^2.0.2" esbuild@~0.28.0: - version "0.28.1" - resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.28.1.tgz#ef45b4634c9c9d97a296aea4114a5f9840f95578" - integrity sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw== + version "0.28.2" + resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.28.2.tgz#0f43bd1bad955b72d24e2261e3abe5957ccf0816" + integrity sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA== optionalDependencies: - "@esbuild/aix-ppc64" "0.28.1" - "@esbuild/android-arm" "0.28.1" - "@esbuild/android-arm64" "0.28.1" - "@esbuild/android-x64" "0.28.1" - "@esbuild/darwin-arm64" "0.28.1" - "@esbuild/darwin-x64" "0.28.1" - "@esbuild/freebsd-arm64" "0.28.1" - "@esbuild/freebsd-x64" "0.28.1" - "@esbuild/linux-arm" "0.28.1" - "@esbuild/linux-arm64" "0.28.1" - "@esbuild/linux-ia32" "0.28.1" - "@esbuild/linux-loong64" "0.28.1" - "@esbuild/linux-mips64el" "0.28.1" - "@esbuild/linux-ppc64" "0.28.1" - "@esbuild/linux-riscv64" "0.28.1" - "@esbuild/linux-s390x" "0.28.1" - "@esbuild/linux-x64" "0.28.1" - "@esbuild/netbsd-arm64" "0.28.1" - "@esbuild/netbsd-x64" "0.28.1" - "@esbuild/openbsd-arm64" "0.28.1" - "@esbuild/openbsd-x64" "0.28.1" - "@esbuild/openharmony-arm64" "0.28.1" - "@esbuild/sunos-x64" "0.28.1" - "@esbuild/win32-arm64" "0.28.1" - "@esbuild/win32-ia32" "0.28.1" - "@esbuild/win32-x64" "0.28.1" + "@esbuild/aix-ppc64" "0.28.2" + "@esbuild/android-arm" "0.28.2" + "@esbuild/android-arm64" "0.28.2" + "@esbuild/android-x64" "0.28.2" + "@esbuild/darwin-arm64" "0.28.2" + "@esbuild/darwin-x64" "0.28.2" + "@esbuild/freebsd-arm64" "0.28.2" + "@esbuild/freebsd-x64" "0.28.2" + "@esbuild/linux-arm" "0.28.2" + "@esbuild/linux-arm64" "0.28.2" + "@esbuild/linux-ia32" "0.28.2" + "@esbuild/linux-loong64" "0.28.2" + "@esbuild/linux-mips64el" "0.28.2" + "@esbuild/linux-ppc64" "0.28.2" + "@esbuild/linux-riscv64" "0.28.2" + "@esbuild/linux-s390x" "0.28.2" + "@esbuild/linux-x64" "0.28.2" + "@esbuild/netbsd-arm64" "0.28.2" + "@esbuild/netbsd-x64" "0.28.2" + "@esbuild/openbsd-arm64" "0.28.2" + "@esbuild/openbsd-x64" "0.28.2" + "@esbuild/openharmony-arm64" "0.28.2" + "@esbuild/sunos-x64" "0.28.2" + "@esbuild/win32-arm64" "0.28.2" + "@esbuild/win32-ia32" "0.28.2" + "@esbuild/win32-x64" "0.28.2" 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" + resolved "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz" integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== eslint-config-prettier@^10.1.8: version "10.1.8" - resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-10.1.8.tgz#15734ce4af8c2778cc32f0b01b37b0b5cd1ecb97" + resolved "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.8.tgz" integrity sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w== eslint-scope@^7.2.2: version "7.2.2" - resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-7.2.2.tgz#deb4f92563390f32006894af62a22dba1c46423f" + resolved "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz" integrity sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg== dependencies: esrecurse "^4.3.0" @@ -608,17 +615,17 @@ eslint-scope@^7.2.2: 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" + resolved "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz" integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag== eslint-visitor-keys@^5.0.0: version "5.0.1" - resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz#9e3c9489697824d2d4ce3a8ad12628f91e9f59be" + resolved "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz" integrity sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA== eslint@^8.57.0: version "8.57.1" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.57.1.tgz#7df109654aba7e3bbe5c8eae533c5e461d3c6ca9" + resolved "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz" integrity sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA== dependencies: "@eslint-community/eslint-utils" "^4.2.0" @@ -662,7 +669,7 @@ eslint@^8.57.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" + resolved "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz" integrity sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ== dependencies: acorn "^8.9.0" @@ -671,31 +678,31 @@ espree@^9.6.0, espree@^9.6.1: esquery@^1.4.2: version "1.7.0" - resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.7.0.tgz#08d048f261f0ddedb5bae95f46809463d9c9496d" + resolved "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz" integrity sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g== 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" + resolved "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz" integrity sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag== dependencies: estraverse "^5.2.0" estraverse@^5.1.0, estraverse@^5.2.0: version "5.3.0" - resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123" + resolved "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz" 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" + resolved "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz" integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== ethers@^6.15.0, ethers@^6.17.0: version "6.17.0" - resolved "https://registry.yarnpkg.com/ethers/-/ethers-6.17.0.tgz#ad85ee1fc45fe2d95cb1e21c4e3ff9f7f1b093cc" + resolved "https://registry.npmjs.org/ethers/-/ethers-6.17.0.tgz" integrity sha512-BpyrpIPJ3ydEVow8zGaz1DuPS7YU8DcWxuBnY9a0UA/lvAPwrMr+EPXsfrul628SRaekPNeIM4UFh/91GWZang== dependencies: "@adraffy/ens-normalize" "1.11.1" @@ -708,46 +715,46 @@ ethers@^6.15.0, ethers@^6.17.0: exponential-backoff@^3.1.3: version "3.1.3" - resolved "https://registry.yarnpkg.com/exponential-backoff/-/exponential-backoff-3.1.3.tgz#51cf92c1c0493c766053f9d3abee4434c244d2f6" + resolved "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.3.tgz" integrity sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA== 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" + resolved "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz" integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== 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" + resolved "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz" 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" + resolved "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz" integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw== fastq@^1.6.0: version "1.20.1" - resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.20.1.tgz#ca750a10dc925bc8b18839fd203e3ef4b3ced675" + resolved "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz" integrity sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw== dependencies: reusify "^1.0.4" fdir@^6.5.0: version "6.5.0" - resolved "https://registry.yarnpkg.com/fdir/-/fdir-6.5.0.tgz#ed2ab967a331ade62f18d077dae192684d50d350" + resolved "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz" integrity sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg== 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" + resolved "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz" integrity sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg== dependencies: flat-cache "^3.0.4" find-up@^5.0.0: version "5.0.0" - resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc" + resolved "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz" integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng== dependencies: locate-path "^6.0.0" @@ -755,7 +762,7 @@ find-up@^5.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" + resolved "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz" integrity sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw== dependencies: flatted "^3.2.9" @@ -764,12 +771,12 @@ flat-cache@^3.0.4: flatted@^3.2.9: version "3.4.2" - resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.4.2.tgz#f5c23c107f0f37de8dbdf24f13722b3b98d52726" + resolved "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz" integrity sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA== follow-redirects@^1.16.0: version "1.16.0" - resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.16.0.tgz#28474a159d3b9d11ef62050a14ed60e4df6d61bc" + resolved "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz" integrity sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw== form-data@^4.0.5: @@ -785,7 +792,7 @@ form-data@^4.0.5: fs.realpath@^1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + resolved "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz" integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== fsevents@~2.3.3: @@ -795,12 +802,12 @@ fsevents@~2.3.3: function-bind@^1.1.2: version "1.1.2" - resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" + resolved "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz" integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== get-intrinsic@^1.2.6: version "1.3.0" - resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz#743f0e3b6964a93a5491ed1bffaae054d7f98d01" + resolved "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz" integrity sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ== dependencies: call-bind-apply-helpers "^1.0.2" @@ -816,7 +823,7 @@ get-intrinsic@^1.2.6: get-proto@^1.0.1: version "1.0.1" - resolved "https://registry.yarnpkg.com/get-proto/-/get-proto-1.0.1.tgz#150b3f2743869ef3e851ec0c49d15b1d14d00ee1" + resolved "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz" integrity sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g== dependencies: dunder-proto "^1.0.1" @@ -824,14 +831,14 @@ get-proto@^1.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" + resolved "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz" integrity sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A== dependencies: is-glob "^4.0.3" glob@^7.1.3: version "7.2.3" - resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" + resolved "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz" integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== dependencies: fs.realpath "^1.0.0" @@ -843,41 +850,41 @@ glob@^7.1.3: globals@^13.19.0: version "13.24.0" - resolved "https://registry.yarnpkg.com/globals/-/globals-13.24.0.tgz#8432a19d78ce0c1e833949c36adb345400bb1171" + resolved "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz" integrity sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ== dependencies: type-fest "^0.20.2" gopd@^1.2.0: version "1.2.0" - resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.2.0.tgz#89f56b8217bdbc8802bd299df6d7f1081d7e51a1" + resolved "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz" integrity sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg== graphemer@^1.4.0: version "1.4.0" - resolved "https://registry.yarnpkg.com/graphemer/-/graphemer-1.4.0.tgz#fb2f1d55e0e3a1849aeffc90c4fa0dd53a0e66c6" + resolved "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz" 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" + resolved "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz" integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== has-symbols@^1.0.3, has-symbols@^1.1.0: version "1.1.0" - resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.1.0.tgz#fc9c6a783a084951d0b971fe1018de813707a338" + resolved "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz" integrity sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ== has-tostringtag@^1.0.2: version "1.0.2" - resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz#2cdc42d40bef2e5b4eeab7c01a73c54ce7ab5abc" + resolved "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz" integrity sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw== dependencies: has-symbols "^1.0.3" hasown@^2.0.2: version "2.0.2" - resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003" + resolved "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz" integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ== dependencies: function-bind "^1.1.2" @@ -891,17 +898,17 @@ hasown@^2.0.4: ignore@^5.2.0: version "5.3.2" - resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.2.tgz#3cd40e729f3643fd87cb04e50bf0eb722bc596f5" + resolved "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz" integrity sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g== ignore@^7.0.5: version "7.0.5" - resolved "https://registry.yarnpkg.com/ignore/-/ignore-7.0.5.tgz#4cb5f6cd7d4c7ab0365738c7aea888baa6d7efd9" + resolved "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz" integrity sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg== import-fresh@^3.2.1: version "3.3.1" - resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.1.tgz#9cecb56503c0ada1f2741dbbd6546e4b13b57ccf" + resolved "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz" integrity sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ== dependencies: parent-module "^1.0.0" @@ -909,12 +916,12 @@ import-fresh@^3.2.1: imurmurhash@^0.1.4: version "0.1.4" - resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" + resolved "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz" integrity sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA== inflight@^1.0.4: version "1.0.6" - resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + resolved "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz" integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA== dependencies: once "^1.3.0" @@ -922,63 +929,63 @@ inflight@^1.0.4: inherits@2: version "2.0.4" - resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== is-extglob@^2.1.1: version "2.1.1" - resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" + resolved "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz" integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== is-glob@^4.0.0, is-glob@^4.0.3: version "4.0.3" - resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" + resolved "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz" integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== dependencies: is-extglob "^2.1.1" 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" + resolved "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz" integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ== isexe@^2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" + resolved "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz" integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== js-yaml@^4.1.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.3.0.tgz#d1900572a7f7cf0b5f540c83673e60bad3436592" - integrity sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q== + version "4.3.2" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.3.2.tgz#8e44fb14a2643c59726bb15787b5f1512cb3d3fb" + integrity sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA== 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" + resolved "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz" 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" + resolved "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz" 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" + resolved "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz" integrity sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw== keyv@^4.5.3: version "4.5.4" - resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.5.4.tgz#a879a99e29452f942439f2a405e3af8b31d4de93" + resolved "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz" 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" + resolved "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz" integrity sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ== dependencies: prelude-ls "^1.2.1" @@ -986,24 +993,24 @@ levn@^0.4.1: locate-path@^6.0.0: version "6.0.0" - resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-6.0.0.tgz#55321eb309febbc59c4801d931a72452a681d286" + resolved "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz" integrity sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw== dependencies: p-locate "^5.0.0" lodash.merge@^4.6.2: version "4.6.2" - resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" + resolved "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz" integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== math-intrinsics@^1.1.0: version "1.1.0" - resolved "https://registry.yarnpkg.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz#a0dd74be81e2aa5c2f27e65ce283605ee4e2b7f9" + resolved "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz" integrity sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g== mime-db@1.52.0: version "1.52.0" - resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" + resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz" integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== mime-types@^2.1.35: @@ -1014,39 +1021,39 @@ mime-types@^2.1.35: mime-db "1.52.0" minimatch@^10.2.2: - version "10.2.4" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-10.2.4.tgz#465b3accbd0218b8281f5301e27cedc697f96fde" - integrity sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg== + version "10.2.6" + resolved "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz" + integrity sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A== dependencies: - brace-expansion "^5.0.2" + brace-expansion "^5.0.8" 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" + resolved "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz" integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== dependencies: brace-expansion "^1.1.7" ms@^2.1.3: version "2.1.3" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" + resolved "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz" integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== natural-compare@^1.4.0: version "1.4.0" - resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" + resolved "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz" integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw== once@^1.3.0: version "1.4.0" - resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + resolved "https://registry.npmjs.org/once/-/once-1.4.0.tgz" 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" + resolved "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz" integrity sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g== dependencies: deep-is "^0.1.3" @@ -1058,156 +1065,156 @@ optionator@^0.9.3: p-limit@^3.0.2: version "3.1.0" - resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b" + resolved "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz" 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" + resolved "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz" integrity sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw== dependencies: p-limit "^3.0.2" parent-module@^1.0.0: version "1.0.1" - resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2" + resolved "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz" 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" + resolved "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz" 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" + resolved "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz" 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" + resolved "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz" integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== -picomatch@^4.0.3: - version "4.0.4" - resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-4.0.4.tgz#fd6f5e00a143086e074dffe4c924b8fb293b0589" - integrity sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A== +picomatch@^4.0.4: + version "4.0.7" + resolved "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz" + integrity sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA== prelude-ls@^1.2.1: version "1.2.1" - resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" + resolved "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz" integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== prettier@^3.9.4: - version "3.9.4" - resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.9.4.tgz#a9c477cf1614376bd1f6bbc593d8c0d414bcec87" - integrity sha512-yWG/o/4oJfo036EKAfK6ACAoDOfHeRHx4tuxkfBZiauURiaSmYwlpOr5LQqKtIkRD2z1PLteme2WoxEnj4tHTg== + version "3.9.6" + resolved "https://registry.npmjs.org/prettier/-/prettier-3.9.6.tgz" + integrity sha512-OpN0zzVdiaiAhxpuuj5efpIS4sY9j7bY6uR5mnj5yPzGkdkjNKSJeUThPb60Jw29QuAZgA4o+/iB49kFiaBX6g== proxy-from-env@^2.1.0: version "2.1.0" - resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-2.1.0.tgz#a7487568adad577cfaaa7e88c49cab3ab3081aba" + resolved "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz" integrity sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA== punycode@^2.1.0: version "2.3.1" - resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5" + resolved "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz" integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg== queue-microtask@^1.2.2: version "1.2.3" - resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" + resolved "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz" integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== resolve-from@^4.0.0: version "4.0.0" - resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" + resolved "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz" integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== reusify@^1.0.4: version "1.1.0" - resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.1.0.tgz#0fe13b9522e1473f51b558ee796e08f11f9b489f" + resolved "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz" integrity sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw== rimraf@^3.0.2: version "3.0.2" - resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" + resolved "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz" 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" + resolved "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz" integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA== dependencies: queue-microtask "^1.2.2" semver@^7.7.3: - version "7.7.3" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.3.tgz#4b5f4143d007633a8dc671cd0a6ef9147b8bb946" - integrity sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q== + version "7.8.5" + resolved "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz" + integrity sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA== shebang-command@^2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" + resolved "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz" 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" + resolved "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz" integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== strip-ansi@^6.0.1: version "6.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== dependencies: ansi-regex "^5.0.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" + resolved "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz" integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== supports-color@^7.1.0: version "7.2.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" + resolved "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz" 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" + resolved "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz" integrity sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw== tinyglobby@^0.2.15: - version "0.2.15" - resolved "https://registry.yarnpkg.com/tinyglobby/-/tinyglobby-0.2.15.tgz#e228dd1e638cea993d2fdb4fcd2d4602a79951c2" - integrity sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ== + version "0.2.17" + resolved "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz" + integrity sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g== dependencies: fdir "^6.5.0" - picomatch "^4.0.3" + picomatch "^4.0.4" ts-api-utils@^2.5.0: version "2.5.0" - resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-2.5.0.tgz#4acd4a155e22734990a5ed1fe9e97f113bcb37c1" + resolved "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz" integrity sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA== tslib@2.7.0: version "2.7.0" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.7.0.tgz#d9b40c5c40ab59e8738f297df3087bf1a2690c01" + resolved "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz" integrity sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA== tsx@^4.22.4: version "4.22.4" - resolved "https://registry.yarnpkg.com/tsx/-/tsx-4.22.4.tgz#0ab3b7fb4ec7feeee74e5b1f26337caa71e44700" + resolved "https://registry.npmjs.org/tsx/-/tsx-4.22.4.tgz" integrity sha512-X8EX+XV4QR5xCsrgxaED954zTDfY8KqlDtskKEL0cHhyS/P8b4IFOvGDQpsC9Q1XnLq915wEfwwY/zzskCtmhg== dependencies: esbuild "~0.28.0" @@ -1216,61 +1223,61 @@ tsx@^4.22.4: 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" + resolved "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz" integrity sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew== dependencies: prelude-ls "^1.2.1" type-fest@^0.20.2: version "0.20.2" - resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4" + resolved "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz" integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ== typescript@6.0.2: version "6.0.2" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-6.0.2.tgz#0b1bfb15f68c64b97032f3d78abbf98bdbba501f" + resolved "https://registry.npmjs.org/typescript/-/typescript-6.0.2.tgz" integrity sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ== undici-types@~6.19.2: version "6.19.8" - resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.19.8.tgz#35111c9d1437ab83a7cdc0abae2f26d88eda0a02" + resolved "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz" integrity sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw== undici-types@~8.3.0: version "8.3.0" - resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-8.3.0.tgz#44e9fc9f3244648cdea35e4f9bb2d681e9410809" + resolved "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz" integrity sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ== uri-js@^4.2.2: version "4.4.1" - resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e" + resolved "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz" integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg== dependencies: punycode "^2.1.0" which@^2.0.1: version "2.0.2" - resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" + resolved "https://registry.npmjs.org/which/-/which-2.0.2.tgz" 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" + resolved "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz" integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA== wrappy@1: version "1.0.2" - resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + resolved "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz" integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== ws@8.21.0: version "8.21.0" - resolved "https://registry.yarnpkg.com/ws/-/ws-8.21.0.tgz#012e413fc07429945121b0c153158c4343086951" + resolved "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz" integrity sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g== yocto-queue@^0.1.0: version "0.1.0" - resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" + resolved "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz" integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==