diff --git a/queries/foundry.toml b/queries/foundry.toml new file mode 100644 index 0000000..d551a11 --- /dev/null +++ b/queries/foundry.toml @@ -0,0 +1,13 @@ +[profile.default] +src = "src" +out = "out" +libs = ["dependencies"] + +[fs_permissions] +read = ["./dependencies/sxt-proof-of-sql-contracts-0.2.1/config"] + +[dependencies] +forge-std = "1.9.7" +"@openzeppelin-contracts" = "5.2.0" +sxt-proof-of-sql-contracts = { version = "0.2.1", url = "https://github.com/spaceandtimefdn/sxt-proof-of-sql-contracts/releases/download/v0.2.1/posql-contracts.zip"} +# See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options diff --git a/queries/remappings.txt b/queries/remappings.txt new file mode 100644 index 0000000..8147b32 --- /dev/null +++ b/queries/remappings.txt @@ -0,0 +1,3 @@ +@openzeppelin-contracts/=dependencies/@openzeppelin-contracts-5.2.0/ +forge-std/=dependencies/forge-std-1.9.7/ +sxt-proof-of-sql-contracts/=dependencies/sxt-proof-of-sql-contracts-0.2.1/ diff --git a/queries/script/deployPoSQLCats.s.sol b/queries/script/deployPoSQLCats.s.sol new file mode 100644 index 0000000..af72e97 --- /dev/null +++ b/queries/script/deployPoSQLCats.s.sol @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.28; + +import {Script} from "forge-std/src/Script.sol"; +import {stdJson} from "forge-std/src/StdJson.sol"; +import {PoSQLCats} from "../src/PoSQLCats.sol"; + +/// @title Deploy PoSQLCats +/// @notice Reads per-chain payout from config/examples.addresses.json (or ENV override) and deploys. +/// +/// ## How to Run +/// `forge script script/deployPoSQLCats.s.sol:DeployPoSQLCats --broadcast --rpc-url=$ETH_RPC_URL --private-key=$PRIVATE_KEY --verify -vvvvv` +contract DeployPoSQLCats is Script { + using stdJson for string; + + function run() public { + // pick chain id from the RPC you pass to forge, or override via env + uint256 chainId = block.chainid; + + string memory path = string.concat(vm.projectRoot(), "/config/examples.addresses.json"); + /// forge-lint: disable-start(unsafe-cheatcode) + string memory json = vm.readFile(path); + + // Build JSON pointer like ".11155111.QUERY_ROUTER_ADDRESS" + string memory base = string.concat(".", vm.toString(chainId)); + address queryRouter = json.readAddress(string.concat(base, ".QUERY_ROUTER_ADDRESS")); + bytes32 version = json.readBytes32(string.concat(base, ".PROOF_OF_SQL_VERSION_HASH")); + address sxt = json.readAddress(string.concat(base, ".SXT_TOKEN_CONTRACT_ADDRESS")); + uint248 amount = uint248(uint256(json.readUint(string.concat(base, ".PAYMENT_AMOUNT")))); + + vm.startBroadcast(); // uses PRIVATE_KEY from env or CLI + new PoSQLCats(queryRouter, version, sxt, amount); + vm.stopBroadcast(); + } +} diff --git a/queries/script/deployPoSQLHelloWorld.s.sol b/queries/script/deployPoSQLHelloWorld.s.sol new file mode 100644 index 0000000..580a198 --- /dev/null +++ b/queries/script/deployPoSQLHelloWorld.s.sol @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.28; + +import {Script} from "forge-std/src/Script.sol"; +import {stdJson} from "forge-std/src/StdJson.sol"; +import {PoSQLHelloWorld} from "../src/PoSQLHelloWorld.sol"; + +/// @title Deploy PoSQLHelloWorld +/// @notice Reads per-chain payout from config/examples.addresses.json (or ENV override) and deploys. +/// +/// ## How to Run +/// `forge script script/deployPoSQLHelloWorld.s.sol:DeployPoSQLHelloWorld --broadcast --rpc-url=$ETH_RPC_URL --private-key=$PRIVATE_KEY --verify -vvvvv` +contract DeployPoSQLHelloWorld is Script { + using stdJson for string; + + function run() public { + // pick chain id from the RPC you pass to forge, or override via env + uint256 chainId = block.chainid; + + string memory path = string.concat(vm.projectRoot(), "/config/examples.addresses.json"); + /// forge-lint: disable-start(unsafe-cheatcode) + string memory json = vm.readFile(path); + + // Build JSON pointer like ".11155111.QUERY_ROUTER_ADDRESS" + string memory base = string.concat(".", vm.toString(chainId)); + address queryRouter = json.readAddress(string.concat(base, ".QUERY_ROUTER_ADDRESS")); + bytes32 version = json.readBytes32(string.concat(base, ".PROOF_OF_SQL_VERSION_HASH")); + address sxt = json.readAddress(string.concat(base, ".SXT_TOKEN_CONTRACT_ADDRESS")); + uint248 amount = uint248(uint256(json.readUint(string.concat(base, ".PAYMENT_AMOUNT")))); + + vm.startBroadcast(); // uses PRIVATE_KEY from env or CLI + new PoSQLHelloWorld(queryRouter, version, sxt, amount); + vm.stopBroadcast(); + } +} diff --git a/queries/script/deployPoSQLTimeStampWithInequality.s.sol b/queries/script/deployPoSQLTimeStampWithInequality.s.sol new file mode 100644 index 0000000..846cf8f --- /dev/null +++ b/queries/script/deployPoSQLTimeStampWithInequality.s.sol @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.28; + +import {Script} from "forge-std/src/Script.sol"; +import {stdJson} from "forge-std/src/StdJson.sol"; +import {PoSQLTimeStampWithInequality} from "../src/PoSQLTimeStampWithInequality.sol"; + +/// @title Deploy PoSQLHelloWorld +/// @notice Reads per-chain payout from config/examples.addresses.json (or ENV override) and deploys. +/// +/// ## How to Run +/// `forge script script/deployPoSQLTimeStampWithInequality.s.sol:DeployPoSQLTimeStampWithInequality --broadcast --rpc-url=$ETH_RPC_URL --private-key=$PRIVATE_KEY --verify -vvvvv` +contract DeployPoSQLTimeStampWithInequality is Script { + using stdJson for string; + + function run() public { + // pick chain id from the RPC you pass to forge, or override via env + uint256 chainId = block.chainid; + + string memory path = string.concat(vm.projectRoot(), "/config/examples.addresses.json"); + /// forge-lint: disable-start(unsafe-cheatcode) + string memory json = vm.readFile(path); + + // Build JSON pointer like ".11155111.QUERY_ROUTER_ADDRESS" + string memory base = string.concat(".", vm.toString(chainId)); + address queryRouter = json.readAddress(string.concat(base, ".QUERY_ROUTER_ADDRESS")); + bytes32 version = json.readBytes32(string.concat(base, ".PROOF_OF_SQL_VERSION_HASH")); + address sxt = json.readAddress(string.concat(base, ".SXT_TOKEN_CONTRACT_ADDRESS")); + uint248 amount = uint248(uint256(json.readUint(string.concat(base, ".PAYMENT_AMOUNT")))); + + vm.startBroadcast(); // uses PRIVATE_KEY from env or CLI + new PoSQLTimeStampWithInequality(queryRouter, version, sxt, amount); + vm.stopBroadcast(); + } +} diff --git a/queries/src/PoSQLCats.sol b/queries/src/PoSQLCats.sol new file mode 100644 index 0000000..a51992b --- /dev/null +++ b/queries/src/PoSQLCats.sol @@ -0,0 +1,97 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.28; + +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import {ParamsBuilder, ProofOfSqlTable} from "sxt-proof-of-sql-contracts/src/PoSQL.sol"; +import {IQueryCallback} from "sxt-proof-of-sql-contracts/src/IQueryCallback.sol"; +import {IQueryRouter} from "sxt-proof-of-sql-contracts/src/query-router/interfaces/IQueryRouter.sol"; + +/// @title PoSQLCats +/// @notice Example contract demonstrating a Proof of SQL query with parameterized filters. +/// @dev This contract uses QueryRouter to pay for and execute the query. +/// the query is a SQL query that returns cats filtered by age and gender. +/// note: make sure that the caller holds SXT enough to cover the query payment. Also this is a simple +/// example and does not include zero address checks and other security checks. + +/// @dev to deploy this contract, use the DeployPoSQLCats script +contract PoSQLCats is IQueryCallback { + using SafeERC20 for IERC20; + + /// @notice QueryRouter contract address + address public immutable QUERY_ROUTER; + /// @notice Proof of SQL version hash + bytes32 public immutable VERSION; + /// @notice SXT token address + address public immutable SXT; + /// @notice The ammount of SXT to pay for the query + uint256 public immutable PAYMENT_AMOUNT; + + uint256 public constant MAX_GAS_PRICE = 1e6; + uint64 public constant GAS_LIMIT = 1e6; + + /// @dev hex-serialized SQL query plan: + /// select * from CATS_C0A95B8A7EBCAFF89436492A205E98D2E3277B37.CATS WHERE age > $1 and is_female = $2 + bytes public constant QUERY_PLAN = + hex"00000000000000010000000000000032434154535f433041393542384137454243414646383934333634393241323035453938443245333237374233372e434154530000000000000004000000000000000000000000000000044e414d450000000700000000000000000000000000000003414745000000050000000000000000000000000000000949535f46454d414c45000000000000000000000000000000000000000e4d4554415f5355424d49545445520000000b000000000000000400000000000000044e414d450000000000000003414745000000000000000949535f46454d414c45000000000000000e4d4554415f5355424d4954544552000000000000000000000000000000060000000a0000000000000000000000010000000b00000000000000000000000500000000020000000000000000000000020000000b0000000000000001000000000000000000000004000000000000000000000000000000000000000000000001000000000000000000000002000000000000000000000003"; + + constructor(address queryRouter, bytes32 version, address sxt, uint248 amount) { + QUERY_ROUTER = queryRouter; + VERSION = version; + SXT = sxt; + PAYMENT_AMOUNT = amount; + } + + /// @notice Pay for and execute a query to get cats by age and gender. + /// @param minAge The minimum age for cats to return (age > minAge) + /// @param isFemale Whether to filter for female cats + function query(int64 minAge, bool isFemale) external { + // 0. pull SXT from the caller (must have approved this contract beforehand) + IERC20(SXT).safeTransferFrom(msg.sender, address(this), PAYMENT_AMOUNT); + + // 1. approve payment to be spent by QueryRouter, that will cover PoSQLVerifier's fees and fulfillment callback gas. + // Note: make sure that the caller address holds at least `PAYMENT_AMOUNT` of SXT. + IERC20(SXT).forceApprove(QUERY_ROUTER, PAYMENT_AMOUNT); // use forceApprove to handle nonzero allowances + + // 2. Assemble the SQL parameters using the `ParamsBuilder` library + bytes memory ageParam = ParamsBuilder.bigIntParam(minAge); + bytes memory genderParam = ParamsBuilder.boolParam(isFemale); + bytes[] memory queryParameters = new bytes[](2); + queryParameters[0] = ageParam; + queryParameters[1] = genderParam; + bytes memory serializedParams = ParamsBuilder.serializeParamArray(queryParameters); + + // 3. Assemble the request needed to run the query + IQueryRouter.Query memory queryToRequest = IQueryRouter.Query({ + version: VERSION, innerQuery: QUERY_PLAN, parameters: serializedParams, metadata: hex"" + }); + + // 4. Assemble the information needed to call the callback + IQueryRouter.Callback memory callback = IQueryRouter.Callback({ + maxGasPrice: MAX_GAS_PRICE, + gasLimit: GAS_LIMIT, + callbackContract: address(this), + selector: IQueryCallback.queryCallback.selector, + callbackData: "" + }); + + // 5. Execute the query. + IQueryRouter(QUERY_ROUTER) // aderyn-ignore unchecked-return + .requestQuery(queryToRequest, callback, PAYMENT_AMOUNT, uint64(block.timestamp + 1 hours)); + } + + /// @notice Example event emitting the query result. + event QueryFulfilled(uint256 catCount); + + /// @inheritdoc IQueryCallback + /// @notice Handle the query result. + /// @dev This will be called once the query has been executed and verified on-chain. + function queryCallback(bytes32, bytes calldata queryResult, bytes calldata) external { + // Use the `ProofOfSqlTable` to deserialize and read data from the result + (, ProofOfSqlTable.Table memory tableResult) = ProofOfSqlTable.__deserializeFromBytes(queryResult); + // The query returns 4 columns: NAME, AGE, IS_FEMALE, META_SUBMITTER + uint256 catCount = ProofOfSqlTable.readVarCharColumn(tableResult, 0).length; + // Emit the count. + emit QueryFulfilled(catCount); + } +} diff --git a/queries/src/PoSQLHelloWorld.sol b/queries/src/PoSQLHelloWorld.sol new file mode 100644 index 0000000..d30f9ca --- /dev/null +++ b/queries/src/PoSQLHelloWorld.sol @@ -0,0 +1,93 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.28; + +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import {ParamsBuilder, ProofOfSqlTable} from "sxt-proof-of-sql-contracts/src/PoSQL.sol"; +import {IQueryCallback} from "sxt-proof-of-sql-contracts/src/IQueryCallback.sol"; +import {IQueryRouter} from "sxt-proof-of-sql-contracts/src/query-router/interfaces/IQueryRouter.sol"; + +/// @title PoSQLHelloWorld +/// @notice Example "Hello World"-style contract of a Proof of SQL query. +/// @dev This contract uses QueryRouter to pay for and execute the query. +/// the query is a simple SQL query that returns the number of ethereum contracts created by msg.sender. +/// note: make sure that this contract holds SXT enough to cover the query payment. Also this is a simple +/// example and does not include zero address checks and other security checks. + +/// @dev to deploy this contract, use the DeployPoSQLHelloWorld script: +/// forge script script/deployPoSQLHelloWorld.s.sol:DeployPoSQLHelloWorld --broadcast --rpc-url=$ETH_RPC_URL --private-key=$PRIVATE_KEY --verify -vvvvv +contract PoSQLHelloWorld is IQueryCallback { + using SafeERC20 for IERC20; + + /// @notice QueryRouter contract address + address public immutable QUERY_ROUTER; + /// @notice Proof of SQL version hash + bytes32 public immutable VERSION; + /// @notice SXT token address + address public immutable SXT; + /// @notice The ammount of SXT to pay for the query + uint256 public immutable PAYMENT_AMOUNT; + + uint256 public constant MAX_GAS_PRICE = 1e6; + uint64 public constant GAS_LIMIT = 1e6; + + /// @dev hex-serialized SQL query plan: + /// SELECT BLOCK_NUMBER FROM ETHEREUM.CONTRACTS WHERE CONTRACT_CREATOR_ADDRESS=$1 + bytes public constant QUERY_PLAN = + hex"00000000000000010000000000000012455448455245554d2e434f4e54524143545300000000000000020000000000000000000000000000000c424c4f434b5f4e554d4245520000000500000000000000000000000000000018434f4e54524143545f43524541544f525f414444524553530000000b0000000000000001000000000000000c424c4f434b5f4e554d424552000000000000000000000000000000020000000000000000000000010000000b00000000000000000000000b0000000000000001000000000000000000000000"; + + constructor(address queryRouter, bytes32 version, address sxt, uint248 amount) { + QUERY_ROUTER = queryRouter; + VERSION = version; + SXT = sxt; + PAYMENT_AMOUNT = amount; + } + + /// @notice Pay for and execute a "Hello World"-style query. + function query() external { + // 0. pull SXT from the caller (must have approved this contract beforehand) + IERC20(SXT).safeTransferFrom(msg.sender, address(this), PAYMENT_AMOUNT); + + // 1. approve payment to be spent by QueryRouter, that will cover PoSQLVerifier's fees and fulfillment callback gas. + // Note: make sure that the caller address holds at least `PAYMENT_AMOUNT` of SXT. + IERC20(SXT).forceApprove(QUERY_ROUTER, PAYMENT_AMOUNT); // use forceApprove to handle nonzero allowances + + // 2. Assemble the SQL parameters using the `ParamsBuilder` library + bytes memory param = ParamsBuilder.varBinaryParam(abi.encodePacked(msg.sender)); + bytes[] memory queryParameters = new bytes[](1); + queryParameters[0] = param; + bytes memory serializedParams = ParamsBuilder.serializeParamArray(queryParameters); + + // 3. Assemble the request needed to run the query + IQueryRouter.Query memory queryToRequest = IQueryRouter.Query({ + version: VERSION, innerQuery: QUERY_PLAN, parameters: serializedParams, metadata: hex"" + }); + + // 4. Assemble the information needed to call the callback + IQueryRouter.Callback memory callback = IQueryRouter.Callback({ + maxGasPrice: MAX_GAS_PRICE, + gasLimit: GAS_LIMIT, + callbackContract: address(this), + selector: IQueryCallback.queryCallback.selector, + callbackData: "" + }); + + // 5. Execute the query. + IQueryRouter(QUERY_ROUTER) // aderyn-ignore unchecked-return + .requestQuery(queryToRequest, callback, PAYMENT_AMOUNT, uint64(block.timestamp + 1 hours)); + } + + /// @notice Example event emitting the query result. + event QueryFulfilled(uint256 contractCount); + + /// @inheritdoc IQueryCallback + /// @notice Handle the query result. + /// @dev This will be called once the query has been executed and verified on-chain. + function queryCallback(bytes32, bytes calldata queryResult, bytes calldata) external { + // Use the `ProofOfSqlTable` to deserialize and read data from the result + (, ProofOfSqlTable.Table memory tableResult) = ProofOfSqlTable.__deserializeFromBytes(queryResult); + uint256 contractCount = ProofOfSqlTable.readBigIntColumn(tableResult, 0).length; + // Emit the count. + emit QueryFulfilled(contractCount); + } +} diff --git a/queries/src/PoSQLTimeStampWithInequality.sol b/queries/src/PoSQLTimeStampWithInequality.sol new file mode 100644 index 0000000..cd59361 --- /dev/null +++ b/queries/src/PoSQLTimeStampWithInequality.sol @@ -0,0 +1,96 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.28; + +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import {ParamsBuilder, ProofOfSqlTable} from "sxt-proof-of-sql-contracts/src/PoSQL.sol"; +import {IQueryCallback} from "sxt-proof-of-sql-contracts/src/IQueryCallback.sol"; +import {IQueryRouter} from "sxt-proof-of-sql-contracts/src/query-router/interfaces/IQueryRouter.sol"; + +/// @title PoSQLTimeStampWithInequality +/// @notice Example "Hello World"-style contract of a Proof of SQL query using a timestamp and an inequality. +/// @dev This contract uses QueryRouter to pay for and execute the query. +/// the query is a simple SQL query that retrieves the number of blocks that occurred in a specific 60,000 millisecond window. +/// note: make sure that this contract holds SXT enough to cover the query payment. Also this is a simple +/// example and does not include zero address checks and other security checks. + +/// @dev to deploy this contract, use the DeployPoSQLTimeStampWithInequality script: +/// forge script script/deployPoSQLTimeStampWithInequality.s.sol:DeployPoSQLTimeStampWithInequality --broadcast --rpc-url=$ETH_RPC_URL --private-key=$PRIVATE_KEY --verify -vvvvv +contract PoSQLTimeStampWithInequality is IQueryCallback { + using SafeERC20 for IERC20; + + /// @notice QueryRouter contract address + address public immutable QUERY_ROUTER; + /// @notice Proof of SQL version + bytes32 public immutable VERSION; + /// @notice SXT token + address public immutable SXT; + /// @notice The ammount of SXT to pay for the query + uint256 public immutable PAYMENT_AMOUNT; + + uint256 public constant MAX_GAS_PRICE = 1e6; + uint64 public constant GAS_LIMIT = 1e6; + int64 public constant MINUTES_IN_MILLISECONDS = 6e4; + + /// @dev hex-serialized SQL query plan: + /// SELECT BLOCK_NUMBER FROM ETHEREUM.BLOCKS WHERE TIME_STAMP>$1 AND TIME_STAMP<$2 + bytes public constant QUERY_PLAN = + hex"0000000000000001000000000000000f455448455245554d2e424c4f434b5300000000000000020000000000000000000000000000000c424c4f434b5f4e554d424552000000050000000000000000000000000000000a54494d455f5354414d500000000900000001000000000000000000000001000000000000000c424c4f434b5f4e554d424552000000000000000000000000000000060000000a0000000000000000000000010000000b0000000000000000000000090000000100000000000000000a0000000000000000000000010000000b0000000000000001000000090000000100000000010000000000000001000000000000000000000000"; + + constructor(address queryRouter, bytes32 version, address sxt, uint248 amount) { + QUERY_ROUTER = queryRouter; + VERSION = version; + SXT = sxt; + PAYMENT_AMOUNT = amount; + } + + /// @notice Pay for and execute a "Hello World"-style query with an inequality and timestamp. + function query() external { + // 0. pull SXT from the caller (must have approved this contract beforehand) + IERC20(SXT).safeTransferFrom(msg.sender, address(this), PAYMENT_AMOUNT); + + // 1. approve payment to be spent by QueryRouter, that will cover PoSQLVerifier's fees and fulfillment callback gas. + // Note: make sure that the caller address holds at least `PAYMENT_AMOUNT` of SXT. + IERC20(SXT).forceApprove(QUERY_ROUTER, PAYMENT_AMOUNT); // use forceApprove to handle nonzero allowances + + // 2. Assemble the SQL parameters using the `ParamsBuilder` library + int64 blockTimeStampInMilliseconds = int64(uint64(uint256(1746470771) * 1000)); + + bytes memory param1 = + ParamsBuilder.unixTimestampMillisParam(blockTimeStampInMilliseconds - MINUTES_IN_MILLISECONDS); + bytes memory param2 = ParamsBuilder.unixTimestampMillisParam(blockTimeStampInMilliseconds); + bytes[] memory queryParameters = new bytes[](2); + queryParameters[0] = param1; + queryParameters[1] = param2; + bytes memory serializedParams = ParamsBuilder.serializeParamArray(queryParameters); + + IQueryRouter.Query memory queryToRequest = IQueryRouter.Query({ + version: VERSION, innerQuery: QUERY_PLAN, parameters: serializedParams, metadata: hex"" + }); + + IQueryRouter.Callback memory callback = IQueryRouter.Callback({ + maxGasPrice: MAX_GAS_PRICE, + gasLimit: GAS_LIMIT, + callbackContract: address(this), + selector: IQueryCallback.queryCallback.selector, + callbackData: "" + }); + + // 3. Execute the query. + IQueryRouter(QUERY_ROUTER) // aderyn-ignore unchecked-return + .requestQuery(queryToRequest, callback, PAYMENT_AMOUNT, uint64(block.timestamp + 1 hours)); + } + + /// @notice Example event emitting the total number of blocks in the 60 second window. + event QueryFulfilled(uint256 totalBlocks); + + /// @inheritdoc IQueryCallback + /// @notice Handle the query result. + /// @dev This will be called once the query has been executed and verified on-chain. + function queryCallback(bytes32, bytes calldata queryResult, bytes calldata) external { + (, ProofOfSqlTable.Table memory tableResult) = ProofOfSqlTable.__deserializeFromBytes(queryResult); + uint256 length = ProofOfSqlTable.readBigIntColumn(tableResult, 0).length; + // Emit the total number of blocks in the 60 second window + emit QueryFulfilled(length); + } +}