diff --git a/.gitignore b/.gitignore index d21b78a..0654391 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,5 @@ .DS_Store /tests/.bin -/tests/.latest.json \ No newline at end of file +/tests/.latest.json +CLAUDE.md diff --git a/abis/Prices/ERC20.json b/abis/Prices/PriceOracle.json similarity index 100% rename from abis/Prices/ERC20.json rename to abis/Prices/PriceOracle.json diff --git a/abis/spell4_2_0.json b/abis/spell4_2_0.json new file mode 100644 index 0000000..81e1df4 --- /dev/null +++ b/abis/spell4_2_0.json @@ -0,0 +1,232 @@ +[ + { + "inputs": [ + { + "internalType": "bool", + "name": "_mainnet", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "inputs": [], + "name": "TestError", + "type": "error" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "contract IRToken", + "name": "rToken", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "newGovernor", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "newTimelock", + "type": "address" + } + ], + "name": "NewGovernanceDeployed", + "type": "event" + }, + { + "inputs": [], + "name": "NEW_VERSION_HASH", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "PRIOR_VERSION_HASH", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract IERC20", + "name": "", + "type": "address" + } + ], + "name": "assets", + "outputs": [ + { + "internalType": "contract Asset", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract IRToken", + "name": "", + "type": "address" + } + ], + "name": "cast", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract IRToken", + "name": "rToken", + "type": "address" + }, + { + "internalType": "contract Governance", + "name": "oldGovernor", + "type": "address" + }, + { + "internalType": "address[]", + "name": "guardians", + "type": "address[]" + } + ], + "name": "castSpell", + "outputs": [ + { + "internalType": "address", + "name": "newGovernor", + "type": "address" + }, + { + "internalType": "address", + "name": "newTimelock", + "type": "address" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "deployer", + "outputs": [ + { + "internalType": "contract IDeployer", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "mainnet", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract IRToken", + "name": "", + "type": "address" + } + ], + "name": "newGovs", + "outputs": [ + { + "internalType": "contract IGovernor", + "name": "anastasius", + "type": "address" + }, + { + "internalType": "contract TimelockController", + "name": "timelock", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "registries", + "outputs": [ + { + "internalType": "contract VersionRegistry", + "name": "versionRegistry", + "type": "address" + }, + { + "internalType": "contract AssetPluginRegistry", + "name": "assetPluginRegistry", + "type": "address" + }, + { + "internalType": "contract DAOFeeRegistry", + "name": "daoFeeRegistry", + "type": "address" + }, + { + "internalType": "contract ITrustedFillerRegistry", + "name": "trustedFillerRegistry", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "version", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "pure", + "type": "function" + } +] diff --git a/networks.json b/networks.json index eefdc55..12951f5 100644 --- a/networks.json +++ b/networks.json @@ -4,9 +4,17 @@ "address": "0xFd6CC4F251eaE6d02f9F7B41D1e80464D3d2F377", "startBlock": 16680990 }, + "TestDeployer": { + "address": "0x8fcbd0baaeb442f1f3f374fcb63933e6d4cb8710", + "startBlock": 23992760 + }, "DeployerRegistry": { "address": "0xD85Fac03804a3e44D29c494f3761D11A2262cBBe", "startBlock": 16680990 + }, + "spell4_2_0": { + "address": "0xbFf761D367291281f3c4DB4Bda2C591d6DDE3601", + "startBlock": 23992880 } }, "base": { @@ -14,9 +22,17 @@ "address": "0x0000000000000000000000000000000000000000", "startBlock": 2000063551 }, + "TestDeployer": { + "address": "0x5705F85A05c8b57818663C7AB6a11f88323a1A57", + "startBlock": 39353460 + }, "DeployerRegistry": { "address": "0x1265Ec05FD621d82F224814902c925a600307fb3", "startBlock": 4521504 + }, + "spell4_2_0": { + "address": "0xB57DB893c95e50f67A62B8dcE411D8e06FF224E1", + "startBlock": 39354176 } }, "arbitrum-one": { @@ -27,6 +43,10 @@ "DeployerRegistry": { "address": "0x19927e8bF2dB907Bad30cc974dde2b13aC2FB65E", "startBlock": 205179115 + }, + "spell4_2_0": { + "address": "0x0000000000000000000000000000000000000000", + "startBlock": 2000063551 } } } diff --git a/package-lock.json b/package-lock.json index defb6d9..0589e13 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,10 +1,12 @@ { "name": "reserve", + "version": "4.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "reserve", + "version": "4.2.0", "license": "UNLICENSED", "dependencies": { "matchstick-as": "^0.6.0" diff --git a/package.json b/package.json index e1ba94e..f21e84f 100644 --- a/package.json +++ b/package.json @@ -1,14 +1,18 @@ { "name": "reserve", "license": "UNLICENSED", + "version": "4.2.0", "scripts": { "codegen": "graph codegen", "build": "graph build", - "deploy": "graph deploy --studio reserve", "create-local": "graph create --node http://127.0.0.1:8020/ lcamargof/reserve", "remove-local": "graph remove --node http://127.0.0.1:8020/ lcamargof/reserve", "deploy-local": "graph deploy --node http://127.0.0.1:8020/ --ipfs http://127.0.0.1:5001 lcamargof/reserve", - "deploy-testnet": "graph deploy --product hosted-service lcamargof/reserve-test" + "deploy:mainnet": "node scripts/deploy.js --network mainnet", + "deploy:base": "node scripts/deploy.js --network base", + "deploy:arbitrum": "node scripts/deploy.js --network arbitrum-one", + "deploy:all": "node scripts/deploy-all.js", + "deploy": "node scripts/deploy-interactive.js" }, "devDependencies": { "@graphprotocol/graph-cli": "0.56.0", diff --git a/schema.graphql b/schema.graphql index 02ee852..58f0de8 100644 --- a/schema.graphql +++ b/schema.graphql @@ -9,7 +9,7 @@ enum RewardTokenType { #### Deployer Metadata #### ########################### -type Deployer @entity { +type Deployer @entity(immutable: false) { " Smart contract address of the Deployer " id: ID! @@ -24,7 +24,7 @@ type Deployer @entity { ##### Protocol Metadata ##### ############################# -type Protocol @entity { +type Protocol @entity(immutable: false) { " Smart contract address of the protocol's main contract (Factory, Registry, etc) " id: ID! @@ -122,7 +122,7 @@ type Protocol @entity { ##### Protocol Timeseries ##### ############################### -type UsageMetricsDailySnapshot @entity { +type UsageMetricsDailySnapshot @entity(immutable: false) { " ID is # of days since Unix epoch time " id: ID! @@ -169,7 +169,7 @@ type UsageMetricsDailySnapshot @entity { timestamp: BigInt! } -type UsageMetricsHourlySnapshot @entity { +type UsageMetricsHourlySnapshot @entity(immutable: false) { " { # of hours since Unix epoch time } " id: ID! @@ -216,7 +216,7 @@ type UsageMetricsHourlySnapshot @entity { timestamp: BigInt! } -type FinancialsDailySnapshot @entity { +type FinancialsDailySnapshot @entity(immutable: false) { " ID is # of days since Unix epoch time " id: ID! @@ -267,7 +267,7 @@ type FinancialsDailySnapshot @entity { #### RToken-Level Data #### ########################### -type Collateral @entity { +type Collateral @entity(immutable: false) { " Collateral erc20 address " id: ID! @@ -275,7 +275,7 @@ type Collateral @entity { symbol: String! } -type RToken @entity { +type RToken @entity(immutable: false) { " RToken address " id: ID! @@ -395,7 +395,7 @@ type RToken @entity { historicalBaskets: [RTokenHistoricalBaskets!]! @derivedFrom(field: "rToken") } -type RTokenDailySnapshot @entity { +type RTokenDailySnapshot @entity(immutable: false) { " { Smart contract address of the rToken }-{ # of days since Unix epoch time } " id: ID! @@ -456,7 +456,7 @@ type RTokenDailySnapshot @entity { rsrPrice: BigDecimal! } -type RTokenHourlySnapshot @entity { +type RTokenHourlySnapshot @entity(immutable: false) { " { Smart contract address of the rToken }-{ # of hours since Unix epoch time } " id: ID! @@ -514,7 +514,7 @@ type RTokenHourlySnapshot @entity { cumulativeRSRRevenueUSD: BigDecimal! } -type Trade @entity { +type Trade @entity(immutable: false) { " { Smart contract address of the auction } " id: ID! @@ -574,7 +574,7 @@ type Trade @entity { ####### Token Metadata ###### ############################# -type RewardToken @entity { +type RewardToken @entity(immutable: false) { " { Reward token type }-{ Smart contract address of the reward token } " id: ID! @@ -588,7 +588,7 @@ type RewardToken @entity { type: RewardTokenType! } -type Token @entity { +type Token @entity(immutable: false) { " Smart contract address of the token " id: ID! @@ -658,7 +658,7 @@ type Token @entity { hourlyTokenSnapshot: [TokenHourlySnapshot!]! @derivedFrom(field: "token") } -type RTokenContract @entity { +type RTokenContract @entity(immutable: false) { " {Address of related contract} " id: ID! @@ -669,7 +669,7 @@ type RTokenContract @entity { rToken: RToken! } -type RevenueDistribution @entity { +type RevenueDistribution @entity(immutable: false) { " { Address Of the Account }-{ Address of the RToken } " id: ID! @@ -690,7 +690,7 @@ type RevenueDistribution @entity { ####### Token Timeseries ###### ############################### -type TokenDailySnapshot @entity { +type TokenDailySnapshot @entity(immutable: false) { " { Token Address }-{ # of days since Unix epoch time } " id: ID! @@ -740,7 +740,7 @@ type TokenDailySnapshot @entity { timestamp: BigInt! } -type TokenHourlySnapshot @entity { +type TokenHourlySnapshot @entity(immutable: false) { " { Token Address }-{ # of hours since Unix epoch time } " id: ID! @@ -820,7 +820,7 @@ interface Event { timestamp: BigInt! } -type Entry implements Event @entity { +type Entry implements Event @entity(immutable: false) { " { Token ID }-{ Transaction hash }-{ Log index } " id: ID! @@ -869,7 +869,7 @@ type Entry implements Event @entity { ################################## # An account is a unique Ethereum address -type Account @entity { +type Account @entity(immutable: false) { " Address of the account " id: ID! @@ -890,7 +890,7 @@ type Account @entity { @derivedFrom(field: "account") } -type AccountRToken @entity { +type AccountRToken @entity(immutable: false) { " { Address Of the Account }-{ Address of the rToken } " id: ID! @@ -933,7 +933,7 @@ type AccountRToken @entity { records: [AccountStakeRecord!]! @derivedFrom(field: "account") } -type AccountStakeRecord @entity { +type AccountStakeRecord @entity(immutable: false) { " { Address Of the Account }-{ Address of the rToken }-{ tx hash } " id: ID! @@ -974,7 +974,7 @@ type AccountStakeRecord @entity { timestamp: BigInt! } -type AccountBalance @entity { +type AccountBalance @entity(immutable: false) { " { Address Of the Account }-{ Address of the Token }" id: ID! @@ -998,7 +998,7 @@ type AccountBalance @entity { } # Helper entity for calculating daily/hourly active users -type ActiveAccount @entity { +type ActiveAccount @entity(immutable: false) { " { Address of the account }-{ [Optional] token address }-{ Days since Unix epoch }-{ [Optional] HH: hour of the day } " id: ID! } @@ -1007,7 +1007,7 @@ type ActiveAccount @entity { ###### Account Timeseries ##### ############################### -type AccountBalanceDailySnapshot @entity { +type AccountBalanceDailySnapshot @entity(immutable: false) { " { Address Of the Account }-{ Address of the Token }-{ # of hours since Unix epoch time } " id: ID! @@ -1033,7 +1033,7 @@ type AccountBalanceDailySnapshot @entity { timestamp: BigInt! } -type AccountRTokenDailySnapshot @entity { +type AccountRTokenDailySnapshot @entity(immutable: false) { " { Address Of the Account }-{ Address of the RToken }-{ # of hours since Unix epoch time } " id: ID! @@ -1115,7 +1115,7 @@ type DelegateVotingPowerChange @entity(immutable: true) { } # In theory this entity can be part of the RToken entity, but keeping it separate for now -type Governance @entity { +type Governance @entity(immutable: false) { " Address of the rToken " id: ID! @@ -1165,7 +1165,7 @@ type Governance @entity { proposalList: [Proposal!]! @derivedFrom(field: "governance") } -type GovernanceFramework @entity { +type GovernanceFramework @entity(immutable: false) { "Governance contract addresss" id: ID! "Name of the governance framework" @@ -1196,14 +1196,14 @@ type GovernanceFramework @entity { } " Match timelock id with proposal id " -type TimelockProposal @entity { +type TimelockProposal @entity(immutable: false) { " Timelock id " id: ID! " Proposal entity ID " proposalId: String! } -type Proposal @entity { +type Proposal @entity(immutable: false) { " Internal proposal ID (uint256) " id: ID! "Transaction hash of the proposal creation" @@ -1331,7 +1331,7 @@ enum VoteChoice { ABSTAIN } -type TokenHolder @entity { +type TokenHolder @entity(immutable: false) { "A TokenHolder is any address that holds any amount of tokens, the id used is the blockchain address." id: ID! "Holder address" @@ -1352,7 +1352,7 @@ type TokenHolder @entity { accountRToken: AccountRToken! } -type Delegate @entity { +type Delegate @entity(immutable: false) { "A Delegate is any address that has been delegated with voting tokens by a token holder, id is the blockchain address of said delegate" id: ID! @@ -1382,7 +1382,7 @@ type Delegate @entity { } # Timeseries Data -type stTokenDailySnapshot @entity { +type stTokenDailySnapshot @entity(immutable: false) { "Number of days from Unix epoch time" id: ID! "Total Supply at snapshot" @@ -1399,7 +1399,7 @@ type stTokenDailySnapshot @entity { governance: Governance! } -type VoteDailySnapshot @entity { +type VoteDailySnapshot @entity(immutable: false) { "Number of days from Unix epoch time" id: ID! "Proposal this snapshot is associated with" @@ -1418,7 +1418,7 @@ type VoteDailySnapshot @entity { timestamp: BigInt! } -type RTokenHistoricalBaskets @entity { +type RTokenHistoricalBaskets @entity(immutable: false) { " RToken address " id: ID! diff --git a/scripts/deploy-all.js b/scripts/deploy-all.js new file mode 100644 index 0000000..f13246f --- /dev/null +++ b/scripts/deploy-all.js @@ -0,0 +1,153 @@ +#!/usr/bin/env node + +const { spawn } = require("child_process"); +const fs = require("fs"); +const path = require("path"); + +// ANSI color codes +const colors = { + reset: "\x1b[0m", + bright: "\x1b[1m", + red: "\x1b[31m", + green: "\x1b[32m", + yellow: "\x1b[33m", + blue: "\x1b[34m", + cyan: "\x1b[36m", +}; + +// Parse command line arguments +const args = process.argv.slice(2); +const versionIndex = args.indexOf("--version"); + +// Get version from command line or package.json +let version; +if (versionIndex !== -1 && versionIndex !== args.length - 1) { + version = args[versionIndex + 1]; +} else { + const packageJson = JSON.parse( + fs.readFileSync(path.join(__dirname, "..", "package.json"), "utf8") + ); + version = packageJson.version; +} + +const networks = ["mainnet", "base", "arbitrum-one"]; +const results = []; + +// Display deployment information +console.log( + `${colors.cyan}${colors.bright}========================================${colors.reset}` +); +console.log( + `${colors.blue}${colors.bright}Deploying to All Networks${colors.reset}` +); +console.log( + `${colors.cyan}========================================${colors.reset}` +); +console.log(`${colors.yellow}Version:${colors.reset} ${version}`); +console.log(`${colors.yellow}Networks:${colors.reset} ${networks.join(", ")}`); +console.log( + `${colors.cyan}========================================${colors.reset}\n` +); + +function deployToNetwork(network) { + return new Promise((resolve) => { + console.log( + `${colors.blue}${colors.bright}Deploying to ${network}...${colors.reset}` + ); + console.log( + `${colors.cyan}----------------------------------------${colors.reset}` + ); + + const deployScript = path.join(__dirname, "deploy.js"); + const child = spawn( + "node", + [deployScript, "--network", network, "--version", version], + { + stdio: "inherit", + } + ); + + child.on("close", (code) => { + if (code === 0) { + results.push({ network, status: "success" }); + console.log( + `${colors.green}✓ ${network} deployment completed${colors.reset}\n` + ); + } else { + results.push({ network, status: "failed" }); + console.log( + `${colors.red}✗ ${network} deployment failed${colors.reset}\n` + ); + } + resolve(); + }); + + child.on("error", (err) => { + results.push({ network, status: "error", error: err.message }); + console.error( + `${colors.red}✗ ${network} deployment error: ${err.message}${colors.reset}\n` + ); + resolve(); + }); + }); +} + +async function deployAll() { + const startTime = Date.now(); + + // Deploy to each network sequentially + for (const network of networks) { + await deployToNetwork(network); + } + + const endTime = Date.now(); + const duration = Math.round((endTime - startTime) / 1000); + + // Display summary + console.log( + `${colors.cyan}${colors.bright}========================================${colors.reset}` + ); + console.log( + `${colors.blue}${colors.bright}Deployment Summary${colors.reset}` + ); + console.log( + `${colors.cyan}========================================${colors.reset}` + ); + + let successCount = 0; + let failedCount = 0; + + results.forEach((result) => { + const statusColor = result.status === "success" ? colors.green : colors.red; + const statusSymbol = result.status === "success" ? "✓" : "✗"; + console.log( + `${statusColor}${statusSymbol} ${result.network}: ${result.status}${colors.reset}` + ); + + if (result.status === "success") { + successCount++; + } else { + failedCount++; + } + }); + + console.log( + `${colors.cyan}----------------------------------------${colors.reset}` + ); + console.log(`${colors.yellow}Total time:${colors.reset} ${duration}s`); + console.log(`${colors.green}Successful:${colors.reset} ${successCount}`); + if (failedCount > 0) { + console.log(`${colors.red}Failed:${colors.reset} ${failedCount}`); + } + console.log( + `${colors.cyan}========================================${colors.reset}` + ); + + // Exit with error if any deployments failed + if (failedCount > 0) { + process.exit(1); + } +} + +// Run deployments +deployAll(); diff --git a/scripts/deploy-interactive.js b/scripts/deploy-interactive.js new file mode 100644 index 0000000..11f2937 --- /dev/null +++ b/scripts/deploy-interactive.js @@ -0,0 +1,143 @@ +#!/usr/bin/env node + +const readline = require("readline"); +const { spawn } = require("child_process"); +const fs = require("fs"); +const path = require("path"); + +// ANSI color codes +const colors = { + reset: "\x1b[0m", + bright: "\x1b[1m", + red: "\x1b[31m", + green: "\x1b[32m", + yellow: "\x1b[33m", + blue: "\x1b[34m", + cyan: "\x1b[36m", +}; + +const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout, +}); + +function prompt(question) { + return new Promise((resolve) => { + rl.question(question, (answer) => { + resolve(answer); + }); + }); +} + +async function selectNetwork() { + console.log( + `${colors.cyan}${colors.bright}========================================${colors.reset}` + ); + console.log( + `${colors.blue}${colors.bright}Interactive Subgraph Deployment${colors.reset}` + ); + console.log( + `${colors.cyan}========================================${colors.reset}\n` + ); + + console.log(`${colors.yellow}Available networks:${colors.reset}`); + console.log(" 1) mainnet"); + console.log(" 2) base"); + console.log(" 3) arbitrum-one"); + console.log(); + + const choice = await prompt( + `${colors.cyan}Select network (1-3): ${colors.reset}` + ); + + const networks = { + 1: "mainnet", + 2: "base", + 3: "arbitrum-one", + }; + + const network = networks[choice.trim()]; + + if (!network) { + console.error( + `${colors.red}Invalid selection. Please choose 1, 2, or 3.${colors.reset}` + ); + return selectNetwork(); + } + + return network; +} + +async function getVersion() { + const packageJson = JSON.parse( + fs.readFileSync(path.join(__dirname, "..", "package.json"), "utf8") + ); + const defaultVersion = packageJson.version; + + const version = await prompt( + `${colors.cyan}Enter version [${defaultVersion}]: ${colors.reset}` + ); + + return version.trim() || defaultVersion; +} + +async function deploy(network, version) { + console.log(); + console.log(`${colors.green}Starting deployment...${colors.reset}`); + console.log(); + + return new Promise((resolve, reject) => { + const deployScript = path.join(__dirname, "deploy.js"); + const child = spawn( + "node", + [deployScript, "--network", network, "--version", version], + { + stdio: "inherit", + } + ); + + child.on("close", (code) => { + if (code === 0) { + resolve(); + } else { + reject(new Error(`Deployment failed with code ${code}`)); + } + }); + + child.on("error", (err) => { + reject(err); + }); + }); +} + +async function main() { + try { + const network = await selectNetwork(); + const version = await getVersion(); + + console.log(); + console.log(`${colors.yellow}Deployment configuration:${colors.reset}`); + console.log(` ${colors.blue}Network:${colors.reset} ${network}`); + console.log(` ${colors.blue}Version:${colors.reset} ${version}`); + console.log(); + + const confirm = await prompt( + `${colors.cyan}Proceed with deployment? (y/n): ${colors.reset}` + ); + + if (confirm.toLowerCase() !== "y") { + console.log(`${colors.yellow}Deployment cancelled.${colors.reset}`); + rl.close(); + process.exit(0); + } + + await deploy(network, version); + rl.close(); + } catch (error) { + console.error(`${colors.red}Error: ${error.message}${colors.reset}`); + rl.close(); + process.exit(1); + } +} + +main(); diff --git a/scripts/deploy.js b/scripts/deploy.js new file mode 100644 index 0000000..b333e7a --- /dev/null +++ b/scripts/deploy.js @@ -0,0 +1,166 @@ +#!/usr/bin/env node + +const { spawn } = require("child_process"); +const fs = require("fs"); +const path = require("path"); + +// ANSI color codes +const colors = { + reset: "\x1b[0m", + bright: "\x1b[1m", + red: "\x1b[31m", + green: "\x1b[32m", + yellow: "\x1b[33m", + blue: "\x1b[34m", + cyan: "\x1b[36m", +}; + +// Parse command line arguments +const args = process.argv.slice(2); +const networkIndex = args.indexOf("--network"); +const versionIndex = args.indexOf("--version"); + +// Validate network argument +if (networkIndex === -1 || networkIndex === args.length - 1) { + console.error( + `${colors.red}Error: --network argument is required${colors.reset}` + ); + console.log( + "Usage: node scripts/deploy.js --network [--version ]" + ); + process.exit(1); +} + +const network = args[networkIndex + 1]; +const validNetworks = ["mainnet", "base", "arbitrum-one"]; + +if (!validNetworks.includes(network)) { + console.error( + `${colors.red}Error: Invalid network '${network}'${colors.reset}` + ); + console.log(`Valid networks: ${validNetworks.join(", ")}`); + process.exit(1); +} + +// Get version from command line or package.json +let version; +if (versionIndex !== -1 && versionIndex !== args.length - 1) { + version = args[versionIndex + 1]; +} else { + const packageJson = JSON.parse( + fs.readFileSync(path.join(__dirname, "..", "package.json"), "utf8") + ); + version = packageJson.version; +} + +const subgraphName = `dtf-yield-${network}`; + +// Display deployment information +console.log( + `${colors.cyan}${colors.bright}========================================${colors.reset}` +); +console.log( + `${colors.blue}${colors.bright}Deploying Subgraph to Goldsky${colors.reset}` +); +console.log( + `${colors.cyan}========================================${colors.reset}` +); +console.log(`${colors.yellow}Network:${colors.reset} ${network}`); +console.log(`${colors.yellow}Version:${colors.reset} ${version}`); +console.log( + `${colors.yellow}Subgraph:${colors.reset} ${subgraphName}/${version}` +); +console.log( + `${colors.cyan}========================================${colors.reset}\n` +); + +// Execute command with live output +function executeCommand(command, args, description) { + return new Promise((resolve, reject) => { + console.log(`${colors.yellow}⏳ ${description}...${colors.reset}`); + + const child = spawn(command, args, { + stdio: ["inherit", "pipe", "pipe"], + shell: process.platform === "win32", + }); + + child.stdout.on("data", (data) => { + process.stdout.write(data); + }); + + child.stderr.on("data", (data) => { + process.stderr.write(data); + }); + + child.on("close", (code) => { + if (code === 0) { + console.log( + `${colors.green}✓ ${description} completed successfully${colors.reset}\n` + ); + resolve(); + } else { + console.log( + `${colors.red}✗ ${description} failed with code ${code}${colors.reset}\n` + ); + reject(new Error(`Command failed with code ${code}`)); + } + }); + + child.on("error", (err) => { + console.error( + `${colors.red}✗ Failed to execute command: ${err.message}${colors.reset}` + ); + reject(err); + }); + }); +} + +async function deploy() { + try { + // Step 1: Run codegen + await executeCommand("npm", ["run", "codegen"], "Running codegen"); + + // Step 2: Build the subgraph for the network + await executeCommand( + "npx", + ["graph", "build", "--network", network], + `Building subgraph for ${network}` + ); + + // Step 3: Deploy to Goldsky + await executeCommand( + "goldsky", + ["subgraph", "deploy", `${subgraphName}/${version}`, "--path", "."], + `Deploying to Goldsky as ${subgraphName}/${version}` + ); + + console.log( + `${colors.green}${colors.bright}========================================${colors.reset}` + ); + console.log( + `${colors.green}${colors.bright}✓ Deployment completed successfully!${colors.reset}` + ); + console.log( + `${colors.green}========================================${colors.reset}` + ); + console.log( + `${colors.blue}Subgraph:${colors.reset} ${subgraphName}/${version}` + ); + console.log(`${colors.blue}Network:${colors.reset} ${network}`); + } catch (error) { + console.error( + `${colors.red}${colors.bright}========================================${colors.reset}` + ); + console.error( + `${colors.red}${colors.bright}✗ Deployment failed${colors.reset}` + ); + console.error( + `${colors.red}========================================${colors.reset}` + ); + console.error(`${colors.red}Error: ${error.message}${colors.reset}`); + process.exit(1); + } +} + +// Run deployment +deploy(); diff --git a/src/mappings/rToken.ts b/src/mappings/rToken.ts index d8885e5..f13aac0 100644 --- a/src/mappings/rToken.ts +++ b/src/mappings/rToken.ts @@ -182,7 +182,7 @@ export function handleRoleGranted(event: RoleGranted): void { timelockContract.rToken = rToken.id; timelockContract.name = ContractName.TIMELOCK; timelockContract.save(); - TimelockTemplate.create(event.params.account); + TimelockTemplate.create(timelockAddress); let governance = Governance.load(rTokenContract.rToken); let hasTimelock = false; @@ -195,11 +195,31 @@ export function handleRoleGranted(event: RoleGranted): void { log.error("Timelock has been changed", []); let network = dataSource.network(); + let timelockAddressHex = timelockAddress.toHexString(); - let governanceAddress = - SPELL_3_4_0_TIMELOCK_GOVERNANCE[network][ - timelockAddress.toHexString() - ]; + // Check if network exists in the spell map + if (!SPELL_3_4_0_TIMELOCK_GOVERNANCE.has(network)) { + // 4.2.0 spell or newer + return; + } + + let networkMap = SPELL_3_4_0_TIMELOCK_GOVERNANCE.get(network); + + if (!networkMap) { + return; + } + + // Check if timelock address exists in the network map + if (!networkMap.has(timelockAddressHex)) { + // 4.2.0 spell or newer + return; + } + + let governanceAddress = networkMap.get(timelockAddressHex); + + if (!governanceAddress) { + return; + } // Init governance let governorContract = new RTokenContract( diff --git a/src/mappings/spell4_2_0.ts b/src/mappings/spell4_2_0.ts new file mode 100644 index 0000000..b0595dc --- /dev/null +++ b/src/mappings/spell4_2_0.ts @@ -0,0 +1,44 @@ +import { NewGovernanceDeployed } from "../../generated/spell4_2_0/spell4_2_0"; +import { RToken, RTokenContract } from "../../generated/schema"; +import { + Governance as GovernanceTemplate, + Timelock as TimelockTemplate, +} from "../../generated/templates"; +import { ContractName } from "../common/constants"; +import { getGovernance } from "../governance/handlers"; +import { getGovernanceFramework } from "./governance"; + +export function handleNewGovernanceDeployed( + event: NewGovernanceDeployed +): void { + let rToken = RToken.load(event.params.rToken.toHexString()); + + if (!rToken) { + return; + } + + let governorAddress = event.params.newGovernor; + let timelockAddress = event.params.newTimelock; + + let governorContract = new RTokenContract(governorAddress.toHexString()); + governorContract.rToken = rToken.id; + governorContract.name = ContractName.GOVERNOR; + governorContract.save(); + + let timelockContract = new RTokenContract(timelockAddress.toHexString()); + timelockContract.rToken = rToken.id; + timelockContract.name = ContractName.TIMELOCK; + timelockContract.save(); + + GovernanceTemplate.create(governorAddress); + TimelockTemplate.create(timelockAddress); + + let governance = getGovernance(rToken.id); + governance.save(); + + getGovernanceFramework( + governorAddress.toHexString(), + event.block.number, + event.block.timestamp + ); +} diff --git a/subgraph.yaml b/subgraph.yaml index 72eb8ad..5f7dde7 100644 --- a/subgraph.yaml +++ b/subgraph.yaml @@ -4,11 +4,11 @@ schema: dataSources: - name: DeployerRegistry kind: ethereum/contract - network: mainnet + network: base source: abi: DeployerRegistry - address: "0xD85Fac03804a3e44D29c494f3761D11A2262cBBe" - startBlock: 16680990 + address: "0x1265Ec05FD621d82F224814902c925a600307fb3" + startBlock: 4521504 mapping: kind: ethereum/events apiVersion: 0.0.6 @@ -24,11 +24,11 @@ dataSources: file: ./src/mappings/deployerRegistry.ts - name: Deployer kind: ethereum/contract - network: mainnet + network: base source: abi: Deployer - address: "0xFd6CC4F251eaE6d02f9F7B41D1e80464D3d2F377" - startBlock: 16680990 + address: "0x0000000000000000000000000000000000000000" + startBlock: 2000063551 mapping: kind: ethereum/events apiVersion: 0.0.6 @@ -51,7 +51,7 @@ dataSources: - name: Facade file: ./abis/Facade.json - name: PriceOracleERC20 - file: ./abis/Prices/ERC20.json + file: ./abis/Prices/PriceOracle.json - name: ChainLinkContract file: ./abis/Prices/ChainLink.json - name: ChainLinkAggregator @@ -61,10 +61,76 @@ dataSources: address,string) handler: handleCreateToken file: ./src/mappings/deployer.ts + - name: TestDeployer + kind: ethereum/contract + network: base + source: + abi: Deployer + address: "0x5705F85A05c8b57818663C7AB6a11f88323a1A57" + startBlock: 39353460 + mapping: + kind: ethereum/events + apiVersion: 0.0.6 + language: wasm/assemblyscript + entities: + - Protocol + - RToken + - Token + - RewardToken + - Collateral + abis: + - name: RToken + file: ./abis/RToken.json + - name: Deployer + file: ./abis/Deployer.json + - name: Main + file: ./abis/Main.json + - name: ERC20 + file: ./abis/ERC20.json + - name: Facade + file: ./abis/Facade.json + - name: PriceOracleERC20 + file: ./abis/Prices/PriceOracle.json + - name: ChainLinkContract + file: ./abis/Prices/ChainLink.json + - name: ChainLinkAggregator + file: ./abis/Prices/ChainlinkAggregator.json + eventHandlers: + - event: RTokenCreated(indexed address,indexed address,address,indexed + address,string) + handler: handleCreateToken + file: ./src/mappings/deployer.ts + - name: spell4_2_0 + kind: ethereum/contract + network: base + source: + abi: spell4_2_0 + address: "0x908Cd3B4B4B6c60d5EB7d1Ca7ECda0e7ceCd6dB1" + startBlock: 39354176 + mapping: + kind: ethereum/events + apiVersion: 0.0.6 + language: wasm/assemblyscript + entities: + - RToken + - RTokenContract + - Governance + - GovernanceFramework + abis: + - name: spell4_2_0 + file: ./abis/spell4_2_0.json + - name: Governor + file: ./abis/Governor.json + - name: Timelock + file: ./abis/Timelock.json + eventHandlers: + - event: NewGovernanceDeployed(indexed address,indexed address,indexed address) + handler: handleNewGovernanceDeployed + file: ./src/mappings/spell4_2_0.ts templates: - name: Deployer kind: ethereum/contract - network: mainnet + network: base source: abi: Deployer mapping: @@ -89,7 +155,7 @@ templates: - name: Facade file: ./abis/Facade.json - name: PriceOracleERC20 - file: ./abis/Prices/ERC20.json + file: ./abis/Prices/PriceOracle.json - name: ChainLinkContract file: ./abis/Prices/ChainLink.json - name: ChainLinkAggregator @@ -101,7 +167,7 @@ templates: file: ./src/mappings/deployer.ts - name: RToken kind: ethereum/contract - network: mainnet + network: base source: abi: RToken mapping: @@ -123,7 +189,7 @@ templates: - name: ERC20 file: ./abis/ERC20.json - name: PriceOracleERC20 - file: ./abis/Prices/ERC20.json + file: ./abis/Prices/PriceOracle.json - name: ChainLinkContract file: ./abis/Prices/ChainLink.json - name: ChainLinkAggregator @@ -136,7 +202,7 @@ templates: file: ./src/mappings/rToken.ts - name: stRSR kind: ethereum/contract - network: mainnet + network: base source: abi: stRSR mapping: @@ -158,7 +224,7 @@ templates: - name: Facade file: ./abis/Facade.json - name: PriceOracleERC20 - file: ./abis/Prices/ERC20.json + file: ./abis/Prices/PriceOracle.json - name: ChainLinkContract file: ./abis/Prices/ChainLink.json - name: ChainLinkAggregator @@ -173,14 +239,16 @@ templates: - event: UnstakingStarted(indexed uint256,indexed uint256,indexed address,uint256,uint256,uint256) handler: handleUnstakeStarted - - event: UnstakingCompleted(indexed uint256,indexed uint256,uint256,indexed + - event: + UnstakingCompleted(indexed uint256,indexed uint256,uint256,indexed address,uint256) handler: handleUnstake - event: ExchangeRateSet(indexed uint192,indexed uint192) handler: handleExchangeRate - event: ExchangeRateSet(uint192,uint192) handler: handleExchangeRate - - event: UnstakingCancelled(indexed uint256,indexed uint256,uint256,indexed + - event: + UnstakingCancelled(indexed uint256,indexed uint256,uint256,indexed address,uint256) handler: handleUnstakeCancel - event: Transfer(indexed address,indexed address,uint256) @@ -188,7 +256,7 @@ templates: file: ./src/mappings/stRSR.ts - name: BasketHandler kind: ethereum/contract - network: mainnet + network: base source: abi: BasketHandler mapping: @@ -211,7 +279,7 @@ templates: file: ./src/mappings/rToken.ts - name: Main kind: ethereum/contract - network: mainnet + network: base source: abi: Main mapping: @@ -236,7 +304,7 @@ templates: file: ./src/mappings/rToken.ts - name: Timelock kind: ethereum/contract - network: mainnet + network: base source: abi: Timelock mapping: @@ -261,7 +329,7 @@ templates: file: ./src/mappings/governance.ts - name: Distributor kind: ethereum/contract - network: mainnet + network: base source: abi: Distributor mapping: @@ -275,7 +343,7 @@ templates: - name: Distributor file: ./abis/distributor.json - name: PriceOracleERC20 - file: ./abis/Prices/ERC20.json + file: ./abis/Prices/PriceOracle.json - name: Facade file: ./abis/Facade.json - name: ChainLinkContract @@ -294,7 +362,7 @@ templates: file: ./src/mappings/rToken.ts - name: RevenueTrader kind: ethereum/contract - network: mainnet + network: base source: abi: RevenueTrader mapping: @@ -323,7 +391,7 @@ templates: file: ./src/mappings/rToken.ts - name: BackingManager kind: ethereum/contract - network: mainnet + network: base source: abi: BackingManager mapping: @@ -352,7 +420,7 @@ templates: file: ./src/mappings/rToken.ts - name: Governance kind: ethereum/contract - network: mainnet + network: base source: abi: Governor mapping: @@ -397,7 +465,7 @@ templates: file: ./src/mappings/governance.ts - name: stRSRVotes kind: ethereum/contract - network: mainnet + network: base source: abi: stRSRVotes mapping: @@ -423,7 +491,7 @@ templates: - name: Facade file: ./abis/Facade.json - name: PriceOracleERC20 - file: ./abis/Prices/ERC20.json + file: ./abis/Prices/PriceOracle.json - name: ChainLinkContract file: ./abis/Prices/ChainLink.json - name: ChainLinkAggregator