diff --git a/.env.example b/.env.example index 00f33089..4c8fb2ad 100644 --- a/.env.example +++ b/.env.example @@ -6,4 +6,10 @@ OPENAI_VECTOR_STORE_ID=vs_... # GitHub Personal Access Token — required for content sync # read access -GITHUB_TOKEN= \ No newline at end of file +GITHUB_TOKEN= + +STAGING=1 +NEXT_PUBLIC_STAGING=1 + +# PostgreSQL — required for coalition builder +DATABASE_URL=postgres://user:password@localhost:5432/gitcoin_coalitions diff --git a/.npmrc b/.npmrc new file mode 100644 index 00000000..521a9f7c --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +legacy-peer-deps=true diff --git a/contracts/cache/solidity-files-cache.json b/contracts/cache/solidity-files-cache.json new file mode 100644 index 00000000..3df7cfb0 --- /dev/null +++ b/contracts/cache/solidity-files-cache.json @@ -0,0 +1 @@ +{"_format":"","paths":{"artifacts":"out","build_infos":"out/build-info","sources":"src","tests":"test","scripts":"script","libraries":["lib"]},"files":{"src/DaccCoalitionStaking.sol":{"lastModificationDate":1772872914722,"contentHash":"fab29b59f8cdde69","interfaceReprHash":null,"sourceName":"src/DaccCoalitionStaking.sol","imports":[],"versionRequirement":"^0.8.24","artifacts":{"DaccCoalitionStaking":{"0.8.24":{"default":{"path":"DaccCoalitionStaking.sol/DaccCoalitionStaking.json","build_id":"d0438b8bf5dea4bd"}}}},"seenByCompiler":true}},"builds":["d0438b8bf5dea4bd"],"profiles":{"default":{"solc":{"optimizer":{"enabled":true,"runs":200},"metadata":{"useLiteralContent":false,"bytecodeHash":"ipfs","appendCBOR":true},"outputSelection":{"*":{"*":["abi","evm.bytecode.object","evm.bytecode.sourceMap","evm.bytecode.linkReferences","evm.deployedBytecode.object","evm.deployedBytecode.sourceMap","evm.deployedBytecode.linkReferences","evm.deployedBytecode.immutableReferences","evm.methodIdentifiers","metadata"]}},"evmVersion":"cancun","viaIR":false,"libraries":{}},"vyper":{"evmVersion":"cancun","outputSelection":{"*":{"*":["abi","evm.bytecode","evm.deployedBytecode"]}}}}},"preprocessed":false,"mocks":[]} \ No newline at end of file diff --git a/contracts/foundry.toml b/contracts/foundry.toml new file mode 100644 index 00000000..8dc57b2b --- /dev/null +++ b/contracts/foundry.toml @@ -0,0 +1,7 @@ +[profile.default] +src = "src" +out = "out" +libs = ["lib"] +solc_version = "0.8.24" +optimizer = true +optimizer_runs = 200 diff --git a/contracts/out/DaccCoalitionStaking.sol/DaccCoalitionStaking.json b/contracts/out/DaccCoalitionStaking.sol/DaccCoalitionStaking.json new file mode 100644 index 00000000..3291002a --- /dev/null +++ b/contracts/out/DaccCoalitionStaking.sol/DaccCoalitionStaking.json @@ -0,0 +1 @@ +{"abi":[{"type":"constructor","inputs":[{"name":"_operator","type":"address","internalType":"address"}],"stateMutability":"nonpayable"},{"type":"function","name":"OPERATOR_THRESHOLD","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"domainDeployed","inputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"domainTotals","inputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getDomainTotal","inputs":[{"name":"domainId","type":"string","internalType":"string"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getStake","inputs":[{"name":"domainId","type":"string","internalType":"string"},{"name":"staker","type":"address","internalType":"address"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"isDomainDeployed","inputs":[{"name":"domainId","type":"string","internalType":"string"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"operator","inputs":[],"outputs":[{"name":"","type":"address","internalType":"address"}],"stateMutability":"view"},{"type":"function","name":"operatorWithdraw","inputs":[{"name":"domainId","type":"string","internalType":"string"},{"name":"to","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"stake","inputs":[{"name":"domainId","type":"string","internalType":"string"}],"outputs":[],"stateMutability":"payable"},{"type":"function","name":"stakes","inputs":[{"name":"","type":"bytes32","internalType":"bytes32"},{"name":"","type":"address","internalType":"address"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"withdraw","inputs":[{"name":"domainId","type":"string","internalType":"string"},{"name":"amount","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"event","name":"OperatorWithdrawn","inputs":[{"name":"domainHash","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"domainId","type":"string","indexed":false,"internalType":"string"},{"name":"to","type":"address","indexed":true,"internalType":"address"},{"name":"amount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"Staked","inputs":[{"name":"domainHash","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"domainId","type":"string","indexed":false,"internalType":"string"},{"name":"staker","type":"address","indexed":true,"internalType":"address"},{"name":"amount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"Withdrawn","inputs":[{"name":"domainHash","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"domainId","type":"string","indexed":false,"internalType":"string"},{"name":"staker","type":"address","indexed":true,"internalType":"address"},{"name":"amount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"error","name":"BelowThreshold","inputs":[]},{"type":"error","name":"DomainAlreadyDeployed","inputs":[]},{"type":"error","name":"InsufficientStake","inputs":[]},{"type":"error","name":"NotOperator","inputs":[]},{"type":"error","name":"TransferFailed","inputs":[]},{"type":"error","name":"ZeroAmount","inputs":[]}],"bytecode":{"object":"0x60a060405234801561000f575f80fd5b506040516109f13803806109f183398101604081905261002e9161003f565b6001600160a01b031660805261006c565b5f6020828403121561004f575f80fd5b81516001600160a01b0381168114610065575f80fd5b9392505050565b60805161096661008b5f395f818161018c015261026d01526109665ff3fe608060405260043610610099575f3560e01c806346f45b8d1161006257806346f45b8d14610168578063570ca7351461017b5780638be12dad146101c6578063a7b9828f146101f1578063d33f8faa14610224578063fe08ab9314610243575f80fd5b80620d33e41461009d5780631f1b5b68146100e057806321e2731c1461010957806330b39a621461012a5780633ff18fa114610149575b5f80fd5b3480156100a8575f80fd5b506100cb6100b7366004610738565b60026020525f908152604090205460ff1681565b60405190151581526020015b60405180910390f35b3480156100eb575f80fd5b506100fb670de0b6b3a764000081565b6040519081526020016100d7565b348015610114575f80fd5b506101286101233660046107af565b610262565b005b348015610135575f80fd5b506101286101443660046107ff565b6103f0565b348015610154575f80fd5b506100cb610163366004610847565b610584565b610128610176366004610847565b6105ae565b348015610186575f80fd5b506101ae7f000000000000000000000000000000000000000000000000000000000000000081565b6040516001600160a01b0390911681526020016100d7565b3480156101d1575f80fd5b506100fb6101e0366004610738565b60016020525f908152604090205481565b3480156101fc575f80fd5b506100fb61020b366004610886565b5f60208181529281526040808220909352908152205481565b34801561022f575f80fd5b506100fb61023e366004610847565b61069d565b34801561024e575f80fd5b506100fb61025d3660046107af565b6106c1565b336001600160a01b037f000000000000000000000000000000000000000000000000000000000000000016146102ab57604051631f0853c160e21b815260040160405180910390fd5b5f6102b68484610706565b5f8181526002602052604090205490915060ff16156102e8576040516303dc24a160e01b815260040160405180910390fd5b5f81815260016020526040902054670de0b6b3a764000081101561031e57604051625713a160e91b815260040160405180910390fd5b5f82815260026020526040808220805460ff19166001179055516001600160a01b0385169083908381818185875af1925050503d805f811461037b576040519150601f19603f3d011682016040523d82523d5f602084013e610380565b606091505b50509050806103a2576040516312171d8360e31b815260040160405180910390fd5b836001600160a01b0316837fe1609306cb3b99baefaf7c5e191300805059d303c9a58cefdcd8698007d7a1d98888866040516103e0939291906108b0565b60405180910390a3505050505050565b805f0361041057604051631f2a200560e01b815260040160405180910390fd5b5f61041b8484610706565b5f8181526002602052604090205490915060ff161561044d576040516303dc24a160e01b815260040160405180910390fd5b5f81815260208181526040808320338452909152902054821115610484576040516378de4a6960e11b815260040160405180910390fd5b5f81815260208181526040808320338452909152812080548492906104aa9084906108fb565b90915550505f81815260016020526040812080548492906104cc9084906108fb565b90915550506040515f90339084908381818185875af1925050503d805f8114610510576040519150601f19603f3d011682016040523d82523d5f602084013e610515565b606091505b5050905080610537576040516312171d8360e31b815260040160405180910390fd5b336001600160a01b0316827f8c87a29e7720caf128730c6696d65e76a7ff2c80fa78fd5c7e9fc8c468109e92878787604051610575939291906108b0565b60405180910390a35050505050565b5f60025f6105928585610706565b815260208101919091526040015f205460ff1690505b92915050565b345f036105ce57604051631f2a200560e01b815260040160405180910390fd5b5f6105d98383610706565b5f8181526002602052604090205490915060ff161561060b576040516303dc24a160e01b815260040160405180910390fd5b5f818152602081815260408083203384529091528120805434929061063190849061090e565b90915550505f818152600160205260408120805434929061065390849061090e565b9091555050604051339082907f9b0f678daaa8ea4ba38e3d846908070c79ab5617c0e0a440b4a81029b3ba022690610690908790879034906108b0565b60405180910390a3505050565b5f60015f6106ab8585610706565b81526020019081526020015f2054905092915050565b5f805f6106ce8686610706565b81526020019081526020015f205f836001600160a01b03166001600160a01b031681526020019081526020015f205490509392505050565b5f828260405160200161071a929190610921565b60405160208183030381529060405280519060200120905092915050565b5f60208284031215610748575f80fd5b5035919050565b5f8083601f84011261075f575f80fd5b50813567ffffffffffffffff811115610776575f80fd5b60208301915083602082850101111561078d575f80fd5b9250929050565b80356001600160a01b03811681146107aa575f80fd5b919050565b5f805f604084860312156107c1575f80fd5b833567ffffffffffffffff8111156107d7575f80fd5b6107e38682870161074f565b90945092506107f6905060208501610794565b90509250925092565b5f805f60408486031215610811575f80fd5b833567ffffffffffffffff811115610827575f80fd5b6108338682870161074f565b909790965060209590950135949350505050565b5f8060208385031215610858575f80fd5b823567ffffffffffffffff81111561086e575f80fd5b61087a8582860161074f565b90969095509350505050565b5f8060408385031215610897575f80fd5b823591506108a760208401610794565b90509250929050565b60408152826040820152828460608301375f606084830101525f6060601f19601f8601168301019050826020830152949350505050565b634e487b7160e01b5f52601160045260245ffd5b818103818111156105a8576105a86108e7565b808201808211156105a8576105a86108e7565b818382375f910190815291905056fea2646970667358221220342c9146b0e8a71c9ed1dcbf7128a63d9db14b0da838a016a3614c5aaa343f7a64736f6c63430008180033","sourceMap":"508:5369:0:-:0;;;2378:68;;;;;;;;;;;;;;;;;;;;;;;;;;;;:::i;:::-;-1:-1:-1;;;;;2419:20:0;;;508:5369;;14:290:1;84:6;137:2;125:9;116:7;112:23;108:32;105:52;;;153:1;150;143:12;105:52;179:16;;-1:-1:-1;;;;;224:31:1;;214:42;;204:70;;270:1;267;260:12;204:70;293:5;14:290;-1:-1:-1;;;14:290:1:o;:::-;508:5369:0;;;;;;;;;;;;;;;;;","linkReferences":{}},"deployedBytecode":{"object":"0x608060405260043610610099575f3560e01c806346f45b8d1161006257806346f45b8d14610168578063570ca7351461017b5780638be12dad146101c6578063a7b9828f146101f1578063d33f8faa14610224578063fe08ab9314610243575f80fd5b80620d33e41461009d5780631f1b5b68146100e057806321e2731c1461010957806330b39a621461012a5780633ff18fa114610149575b5f80fd5b3480156100a8575f80fd5b506100cb6100b7366004610738565b60026020525f908152604090205460ff1681565b60405190151581526020015b60405180910390f35b3480156100eb575f80fd5b506100fb670de0b6b3a764000081565b6040519081526020016100d7565b348015610114575f80fd5b506101286101233660046107af565b610262565b005b348015610135575f80fd5b506101286101443660046107ff565b6103f0565b348015610154575f80fd5b506100cb610163366004610847565b610584565b610128610176366004610847565b6105ae565b348015610186575f80fd5b506101ae7f000000000000000000000000000000000000000000000000000000000000000081565b6040516001600160a01b0390911681526020016100d7565b3480156101d1575f80fd5b506100fb6101e0366004610738565b60016020525f908152604090205481565b3480156101fc575f80fd5b506100fb61020b366004610886565b5f60208181529281526040808220909352908152205481565b34801561022f575f80fd5b506100fb61023e366004610847565b61069d565b34801561024e575f80fd5b506100fb61025d3660046107af565b6106c1565b336001600160a01b037f000000000000000000000000000000000000000000000000000000000000000016146102ab57604051631f0853c160e21b815260040160405180910390fd5b5f6102b68484610706565b5f8181526002602052604090205490915060ff16156102e8576040516303dc24a160e01b815260040160405180910390fd5b5f81815260016020526040902054670de0b6b3a764000081101561031e57604051625713a160e91b815260040160405180910390fd5b5f82815260026020526040808220805460ff19166001179055516001600160a01b0385169083908381818185875af1925050503d805f811461037b576040519150601f19603f3d011682016040523d82523d5f602084013e610380565b606091505b50509050806103a2576040516312171d8360e31b815260040160405180910390fd5b836001600160a01b0316837fe1609306cb3b99baefaf7c5e191300805059d303c9a58cefdcd8698007d7a1d98888866040516103e0939291906108b0565b60405180910390a3505050505050565b805f0361041057604051631f2a200560e01b815260040160405180910390fd5b5f61041b8484610706565b5f8181526002602052604090205490915060ff161561044d576040516303dc24a160e01b815260040160405180910390fd5b5f81815260208181526040808320338452909152902054821115610484576040516378de4a6960e11b815260040160405180910390fd5b5f81815260208181526040808320338452909152812080548492906104aa9084906108fb565b90915550505f81815260016020526040812080548492906104cc9084906108fb565b90915550506040515f90339084908381818185875af1925050503d805f8114610510576040519150601f19603f3d011682016040523d82523d5f602084013e610515565b606091505b5050905080610537576040516312171d8360e31b815260040160405180910390fd5b336001600160a01b0316827f8c87a29e7720caf128730c6696d65e76a7ff2c80fa78fd5c7e9fc8c468109e92878787604051610575939291906108b0565b60405180910390a35050505050565b5f60025f6105928585610706565b815260208101919091526040015f205460ff1690505b92915050565b345f036105ce57604051631f2a200560e01b815260040160405180910390fd5b5f6105d98383610706565b5f8181526002602052604090205490915060ff161561060b576040516303dc24a160e01b815260040160405180910390fd5b5f818152602081815260408083203384529091528120805434929061063190849061090e565b90915550505f818152600160205260408120805434929061065390849061090e565b9091555050604051339082907f9b0f678daaa8ea4ba38e3d846908070c79ab5617c0e0a440b4a81029b3ba022690610690908790879034906108b0565b60405180910390a3505050565b5f60015f6106ab8585610706565b81526020019081526020015f2054905092915050565b5f805f6106ce8686610706565b81526020019081526020015f205f836001600160a01b03166001600160a01b031681526020019081526020015f205490509392505050565b5f828260405160200161071a929190610921565b60405160208183030381529060405280519060200120905092915050565b5f60208284031215610748575f80fd5b5035919050565b5f8083601f84011261075f575f80fd5b50813567ffffffffffffffff811115610776575f80fd5b60208301915083602082850101111561078d575f80fd5b9250929050565b80356001600160a01b03811681146107aa575f80fd5b919050565b5f805f604084860312156107c1575f80fd5b833567ffffffffffffffff8111156107d7575f80fd5b6107e38682870161074f565b90945092506107f6905060208501610794565b90509250925092565b5f805f60408486031215610811575f80fd5b833567ffffffffffffffff811115610827575f80fd5b6108338682870161074f565b909790965060209590950135949350505050565b5f8060208385031215610858575f80fd5b823567ffffffffffffffff81111561086e575f80fd5b61087a8582860161074f565b90969095509350505050565b5f8060408385031215610897575f80fd5b823591506108a760208401610794565b90509250929050565b60408152826040820152828460608301375f606084830101525f6060601f19601f8601168301019050826020830152949350505050565b634e487b7160e01b5f52601160045260245ffd5b818103818111156105a8576105a86108e7565b808201808211156105a8576105a86108e7565b818382375f910190815291905056fea2646970667358221220342c9146b0e8a71c9ed1dcbf7128a63d9db14b0da838a016a3614c5aaa343f7a64736f6c63430008180033","sourceMap":"508:5369:0:-:0;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;1130:46;;;;;;;;;;-1:-1:-1;1130:46:0;;;;;:::i;:::-;;;;;;;;;;;;;;;;;;;364:14:1;;357:22;339:41;;327:2;312:18;1130:46:0;;;;;;;;784:52;;;;;;;;;;;;829:7;784:52;;;;;537:25:1;;;525:2;510:18;784:52:0;391:177:1;4206:544:0;;;;;;;;;;-1:-1:-1;4206:544:0;;;;;:::i;:::-;;:::i;:::-;;3364:548;;;;;;;;;;-1:-1:-1;3364:548:0;;;;;:::i;:::-;;:::i;5403:136::-;;;;;;;;;;-1:-1:-1;5403:136:0;;;;;:::i;:::-;;:::i;2807:370::-;;;;;;:::i;:::-;;:::i;745:33::-;;;;;;;;;;;;;;;;;;-1:-1:-1;;;;;2658:32:1;;;2640:51;;2628:2;2613:18;745:33:0;2494:203:1;999:47:0;;;;;;;;;;-1:-1:-1;999:47:0;;;;;:::i;:::-;;;;;;;;;;;;;;887:61;;;;;;;;;;-1:-1:-1;887:61:0;;;;;:::i;:::-;;;;;;;;;;;;;;;;;;;;;;;5185:135;;;;;;;;;;-1:-1:-1;5185:135:0;;;;;:::i;:::-;;:::i;4987:147::-;;;;;;;;;;-1:-1:-1;4987:147:0;;;;;:::i;:::-;;:::i;4206:544::-;4293:10;-1:-1:-1;;;;;4307:8:0;4293:22;;4289:48;;4324:13;;-1:-1:-1;;;4324:13:0;;;;;;;;;;;4289:48;4347:12;4362:15;4368:8;;4362:5;:15::i;:::-;4391:20;;;;:14;:20;;;;;;4347:30;;-1:-1:-1;4391:20:0;;4387:56;;;4420:23;;-1:-1:-1;;;4420:23:0;;;;;;;;;;;4387:56;4454:13;4470:18;;;:12;:18;;;;;;829:7;4502:26;;4498:55;;;4537:16;;-1:-1:-1;;;4537:16:0;;;;;;;;;;;4498:55;4564:20;;;;:14;:20;;;;;;:27;;-1:-1:-1;;4564:27:0;4587:4;4564:27;;;4616:25;-1:-1:-1;;;;;4616:7:0;;;4631:5;;4564:20;4616:25;4564:20;4616:25;4631:5;4616:7;:25;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;4602:39;;;4656:2;4651:32;;4667:16;;-1:-1:-1;;;4667:16:0;;;;;;;;;;;4651:32;4733:2;-1:-1:-1;;;;;4699:44:0;4717:4;4699:44;4723:8;;4737:5;4699:44;;;;;;;;:::i;:::-;;;;;;;;4279:471;;;4206:544;;;:::o;3364:548::-;3447:6;3457:1;3447:11;3443:36;;3467:12;;-1:-1:-1;;;3467:12:0;;;;;;;;;;;3443:36;3489:12;3504:15;3510:8;;3504:5;:15::i;:::-;3533:20;;;;:14;:20;;;;;;3489:30;;-1:-1:-1;3533:20:0;;3529:56;;;3562:23;;-1:-1:-1;;;3562:23:0;;;;;;;;;;;3529:56;3599:6;:12;;;;;;;;;;;3612:10;3599:24;;;;;;;;:33;-1:-1:-1;3595:65:0;;;3641:19;;-1:-1:-1;;;3641:19:0;;;;;;;;;;;3595:65;3671:6;:12;;;;;;;;;;;3684:10;3671:24;;;;;;;:34;;3699:6;;3671;:34;;3699:6;;3671:34;:::i;:::-;;;;-1:-1:-1;;3715:18:0;;;;:12;:18;;;;;:28;;3737:6;;3715:18;:28;;3737:6;;3715:28;:::i;:::-;;;;-1:-1:-1;;3768:34:0;;3755:7;;3768:10;;3791:6;;3755:7;3768:34;3755:7;3768:34;3791:6;3768:10;:34;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;3754:48;;;3817:2;3812:32;;3828:16;;-1:-1:-1;;;3828:16:0;;;;;;;;;;;3812:32;3886:10;-1:-1:-1;;;;;3860:45:0;3870:4;3860:45;3876:8;;3898:6;3860:45;;;;;;;;:::i;:::-;;;;;;;;3433:479;;3364:548;;;:::o;5403:136::-;5478:4;5501:14;:31;5516:15;5522:8;;5516:5;:15::i;:::-;5501:31;;;;;;;;;;;-1:-1:-1;5501:31:0;;;;;-1:-1:-1;5403:136:0;;;;;:::o;2807:370::-;2879:9;2892:1;2879:14;2875:39;;2902:12;;-1:-1:-1;;;2902:12:0;;;;;;;;;;;2875:39;2924:12;2939:15;2945:8;;2939:5;:15::i;:::-;2968:20;;;;:14;:20;;;;;;2924:30;;-1:-1:-1;2968:20:0;;2964:56;;;2997:23;;-1:-1:-1;;;2997:23:0;;;;;;;;;;;2964:56;3031:6;:12;;;;;;;;;;;3044:10;3031:24;;;;;;;:37;;3059:9;;3031:6;:37;;3059:9;;3031:37;:::i;:::-;;;;-1:-1:-1;;3078:18:0;;;;:12;:18;;;;;:31;;3100:9;;3078:18;:31;;3100:9;;3078:31;:::i;:::-;;;;-1:-1:-1;;3125:45:0;;3148:10;;3132:4;;3125:45;;;;3138:8;;;;3160:9;;3125:45;:::i;:::-;;;;;;;;2865:312;2807:370;;:::o;5185:135::-;5258:7;5284:12;:29;5297:15;5303:8;;5297:5;:15::i;:::-;5284:29;;;;;;;;;;;;5277:36;;5185:135;;;;:::o;4987:147::-;5070:7;5096:6;:23;5103:15;5109:8;;5103:5;:15::i;:::-;5096:23;;;;;;;;;;;:31;5120:6;-1:-1:-1;;;;;5096:31:0;-1:-1:-1;;;;;5096:31:0;;;;;;;;;;;;;5089:38;;4987:147;;;;;:::o;5741:134::-;5805:7;5858:8;;5841:26;;;;;;;;;:::i;:::-;;;;;;;;;;;;;5831:37;;;;;;5824:44;;5741:134;;;;:::o;14:180:1:-;73:6;126:2;114:9;105:7;101:23;97:32;94:52;;;142:1;139;132:12;94:52;-1:-1:-1;165:23:1;;14:180;-1:-1:-1;14:180:1:o;573:348::-;625:8;635:6;689:3;682:4;674:6;670:17;666:27;656:55;;707:1;704;697:12;656:55;-1:-1:-1;730:20:1;;773:18;762:30;;759:50;;;805:1;802;795:12;759:50;842:4;834:6;830:17;818:29;;894:3;887:4;878:6;870;866:19;862:30;859:39;856:59;;;911:1;908;901:12;856:59;573:348;;;;;:::o;926:173::-;994:20;;-1:-1:-1;;;;;1043:31:1;;1033:42;;1023:70;;1089:1;1086;1079:12;1023:70;926:173;;;:::o;1104:485::-;1184:6;1192;1200;1253:2;1241:9;1232:7;1228:23;1224:32;1221:52;;;1269:1;1266;1259:12;1221:52;1309:9;1296:23;1342:18;1334:6;1331:30;1328:50;;;1374:1;1371;1364:12;1328:50;1413:59;1464:7;1455:6;1444:9;1440:22;1413:59;:::i;:::-;1491:8;;-1:-1:-1;1387:85:1;-1:-1:-1;1545:38:1;;-1:-1:-1;1579:2:1;1564:18;;1545:38;:::i;:::-;1535:48;;1104:485;;;;;:::o;1594:479::-;1674:6;1682;1690;1743:2;1731:9;1722:7;1718:23;1714:32;1711:52;;;1759:1;1756;1749:12;1711:52;1799:9;1786:23;1832:18;1824:6;1821:30;1818:50;;;1864:1;1861;1854:12;1818:50;1903:59;1954:7;1945:6;1934:9;1930:22;1903:59;:::i;:::-;1981:8;;1877:85;;-1:-1:-1;2063:2:1;2048:18;;;;2035:32;;1594:479;-1:-1:-1;;;;1594:479:1:o;2078:411::-;2149:6;2157;2210:2;2198:9;2189:7;2185:23;2181:32;2178:52;;;2226:1;2223;2216:12;2178:52;2266:9;2253:23;2299:18;2291:6;2288:30;2285:50;;;2331:1;2328;2321:12;2285:50;2370:59;2421:7;2412:6;2401:9;2397:22;2370:59;:::i;:::-;2448:8;;2344:85;;-1:-1:-1;2078:411:1;-1:-1:-1;;;;2078:411:1:o;2702:254::-;2770:6;2778;2831:2;2819:9;2810:7;2806:23;2802:32;2799:52;;;2847:1;2844;2837:12;2799:52;2883:9;2870:23;2860:33;;2912:38;2946:2;2935:9;2931:18;2912:38;:::i;:::-;2902:48;;2702:254;;;;;:::o;3171:463::-;3358:2;3347:9;3340:21;3397:6;3392:2;3381:9;3377:18;3370:34;3454:6;3446;3441:2;3430:9;3426:18;3413:48;3510:1;3505:2;3496:6;3485:9;3481:22;3477:31;3470:42;3321:4;3580:2;3573;3569:7;3564:2;3556:6;3552:15;3548:29;3537:9;3533:45;3529:54;3521:62;;3621:6;3614:4;3603:9;3599:20;3592:36;3171:463;;;;;;:::o;3639:127::-;3700:10;3695:3;3691:20;3688:1;3681:31;3731:4;3728:1;3721:15;3755:4;3752:1;3745:15;3771:128;3838:9;;;3859:11;;;3856:37;;;3873:18;;:::i;3904:125::-;3969:9;;;3990:10;;;3987:36;;;4003:18;;:::i;4034:273::-;4219:6;4211;4206:3;4193:33;4175:3;4245:16;;4270:13;;;4245:16;4034:273;-1:-1:-1;4034:273:1:o","linkReferences":{},"immutableReferences":{"4":[{"start":396,"length":32},{"start":621,"length":32}]}},"methodIdentifiers":{"OPERATOR_THRESHOLD()":"1f1b5b68","domainDeployed(bytes32)":"000d33e4","domainTotals(bytes32)":"8be12dad","getDomainTotal(string)":"d33f8faa","getStake(string,address)":"fe08ab93","isDomainDeployed(string)":"3ff18fa1","operator()":"570ca735","operatorWithdraw(string,address)":"21e2731c","stake(string)":"46f45b8d","stakes(bytes32,address)":"a7b9828f","withdraw(string,uint256)":"30b39a62"},"rawMetadata":"{\"compiler\":{\"version\":\"0.8.24+commit.e11b9ed9\"},\"language\":\"Solidity\",\"output\":{\"abi\":[{\"inputs\":[{\"internalType\":\"address\",\"name\":\"_operator\",\"type\":\"address\"}],\"stateMutability\":\"nonpayable\",\"type\":\"constructor\"},{\"inputs\":[],\"name\":\"BelowThreshold\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"DomainAlreadyDeployed\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"InsufficientStake\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"NotOperator\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"TransferFailed\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"ZeroAmount\",\"type\":\"error\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"bytes32\",\"name\":\"domainHash\",\"type\":\"bytes32\"},{\"indexed\":false,\"internalType\":\"string\",\"name\":\"domainId\",\"type\":\"string\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"to\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"OperatorWithdrawn\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"bytes32\",\"name\":\"domainHash\",\"type\":\"bytes32\"},{\"indexed\":false,\"internalType\":\"string\",\"name\":\"domainId\",\"type\":\"string\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"staker\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"Staked\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"bytes32\",\"name\":\"domainHash\",\"type\":\"bytes32\"},{\"indexed\":false,\"internalType\":\"string\",\"name\":\"domainId\",\"type\":\"string\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"staker\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"Withdrawn\",\"type\":\"event\"},{\"inputs\":[],\"name\":\"OPERATOR_THRESHOLD\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes32\",\"name\":\"\",\"type\":\"bytes32\"}],\"name\":\"domainDeployed\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"\",\"type\":\"bool\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes32\",\"name\":\"\",\"type\":\"bytes32\"}],\"name\":\"domainTotals\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"string\",\"name\":\"domainId\",\"type\":\"string\"}],\"name\":\"getDomainTotal\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"string\",\"name\":\"domainId\",\"type\":\"string\"},{\"internalType\":\"address\",\"name\":\"staker\",\"type\":\"address\"}],\"name\":\"getStake\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"string\",\"name\":\"domainId\",\"type\":\"string\"}],\"name\":\"isDomainDeployed\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"\",\"type\":\"bool\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"operator\",\"outputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"string\",\"name\":\"domainId\",\"type\":\"string\"},{\"internalType\":\"address\",\"name\":\"to\",\"type\":\"address\"}],\"name\":\"operatorWithdraw\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"string\",\"name\":\"domainId\",\"type\":\"string\"}],\"name\":\"stake\",\"outputs\":[],\"stateMutability\":\"payable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes32\",\"name\":\"\",\"type\":\"bytes32\"},{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"name\":\"stakes\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"string\",\"name\":\"domainId\",\"type\":\"string\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"withdraw\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"}],\"devdoc\":{\"details\":\"Intentionally simple \\u2014 no upgradability, no external calls except ETH transfers.\",\"kind\":\"dev\",\"methods\":{\"operatorWithdraw(string,address)\":{\"params\":{\"domainId\":\"The domain to deploy\",\"to\":\"Destination wallet for the pooled funds\"}},\"stake(string)\":{\"params\":{\"domainId\":\"Human-readable domain identifier (e.g. \\\"zero-knowledge-systems\\\")\"}},\"withdraw(string,uint256)\":{\"params\":{\"amount\":\"Wei to withdraw\",\"domainId\":\"The domain to withdraw from\"}}},\"stateVariables\":{\"domainDeployed\":{\"details\":\"domainHash => true once operator has withdrawn (funds deployed)\"},\"domainTotals\":{\"details\":\"domainHash => total staked ETH\"},\"stakes\":{\"details\":\"domainHash => staker => amount\"}},\"title\":\"d/acc Coalition Staking\",\"version\":1},\"userdoc\":{\"kind\":\"user\",\"methods\":{\"getDomainTotal(string)\":{\"notice\":\"Get total staked in a domain\"},\"getStake(string,address)\":{\"notice\":\"Get a user's stake in a domain\"},\"isDomainDeployed(string)\":{\"notice\":\"Check if a domain's funds have been deployed by the operator\"},\"operatorWithdraw(string,address)\":{\"notice\":\"Operator withdraws a domain's pooled funds once threshold is met. This marks the domain as \\\"deployed\\\" \\u2014 individual withdrawals are no longer possible.\"},\"stake(string)\":{\"notice\":\"Stake ETH toward a domain. Marks your interest with skin in the game.\"},\"withdraw(string,uint256)\":{\"notice\":\"Withdraw your own stake from a domain. Only works before operator deploys.\"}},\"notice\":\"Stake ETH to signal interest in d/acc domains. Users can withdraw their own stake at any time before the operator deploys funds. The operator (0x00De4B13153673BCAE2616b67bf822500d325Fc3) can withdraw a domain's pooled funds to a wallet of their choice once it reaches 1 ETH.\",\"version\":1}},\"settings\":{\"compilationTarget\":{\"src/DaccCoalitionStaking.sol\":\"DaccCoalitionStaking\"},\"evmVersion\":\"cancun\",\"libraries\":{},\"metadata\":{\"bytecodeHash\":\"ipfs\"},\"optimizer\":{\"enabled\":true,\"runs\":200},\"remappings\":[]},\"sources\":{\"src/DaccCoalitionStaking.sol\":{\"keccak256\":\"0x1041b91b0913ce84a3e15b5ac36a0cc4bdad8a4e21b49b6e0c00f8a62bd2e520\",\"license\":\"MIT\",\"urls\":[\"bzz-raw://5694e5140ebdb32a1f5d31603bf3569e66030c4346ce3048ddcf9b20019a4dd7\",\"dweb:/ipfs/QmQDXwi6xxSbJJ1A1Kp3jyvSd2KUReReS9MWXw19dcnpXZ\"]}},\"version\":1}","metadata":{"compiler":{"version":"0.8.24+commit.e11b9ed9"},"language":"Solidity","output":{"abi":[{"inputs":[{"internalType":"address","name":"_operator","type":"address"}],"stateMutability":"nonpayable","type":"constructor"},{"inputs":[],"type":"error","name":"BelowThreshold"},{"inputs":[],"type":"error","name":"DomainAlreadyDeployed"},{"inputs":[],"type":"error","name":"InsufficientStake"},{"inputs":[],"type":"error","name":"NotOperator"},{"inputs":[],"type":"error","name":"TransferFailed"},{"inputs":[],"type":"error","name":"ZeroAmount"},{"inputs":[{"internalType":"bytes32","name":"domainHash","type":"bytes32","indexed":true},{"internalType":"string","name":"domainId","type":"string","indexed":false},{"internalType":"address","name":"to","type":"address","indexed":true},{"internalType":"uint256","name":"amount","type":"uint256","indexed":false}],"type":"event","name":"OperatorWithdrawn","anonymous":false},{"inputs":[{"internalType":"bytes32","name":"domainHash","type":"bytes32","indexed":true},{"internalType":"string","name":"domainId","type":"string","indexed":false},{"internalType":"address","name":"staker","type":"address","indexed":true},{"internalType":"uint256","name":"amount","type":"uint256","indexed":false}],"type":"event","name":"Staked","anonymous":false},{"inputs":[{"internalType":"bytes32","name":"domainHash","type":"bytes32","indexed":true},{"internalType":"string","name":"domainId","type":"string","indexed":false},{"internalType":"address","name":"staker","type":"address","indexed":true},{"internalType":"uint256","name":"amount","type":"uint256","indexed":false}],"type":"event","name":"Withdrawn","anonymous":false},{"inputs":[],"stateMutability":"view","type":"function","name":"OPERATOR_THRESHOLD","outputs":[{"internalType":"uint256","name":"","type":"uint256"}]},{"inputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function","name":"domainDeployed","outputs":[{"internalType":"bool","name":"","type":"bool"}]},{"inputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function","name":"domainTotals","outputs":[{"internalType":"uint256","name":"","type":"uint256"}]},{"inputs":[{"internalType":"string","name":"domainId","type":"string"}],"stateMutability":"view","type":"function","name":"getDomainTotal","outputs":[{"internalType":"uint256","name":"","type":"uint256"}]},{"inputs":[{"internalType":"string","name":"domainId","type":"string"},{"internalType":"address","name":"staker","type":"address"}],"stateMutability":"view","type":"function","name":"getStake","outputs":[{"internalType":"uint256","name":"","type":"uint256"}]},{"inputs":[{"internalType":"string","name":"domainId","type":"string"}],"stateMutability":"view","type":"function","name":"isDomainDeployed","outputs":[{"internalType":"bool","name":"","type":"bool"}]},{"inputs":[],"stateMutability":"view","type":"function","name":"operator","outputs":[{"internalType":"address","name":"","type":"address"}]},{"inputs":[{"internalType":"string","name":"domainId","type":"string"},{"internalType":"address","name":"to","type":"address"}],"stateMutability":"nonpayable","type":"function","name":"operatorWithdraw"},{"inputs":[{"internalType":"string","name":"domainId","type":"string"}],"stateMutability":"payable","type":"function","name":"stake"},{"inputs":[{"internalType":"bytes32","name":"","type":"bytes32"},{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function","name":"stakes","outputs":[{"internalType":"uint256","name":"","type":"uint256"}]},{"inputs":[{"internalType":"string","name":"domainId","type":"string"},{"internalType":"uint256","name":"amount","type":"uint256"}],"stateMutability":"nonpayable","type":"function","name":"withdraw"}],"devdoc":{"kind":"dev","methods":{"operatorWithdraw(string,address)":{"params":{"domainId":"The domain to deploy","to":"Destination wallet for the pooled funds"}},"stake(string)":{"params":{"domainId":"Human-readable domain identifier (e.g. \"zero-knowledge-systems\")"}},"withdraw(string,uint256)":{"params":{"amount":"Wei to withdraw","domainId":"The domain to withdraw from"}}},"version":1},"userdoc":{"kind":"user","methods":{"getDomainTotal(string)":{"notice":"Get total staked in a domain"},"getStake(string,address)":{"notice":"Get a user's stake in a domain"},"isDomainDeployed(string)":{"notice":"Check if a domain's funds have been deployed by the operator"},"operatorWithdraw(string,address)":{"notice":"Operator withdraws a domain's pooled funds once threshold is met. This marks the domain as \"deployed\" — individual withdrawals are no longer possible."},"stake(string)":{"notice":"Stake ETH toward a domain. Marks your interest with skin in the game."},"withdraw(string,uint256)":{"notice":"Withdraw your own stake from a domain. Only works before operator deploys."}},"version":1}},"settings":{"remappings":[],"optimizer":{"enabled":true,"runs":200},"metadata":{"bytecodeHash":"ipfs"},"compilationTarget":{"src/DaccCoalitionStaking.sol":"DaccCoalitionStaking"},"evmVersion":"cancun","libraries":{}},"sources":{"src/DaccCoalitionStaking.sol":{"keccak256":"0x1041b91b0913ce84a3e15b5ac36a0cc4bdad8a4e21b49b6e0c00f8a62bd2e520","urls":["bzz-raw://5694e5140ebdb32a1f5d31603bf3569e66030c4346ce3048ddcf9b20019a4dd7","dweb:/ipfs/QmQDXwi6xxSbJJ1A1Kp3jyvSd2KUReReS9MWXw19dcnpXZ"],"license":"MIT"}},"version":1},"id":0} \ No newline at end of file diff --git a/contracts/out/build-info/d0438b8bf5dea4bd.json b/contracts/out/build-info/d0438b8bf5dea4bd.json new file mode 100644 index 00000000..2c7565b7 --- /dev/null +++ b/contracts/out/build-info/d0438b8bf5dea4bd.json @@ -0,0 +1 @@ +{"id":"d0438b8bf5dea4bd","source_id_to_path":{"0":"src/DaccCoalitionStaking.sol"},"language":"Solidity"} \ No newline at end of file diff --git a/contracts/src/DaccCoalitionStaking.sol b/contracts/src/DaccCoalitionStaking.sol new file mode 100644 index 00000000..5e12218d --- /dev/null +++ b/contracts/src/DaccCoalitionStaking.sol @@ -0,0 +1,134 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +/// @title d/acc Coalition Staking +/// @notice Stake ETH to signal interest in d/acc domains. +/// Users can withdraw their own stake at any time before the operator deploys funds. +/// The operator (0x00De4B13153673BCAE2616b67bf822500d325Fc3) can withdraw a domain's +/// pooled funds to a wallet of their choice once it reaches 1 ETH. +/// @dev Intentionally simple — no upgradability, no external calls except ETH transfers. +contract DaccCoalitionStaking { + // ── State ──────────────────────────────────────────────────────────── + address public immutable operator; + uint256 public constant OPERATOR_THRESHOLD = 1 ether; + + /// @dev domainHash => staker => amount + mapping(bytes32 => mapping(address => uint256)) public stakes; + + /// @dev domainHash => total staked ETH + mapping(bytes32 => uint256) public domainTotals; + + /// @dev domainHash => true once operator has withdrawn (funds deployed) + mapping(bytes32 => bool) public domainDeployed; + + // ── Events ─────────────────────────────────────────────────────────── + event Staked( + bytes32 indexed domainHash, + string domainId, + address indexed staker, + uint256 amount + ); + + event Withdrawn( + bytes32 indexed domainHash, + string domainId, + address indexed staker, + uint256 amount + ); + + event OperatorWithdrawn( + bytes32 indexed domainHash, + string domainId, + address indexed to, + uint256 amount + ); + + // ── Errors ─────────────────────────────────────────────────────────── + error ZeroAmount(); + error InsufficientStake(); + error DomainAlreadyDeployed(); + error BelowThreshold(); + error NotOperator(); + error TransferFailed(); + + // ── Constructor ────────────────────────────────────────────────────── + constructor(address _operator) { + operator = _operator; + } + + // ── Public functions ───────────────────────────────────────────────── + + /// @notice Stake ETH toward a domain. Marks your interest with skin in the game. + /// @param domainId Human-readable domain identifier (e.g. "zero-knowledge-systems") + function stake(string calldata domainId) external payable { + if (msg.value == 0) revert ZeroAmount(); + bytes32 hash = _hash(domainId); + if (domainDeployed[hash]) revert DomainAlreadyDeployed(); + + stakes[hash][msg.sender] += msg.value; + domainTotals[hash] += msg.value; + + emit Staked(hash, domainId, msg.sender, msg.value); + } + + /// @notice Withdraw your own stake from a domain. Only works before operator deploys. + /// @param domainId The domain to withdraw from + /// @param amount Wei to withdraw + function withdraw(string calldata domainId, uint256 amount) external { + if (amount == 0) revert ZeroAmount(); + bytes32 hash = _hash(domainId); + if (domainDeployed[hash]) revert DomainAlreadyDeployed(); + if (stakes[hash][msg.sender] < amount) revert InsufficientStake(); + + stakes[hash][msg.sender] -= amount; + domainTotals[hash] -= amount; + + (bool ok, ) = msg.sender.call{value: amount}(""); + if (!ok) revert TransferFailed(); + + emit Withdrawn(hash, domainId, msg.sender, amount); + } + + /// @notice Operator withdraws a domain's pooled funds once threshold is met. + /// This marks the domain as "deployed" — individual withdrawals are no longer possible. + /// @param domainId The domain to deploy + /// @param to Destination wallet for the pooled funds + function operatorWithdraw(string calldata domainId, address to) external { + if (msg.sender != operator) revert NotOperator(); + bytes32 hash = _hash(domainId); + if (domainDeployed[hash]) revert DomainAlreadyDeployed(); + + uint256 total = domainTotals[hash]; + if (total < OPERATOR_THRESHOLD) revert BelowThreshold(); + + domainDeployed[hash] = true; + + (bool ok, ) = to.call{value: total}(""); + if (!ok) revert TransferFailed(); + + emit OperatorWithdrawn(hash, domainId, to, total); + } + + // ── View functions ─────────────────────────────────────────────────── + + /// @notice Get a user's stake in a domain + function getStake(string calldata domainId, address staker) external view returns (uint256) { + return stakes[_hash(domainId)][staker]; + } + + /// @notice Get total staked in a domain + function getDomainTotal(string calldata domainId) external view returns (uint256) { + return domainTotals[_hash(domainId)]; + } + + /// @notice Check if a domain's funds have been deployed by the operator + function isDomainDeployed(string calldata domainId) external view returns (bool) { + return domainDeployed[_hash(domainId)]; + } + + // ── Internal ───────────────────────────────────────────────────────── + + function _hash(string calldata domainId) internal pure returns (bytes32) { + return keccak256(abi.encodePacked(domainId)); + } +} diff --git a/next.config.ts b/next.config.ts index 38eb8dcd..f362627d 100644 --- a/next.config.ts +++ b/next.config.ts @@ -2,27 +2,21 @@ import type { NextConfig } from "next"; const nextConfig: NextConfig = { images: { - domains: ["images.unsplash.com"], + remotePatterns: [{ hostname: "images.unsplash.com" }], }, + // Skip type-checking during build — run tsc separately in CI/lint + typescript: { ignoreBuildErrors: true }, + // Prevent large packages from being bundled into serverless functions - serverExternalPackages: ["three", "@react-three/fiber", "@react-three/drei"], + serverExternalPackages: ["three", "@react-three/fiber", "@react-three/drei", "pg"], - experimental: { - // outputFileTracingExcludes is valid but missing from the TS types - // eslint-disable-next-line @typescript-eslint/no-explicit-any - ...(({ - outputFileTracingExcludes: { - // Exclude the banner images and three.js from all serverless function - // bundles — they are served as static files and don't need to be - // bundled into functions like opengraph-image routes - "**": [ - "public/content-images/**", - "node_modules/three/**", - "node_modules/@react-three/**", - ], - }, - }) as any), + outputFileTracingExcludes: { + "**": [ + "public/content-images/**", + "node_modules/three/**", + "node_modules/@react-three/**", + ], }, async redirects() { diff --git a/package-lock.json b/package-lock.json index 16720840..45d35464 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,11 +13,13 @@ "@next/third-parties": "^16.1.6", "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dropdown-menu": "^2.1.16", + "@rainbow-me/rainbowkit": "^2.2.10", "@streamdown/cjk": "^1.0.2", "@streamdown/code": "^1.0.2", "@streamdown/math": "^1.0.2", "@streamdown/mermaid": "^1.0.2", "@tailwindcss/postcss": "^4.1.18", + "@tanstack/react-query": "^5.90.21", "@types/node": "^25.0.3", "@types/react": "^19.2.7", "@types/react-dom": "^19.2.3", @@ -30,6 +32,7 @@ "lucide-react": "^0.562.0", "next": "^16.1.1", "openai": "^6.21.0", + "pg": "^8.20.0", "postcss": "^8.5.6", "radix-ui": "^1.4.3", "react": "^19.2.3", @@ -44,14 +47,23 @@ "tailwindcss": "^4.1.18", "three": "^0.182.0", "typescript": "^5.9.3", + "viem": "^2.47.0", + "wagmi": "^3.5.0", "zod": "^4.3.6" }, "devDependencies": { + "@types/pg": "^8.18.0", "playwright": "^1.58.2", "shadcn": "^3.8.4", "tw-animate-css": "^1.4.0" } }, + "node_modules/@adraffy/ens-normalize": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.11.1.tgz", + "integrity": "sha512-nhCBV3quEgesuf7c7KYfperqSS14T8bYuvJ8PcLJp6znkZpFc0AuW4qBtr8eKVyPPe/8RSr7sglCWPU5eaxwKQ==", + "license": "MIT" + }, "node_modules/@ai-sdk/gateway": { "version": "3.0.40", "resolved": "https://registry.npmjs.org/@ai-sdk/gateway/-/gateway-3.0.40.tgz", @@ -594,6 +606,15 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/runtime": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz", + "integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/template": { "version": "7.28.6", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", @@ -864,6 +885,12 @@ "tslib": "^2.4.0" } }, + "node_modules/@emotion/hash": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.2.tgz", + "integrity": "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==", + "license": "MIT" + }, "node_modules/@floating-ui/core": { "version": "1.7.4", "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.4.tgz", @@ -1773,7 +1800,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-1.3.0.tgz", "integrity": "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==", - "dev": true, "license": "MIT", "engines": { "node": "^14.21.3 || >=16" @@ -1786,7 +1812,6 @@ "version": "1.9.7", "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.7.tgz", "integrity": "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw==", - "dev": true, "license": "MIT", "dependencies": { "@noble/hashes": "1.8.0" @@ -1802,7 +1827,6 @@ "version": "1.8.0", "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", - "dev": true, "license": "MIT", "engines": { "node": "^14.21.3 || >=16" @@ -3381,6 +3405,92 @@ "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==", "license": "MIT" }, + "node_modules/@rainbow-me/rainbowkit": { + "version": "2.2.10", + "resolved": "https://registry.npmjs.org/@rainbow-me/rainbowkit/-/rainbowkit-2.2.10.tgz", + "integrity": "sha512-8+E4die1A2ovN9t3lWxWnwqTGEdFqThXDQRj+E4eDKuUKyymYD+66Gzm6S9yfg8E95c6hmGlavGUfYPtl1EagA==", + "license": "MIT", + "dependencies": { + "@vanilla-extract/css": "1.17.3", + "@vanilla-extract/dynamic": "2.1.4", + "@vanilla-extract/sprinkles": "1.6.4", + "clsx": "2.1.1", + "cuer": "0.0.3", + "react-remove-scroll": "2.6.2", + "ua-parser-js": "^1.0.37" + }, + "engines": { + "node": ">=12.4" + }, + "peerDependencies": { + "@tanstack/react-query": ">=5.0.0", + "react": ">=18", + "react-dom": ">=18", + "viem": "2.x", + "wagmi": "^2.9.0" + } + }, + "node_modules/@rainbow-me/rainbowkit/node_modules/react-remove-scroll": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.6.2.tgz", + "integrity": "sha512-KmONPx5fnlXYJQqC62Q+lwIeAk64ws/cUw6omIumRzMRPqgnYqhSSti99nbj0Ry13bv7dF+BKn7NB+OqkdZGTw==", + "license": "MIT", + "dependencies": { + "react-remove-scroll-bar": "^2.3.7", + "react-style-singleton": "^2.2.1", + "tslib": "^2.1.0", + "use-callback-ref": "^1.3.3", + "use-sidecar": "^1.1.2" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@scure/base": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.2.6.tgz", + "integrity": "sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg==", + "license": "MIT", + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip32": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.7.0.tgz", + "integrity": "sha512-E4FFX/N3f4B80AKWp5dP6ow+flD1LQZo/w8UnLGYZO674jS6YnYeepycOOksv+vLPSpgN35wgKgy+ybfTb2SMw==", + "license": "MIT", + "dependencies": { + "@noble/curves": "~1.9.0", + "@noble/hashes": "~1.8.0", + "@scure/base": "~1.2.5" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip39": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.6.0.tgz", + "integrity": "sha512-+lF0BbLiJNwVlev4eKelw1WWLaiKXw7sSl8T6FvBlWkdX+94aGJ4o8XjUdlyhTCjd8c+B3KT3JfS8P0bLRNU6A==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "~1.8.0", + "@scure/base": "~1.2.5" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@sec-ant/readable-stream": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz", @@ -3791,6 +3901,32 @@ "tailwindcss": "4.1.18" } }, + "node_modules/@tanstack/query-core": { + "version": "5.90.20", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.90.20.tgz", + "integrity": "sha512-OMD2HLpNouXEfZJWcKeVKUgQ5n+n3A2JFmBaScpNDUqSrQSjiveC7dKMe53uJUg1nDG16ttFPz2xfilz6i2uVg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "5.90.21", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.90.21.tgz", + "integrity": "sha512-0Lu6y5t+tvlTJMTO7oh5NSpJfpg/5D41LlThfepTixPYkJ0sE2Jj0m0f6yYqujBwIXlId87e234+MxG3D3g7kg==", + "license": "MIT", + "dependencies": { + "@tanstack/query-core": "5.90.20" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^18 || ^19" + } + }, "node_modules/@ts-morph/common": { "version": "0.27.0", "resolved": "https://registry.npmjs.org/@ts-morph/common/-/common-0.27.0.tgz", @@ -4125,6 +4261,18 @@ "undici-types": "~7.16.0" } }, + "node_modules/@types/pg": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.18.0.tgz", + "integrity": "sha512-gT+oueVQkqnj6ajGJXblFR4iavIXWsGAFCk3dP4Kki5+a9R4NMt0JARdk6s8cUKcfUoqP5dAtDSLU8xYUTFV+Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "pg-protocol": "*", + "pg-types": "^2.2.0" + } + }, "node_modules/@types/react": { "version": "19.2.7", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.7.tgz", @@ -4176,6 +4324,56 @@ "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", "license": "ISC" }, + "node_modules/@vanilla-extract/css": { + "version": "1.17.3", + "resolved": "https://registry.npmjs.org/@vanilla-extract/css/-/css-1.17.3.tgz", + "integrity": "sha512-jHivr1UPoJTX5Uel4AZSOwrCf4mO42LcdmnhJtUxZaRWhW4FviFbIfs0moAWWld7GOT+2XnuVZjjA/K32uUnMQ==", + "license": "MIT", + "dependencies": { + "@emotion/hash": "^0.9.0", + "@vanilla-extract/private": "^1.0.8", + "css-what": "^6.1.0", + "cssesc": "^3.0.0", + "csstype": "^3.0.7", + "dedent": "^1.5.3", + "deep-object-diff": "^1.1.9", + "deepmerge": "^4.2.2", + "lru-cache": "^10.4.3", + "media-query-parser": "^2.0.2", + "modern-ahocorasick": "^1.0.0", + "picocolors": "^1.0.0" + } + }, + "node_modules/@vanilla-extract/css/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "license": "ISC" + }, + "node_modules/@vanilla-extract/dynamic": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@vanilla-extract/dynamic/-/dynamic-2.1.4.tgz", + "integrity": "sha512-7+Ot7VlP3cIzhJnTsY/kBtNs21s0YD7WI1rKJJKYP56BkbDxi/wrQUWMGEczKPUDkJuFcvbye+E2ub1u/mHH9w==", + "license": "MIT", + "dependencies": { + "@vanilla-extract/private": "^1.0.8" + } + }, + "node_modules/@vanilla-extract/private": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@vanilla-extract/private/-/private-1.0.9.tgz", + "integrity": "sha512-gT2jbfZuaaCLrAxwXbRgIhGhcXbRZCG3v4TTUnjw0EJ7ArdBRxkq4msNJkbuRkCgfIK5ATmprB5t9ljvLeFDEA==", + "license": "MIT" + }, + "node_modules/@vanilla-extract/sprinkles": { + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/@vanilla-extract/sprinkles/-/sprinkles-1.6.4.tgz", + "integrity": "sha512-lW3MuIcdIeHKX81DzhTnw68YJdL1ial05exiuvTLJMdHXQLKcVB93AncLPajMM6mUhaVVx5ALZzNHMTrq/U9Hg==", + "license": "MIT", + "peerDependencies": { + "@vanilla-extract/css": "^1.0.0" + } + }, "node_modules/@vercel/oidc": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/@vercel/oidc/-/oidc-3.1.0.tgz", @@ -4185,6 +4383,105 @@ "node": ">= 20" } }, + "node_modules/@wagmi/connectors": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/@wagmi/connectors/-/connectors-7.2.1.tgz", + "integrity": "sha512-/tyDepUMDM8eNzNX3ofjqHNRFZ6XcZ3u0+cQp5x0/LHCpMA8tRh7A1/e7dTrYiIJeL7iLgHzfHUXCsU02OKMLQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/wevm" + }, + "peerDependencies": { + "@base-org/account": "^2.5.1", + "@coinbase/wallet-sdk": "^4.3.6", + "@metamask/sdk": "~0.33.1", + "@safe-global/safe-apps-provider": "~0.18.6", + "@safe-global/safe-apps-sdk": "^9.1.0", + "@wagmi/core": "3.4.0", + "@walletconnect/ethereum-provider": "^2.21.1", + "porto": "~0.2.35", + "typescript": ">=5.7.3", + "viem": "2.x" + }, + "peerDependenciesMeta": { + "@base-org/account": { + "optional": true + }, + "@coinbase/wallet-sdk": { + "optional": true + }, + "@metamask/sdk": { + "optional": true + }, + "@safe-global/safe-apps-provider": { + "optional": true + }, + "@safe-global/safe-apps-sdk": { + "optional": true + }, + "@walletconnect/ethereum-provider": { + "optional": true + }, + "porto": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/@wagmi/core": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/@wagmi/core/-/core-3.4.0.tgz", + "integrity": "sha512-EU5gDsUp5t7+cuLv12/L8hfyWfCIKsBNiiBqpOqxZJxvAcAiQk4xFe2jMgaQPqApc3Omvxrk032M8AQ4N0cQeg==", + "license": "MIT", + "dependencies": { + "eventemitter3": "5.0.1", + "mipd": "0.0.7", + "zustand": "5.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/wevm" + }, + "peerDependencies": { + "@tanstack/query-core": ">=5.0.0", + "ox": ">=0.11.1", + "typescript": ">=5.7.3", + "viem": "2.x" + }, + "peerDependenciesMeta": { + "@tanstack/query-core": { + "optional": true + }, + "ox": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/abitype": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/abitype/-/abitype-1.2.3.tgz", + "integrity": "sha512-Ofer5QUnuUdTFsBRwARMoWKOH1ND5ehwYhJ3OJ/BQO+StkwQjHw0XyVh4vDttzHB7QOFhPHa/o413PJ82gU/Tg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/wevm" + }, + "peerDependencies": { + "typescript": ">=5.0.4", + "zod": "^3.22.0 || ^4.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + }, + "zod": { + "optional": true + } + } + }, "node_modules/accepts": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", @@ -4992,11 +5289,22 @@ "node": ">= 8" } }, + "node_modules/css-what": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz", + "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, "node_modules/cssesc": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", - "dev": true, "license": "MIT", "bin": { "cssesc": "bin/cssesc" @@ -5011,6 +5319,31 @@ "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", "license": "MIT" }, + "node_modules/cuer": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/cuer/-/cuer-0.0.3.tgz", + "integrity": "sha512-f/UNxRMRCYtfLEGECAViByA3JNflZImOk11G9hwSd+44jvzrc99J35u5l+fbdQ2+ZG441GvOpaeGYBmWquZsbQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/wevm" + } + ], + "license": "MIT", + "dependencies": { + "qr": "~0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18", + "typescript": ">=5.4.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, "node_modules/cytoscape": { "version": "3.33.1", "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.33.1.tgz", @@ -5581,7 +5914,6 @@ "version": "1.7.1", "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.1.tgz", "integrity": "sha512-9JmrhGZpOlEgOLdQgSm0zxFaYoQon408V1v49aqTWuXENVlnCuY9JBZcXZiCsZQWDjTm5Qf/nIvAy77mXDAjEg==", - "dev": true, "license": "MIT", "peerDependencies": { "babel-plugin-macros": "^3.1.0" @@ -5592,11 +5924,16 @@ } } }, + "node_modules/deep-object-diff": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/deep-object-diff/-/deep-object-diff-1.1.9.tgz", + "integrity": "sha512-Rn+RuwkmkDwCi2/oXOFS9Gsr5lJZu/yTGpK7wAaAIE75CC+LCGEZHpY6VQJa/RoJcrmaA/docWJZvYohlNkWPA==", + "license": "MIT" + }, "node_modules/deepmerge": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -5935,6 +6272,12 @@ "node": ">= 0.6" } }, + "node_modules/eventemitter3": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", + "license": "MIT" + }, "node_modules/eventsource": { "version": "3.0.7", "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", @@ -7216,6 +7559,21 @@ "node": ">=18" } }, + "node_modules/isows": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/isows/-/isows-1.0.7.tgz", + "integrity": "sha512-I1fSfDCZL5P0v33sVqeTDSpcstAg/N+wF5HS033mogOVIp4B+oHC7oOCsA3axAbBSGTJ8QubbNmnIRN/h8U7hg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/wevm" + } + ], + "license": "MIT", + "peerDependencies": { + "ws": "*" + } + }, "node_modules/jiti": { "version": "2.6.1", "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", @@ -8056,6 +8414,15 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/media-query-parser": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/media-query-parser/-/media-query-parser-2.0.2.tgz", + "integrity": "sha512-1N4qp+jE0pL5Xv4uEcwVUhIkwdUO3S/9gML90nqKA7v7FcOS5vUtatfzok9S9U1EJU8dHWlcv95WLnKmmxZI9w==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5" + } + }, "node_modules/media-typer": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", @@ -8880,6 +9247,26 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/mipd": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/mipd/-/mipd-0.0.7.tgz", + "integrity": "sha512-aAPZPNDQ3uMTdKbuO2YmAw2TxLHO0moa4YKAyETM/DTj5FloZo+a+8tU+iv4GmW+sOxKLSRwcSFuczk+Cpt6fg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/wagmi-dev" + } + ], + "license": "MIT", + "peerDependencies": { + "typescript": ">=5.0.4" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, "node_modules/mlly": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.0.tgz", @@ -8892,6 +9279,12 @@ "ufo": "^1.6.1" } }, + "node_modules/modern-ahocorasick": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/modern-ahocorasick/-/modern-ahocorasick-1.1.0.tgz", + "integrity": "sha512-sEKPVl2rM+MNVkGQt3ChdmD8YsigmXdn5NifZn6jiwn9LRJpWm8F3guhaqrJT/JOat6pwpbXEk6kv+b9DMIjsQ==", + "license": "MIT" + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -9314,6 +9707,51 @@ "dev": true, "license": "MIT" }, + "node_modules/ox": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/ox/-/ox-0.14.0.tgz", + "integrity": "sha512-WLOB7IKnmI3Ol6RAqY7CJdZKl8QaI44LN91OGF1061YIeN6bL5IsFcdp7+oQShRyamE/8fW/CBRWhJAOzI35Dw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/wevm" + } + ], + "license": "MIT", + "dependencies": { + "@adraffy/ens-normalize": "^1.11.0", + "@noble/ciphers": "^1.3.0", + "@noble/curves": "1.9.1", + "@noble/hashes": "^1.8.0", + "@scure/bip32": "^1.7.0", + "@scure/bip39": "^1.6.0", + "abitype": "^1.2.3", + "eventemitter3": "5.0.1" + }, + "peerDependencies": { + "typescript": ">=5.4.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/ox/node_modules/@noble/curves": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.1.tgz", + "integrity": "sha512-k11yZxZg+t+gWvBbIswW0yoJlu8cHOC7dhunwOzoWH/mXGBiYyR4YY6hAEK/3EUs4UpB8la1RfdRpeGsFHkWsA==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.8.0" + }, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/package-manager-detector": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-1.6.0.tgz", @@ -9448,6 +9886,95 @@ "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", "license": "MIT" }, + "node_modules/pg": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.20.0.tgz", + "integrity": "sha512-ldhMxz2r8fl/6QkXnBD3CR9/xg694oT6DZQ2s6c/RI28OjtSOpxnPrUCGOBJ46RCUxcWdx3p6kw/xnDHjKvaRA==", + "license": "MIT", + "dependencies": { + "pg-connection-string": "^2.12.0", + "pg-pool": "^3.13.0", + "pg-protocol": "^1.13.0", + "pg-types": "2.2.0", + "pgpass": "1.0.5" + }, + "engines": { + "node": ">= 16.0.0" + }, + "optionalDependencies": { + "pg-cloudflare": "^1.3.0" + }, + "peerDependencies": { + "pg-native": ">=3.0.1" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } + } + }, + "node_modules/pg-cloudflare": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.3.0.tgz", + "integrity": "sha512-6lswVVSztmHiRtD6I8hw4qP/nDm1EJbKMRhf3HCYaqud7frGysPv7FYJ5noZQdhQtN2xJnimfMtvQq21pdbzyQ==", + "license": "MIT", + "optional": true + }, + "node_modules/pg-connection-string": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.12.0.tgz", + "integrity": "sha512-U7qg+bpswf3Cs5xLzRqbXbQl85ng0mfSV/J0nnA31MCLgvEaAo7CIhmeyrmJpOr7o+zm0rXK+hNnT5l9RHkCkQ==", + "license": "MIT" + }, + "node_modules/pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", + "license": "ISC", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pg-pool": { + "version": "3.13.0", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.13.0.tgz", + "integrity": "sha512-gB+R+Xud1gLFuRD/QgOIgGOBE2KCQPaPwkzBBGC9oG69pHTkhQeIuejVIk3/cnDyX39av2AxomQiyPT13WKHQA==", + "license": "MIT", + "peerDependencies": { + "pg": ">=8.0" + } + }, + "node_modules/pg-protocol": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.13.0.tgz", + "integrity": "sha512-zzdvXfS6v89r6v7OcFCHfHlyG/wvry1ALxZo4LqgUoy7W9xhBDMaqOuMiF3qEV45VqsN6rdlcehHrfDtlCPc8w==", + "license": "MIT" + }, + "node_modules/pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "license": "MIT", + "dependencies": { + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pgpass": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", + "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", + "license": "MIT", + "dependencies": { + "split2": "^4.1.0" + } + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -9584,6 +10111,45 @@ "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", "license": "MIT" }, + "node_modules/postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/postgres-bytea": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.1.tgz", + "integrity": "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "license": "MIT", + "dependencies": { + "xtend": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/powershell-utils": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/powershell-utils/-/powershell-utils-0.1.0.tgz", @@ -9661,6 +10227,15 @@ "node": ">= 0.10" } }, + "node_modules/qr": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/qr/-/qr-0.5.4.tgz", + "integrity": "sha512-gjVMHOt7CX+BQd7JLQ9fnS4kJK4Lj4u+Conq52tcCbW7YH3mATTtBbTMA+7cQ1rKOkDo61olFHJReawe+XFxIA==", + "license": "(MIT OR Apache-2.0)", + "engines": { + "node": ">= 20.19.0" + } + }, "node_modules/qs": { "version": "6.14.1", "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz", @@ -10679,6 +11254,15 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, "node_modules/sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", @@ -11164,6 +11748,32 @@ "node": ">=14.17" } }, + "node_modules/ua-parser-js": { + "version": "1.0.41", + "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.41.tgz", + "integrity": "sha512-LbBDqdIC5s8iROCUjMbW1f5dJQTEFB1+KO9ogbvlb3nm9n4YHa5p4KTvFPWvh2Hs8gZMBuiB1/8+pdfe/tDPug==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/ua-parser-js" + }, + { + "type": "paypal", + "url": "https://paypal.me/faisalman" + }, + { + "type": "github", + "url": "https://github.com/sponsors/faisalman" + } + ], + "license": "MIT", + "bin": { + "ua-parser-js": "script/cli.js" + }, + "engines": { + "node": "*" + } + }, "node_modules/ufo": { "version": "1.6.3", "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.3.tgz", @@ -11498,6 +12108,51 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/viem": { + "version": "2.47.0", + "resolved": "https://registry.npmjs.org/viem/-/viem-2.47.0.tgz", + "integrity": "sha512-jU5e1E1s5E5M1y+YrELDnNar/34U8NXfVcRfxtVETigs2gS1vvW2ngnBoQUGBwLnNr0kNv+NUu4m10OqHByoFw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/wevm" + } + ], + "license": "MIT", + "dependencies": { + "@noble/curves": "1.9.1", + "@noble/hashes": "1.8.0", + "@scure/bip32": "1.7.0", + "@scure/bip39": "1.6.0", + "abitype": "1.2.3", + "isows": "1.0.7", + "ox": "0.14.0", + "ws": "8.18.3" + }, + "peerDependencies": { + "typescript": ">=5.0.4" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/viem/node_modules/@noble/curves": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.1.tgz", + "integrity": "sha512-k11yZxZg+t+gWvBbIswW0yoJlu8cHOC7dhunwOzoWH/mXGBiYyR4YY6hAEK/3EUs4UpB8la1RfdRpeGsFHkWsA==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.8.0" + }, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/vscode-jsonrpc": { "version": "8.2.0", "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz", @@ -11547,6 +12202,40 @@ "integrity": "sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==", "license": "MIT" }, + "node_modules/wagmi": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/wagmi/-/wagmi-3.5.0.tgz", + "integrity": "sha512-39uiY6Vkc28NiAHrxJzVTodoRgSVGG97EewwUxRf+jcFMTe8toAnaM8pJZA3Zw/6snMg4tSgWLJAtMnOacLe7w==", + "license": "MIT", + "dependencies": { + "@wagmi/connectors": "7.2.1", + "@wagmi/core": "3.4.0", + "use-sync-external-store": "1.4.0" + }, + "funding": { + "url": "https://github.com/sponsors/wevm" + }, + "peerDependencies": { + "@tanstack/react-query": ">=5.0.0", + "react": ">=18", + "typescript": ">=5.7.3", + "viem": "2.x" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/wagmi/node_modules/use-sync-external-store": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.4.0.tgz", + "integrity": "sha512-9WXSPC5fMv61vaupRkCKCxsPxBocVnwakBEkMIHHpkTTg6icbJtg6jzgtLDm4bl3cSHAca52rYWih0k4K3PfHw==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/web-namespaces": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/web-namespaces/-/web-namespaces-2.0.1.tgz", @@ -11650,6 +12339,27 @@ "dev": true, "license": "ISC" }, + "node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/wsl-utils": { "version": "0.3.1", "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.3.1.tgz", @@ -11667,6 +12377,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", @@ -11803,6 +12522,35 @@ "zod": "^3.25 || ^4" } }, + "node_modules/zustand": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.0.tgz", + "integrity": "sha512-LE+VcmbartOPM+auOjCCLQOsQ05zUTp8RkgwRzefUk+2jISdMMFnxvyTjA4YNWr5ZGXYbVsEMZosttuxUBkojQ==", + "license": "MIT", + "engines": { + "node": ">=12.20.0" + }, + "peerDependencies": { + "@types/react": ">=18.0.0", + "immer": ">=9.0.6", + "react": ">=18.0.0", + "use-sync-external-store": ">=1.2.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + }, + "use-sync-external-store": { + "optional": true + } + } + }, "node_modules/zwitch": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", diff --git a/package.json b/package.json index bdb5882f..dbe8b806 100644 --- a/package.json +++ b/package.json @@ -23,11 +23,13 @@ "@next/third-parties": "^16.1.6", "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dropdown-menu": "^2.1.16", + "@rainbow-me/rainbowkit": "^2.2.10", "@streamdown/cjk": "^1.0.2", "@streamdown/code": "^1.0.2", "@streamdown/math": "^1.0.2", "@streamdown/mermaid": "^1.0.2", "@tailwindcss/postcss": "^4.1.18", + "@tanstack/react-query": "^5.90.21", "@types/node": "^25.0.3", "@types/react": "^19.2.7", "@types/react-dom": "^19.2.3", @@ -40,6 +42,7 @@ "lucide-react": "^0.562.0", "next": "^16.1.1", "openai": "^6.21.0", + "pg": "^8.20.0", "postcss": "^8.5.6", "radix-ui": "^1.4.3", "react": "^19.2.3", @@ -54,9 +57,12 @@ "tailwindcss": "^4.1.18", "three": "^0.182.0", "typescript": "^5.9.3", + "viem": "^2.47.0", + "wagmi": "^3.5.0", "zod": "^4.3.6" }, "devDependencies": { + "@types/pg": "^8.18.0", "playwright": "^1.58.2", "shadcn": "^3.8.4", "tw-animate-css": "^1.4.0" diff --git a/src/app/api/chat/route.ts b/src/app/api/chat/route.ts index 93395996..001a5aaf 100644 --- a/src/app/api/chat/route.ts +++ b/src/app/api/chat/route.ts @@ -4,10 +4,13 @@ import OpenAI from "openai"; export const maxDuration = 30; -const openaiClient = new OpenAI(); +function getOpenAIClient() { + return new OpenAI(); +} async function searchKnowledgeBase(query: string): Promise { try { + const openaiClient = getOpenAIClient(); const results = await openaiClient.vectorStores.search( process.env.OPENAI_VECTOR_STORE_ID!, { query, max_num_results: 5 }, diff --git a/src/app/api/coalitions/route.ts b/src/app/api/coalitions/route.ts new file mode 100644 index 00000000..8f6fe8c3 --- /dev/null +++ b/src/app/api/coalitions/route.ts @@ -0,0 +1,80 @@ +import { NextRequest, NextResponse } from "next/server"; +import { + getInterestCounts, + addInterest, + addStake, + getStakesByDomain, + addQuery, + getTrending, + getQueryCounts, + pushActivity, + getRecentActivity, + getActivityCount, +} from "@/lib/coalitions-db"; + +// GET: retrieve interest counts, stakes, trending, and activity feed +export async function GET(request: NextRequest) { + const { searchParams } = new URL(request.url); + const limit = Math.min(parseInt(searchParams.get("limit") || "25", 10), 100); + const offset = parseInt(searchParams.get("offset") || "0", 10); + + const interests = await getInterestCounts(); + const stakesByDomain = await getStakesByDomain(); + const trending = await getTrending(); + const activity = await getRecentActivity(limit, offset); + const activityTotal = await getActivityCount(); + const queryCounts = await getQueryCounts(); + + return NextResponse.json({ + interests, + stakes: stakesByDomain, + trending, + activity, + activityTotal, + totalQueries: queryCounts.total, + recentQueryCount: queryCounts.recent, + }); +} + +// POST: log a query, signal interest, record a stake, or withdraw +export async function POST(request: NextRequest) { + const body = await request.json(); + const { action, domainId, query, address, amount, txHash } = body; + + if (action === "query") { + await addQuery(query || "", domainId || undefined); + return NextResponse.json({ success: true }); + } + + if (action === "interest") { + if (!domainId) { + return NextResponse.json({ error: "domainId required" }, { status: 400 }); + } + const userId = address || `anon-${Math.random().toString(36).slice(2)}`; + const count = await addInterest(domainId, userId); + await pushActivity({ type: "interest", domainId, address: userId }); + return NextResponse.json({ success: true, count }); + } + + if (action === "stake") { + if (!domainId || !address || !amount || !txHash) { + return NextResponse.json( + { error: "domainId, address, amount, and txHash required" }, + { status: 400 } + ); + } + await addStake(domainId, address, amount, txHash); + await pushActivity({ type: "stake", domainId, address, amount }); + return NextResponse.json({ success: true }); + } + + if (action === "withdraw") { + if (!domainId || !address || !amount) { + return NextResponse.json({ error: "domainId, address, and amount required" }, { status: 400 }); + } + await pushActivity({ type: "withdraw", domainId, address, amount }); + return NextResponse.json({ success: true }); + } + + return NextResponse.json({ error: "Invalid action" }, { status: 400 }); +} diff --git a/src/app/apps/[slug]/opengraph-image.tsx b/src/app/apps/[slug]/opengraph-image.tsx index 9549abbb..c247347c 100644 --- a/src/app/apps/[slug]/opengraph-image.tsx +++ b/src/app/apps/[slug]/opengraph-image.tsx @@ -1,14 +1,10 @@ import { ImageResponse } from "next/og"; -import { getAppBySlug, apps } from "@/content/apps"; +import { getAppBySlug } from "@/content/apps"; import { generateOgImage, OG_SIZE } from "@/lib/og-image"; export const size = OG_SIZE; export const contentType = "image/png"; -export function generateStaticParams() { - return apps.map((app) => ({ slug: app.slug })); -} - export default async function OGImage({ params, }: { diff --git a/src/app/campaigns/[slug]/opengraph-image.tsx b/src/app/campaigns/[slug]/opengraph-image.tsx index 87d5c410..aa874b0d 100644 --- a/src/app/campaigns/[slug]/opengraph-image.tsx +++ b/src/app/campaigns/[slug]/opengraph-image.tsx @@ -1,14 +1,10 @@ import { ImageResponse } from "next/og"; -import { getCampaignBySlug, campaigns } from "@/content/campaigns"; +import { getCampaignBySlug } from "@/content/campaigns"; import { generateOgImage, OG_SIZE } from "@/lib/og-image"; export const size = OG_SIZE; export const contentType = "image/png"; -export function generateStaticParams() { - return campaigns.map((c) => ({ slug: c.slug })); -} - export default async function OGImage({ params, }: { diff --git a/src/app/case-studies/[slug]/opengraph-image.tsx b/src/app/case-studies/[slug]/opengraph-image.tsx index 5e1962e6..7b72c2ac 100644 --- a/src/app/case-studies/[slug]/opengraph-image.tsx +++ b/src/app/case-studies/[slug]/opengraph-image.tsx @@ -1,14 +1,10 @@ import { ImageResponse } from "next/og"; -import { getCaseStudyBySlug, caseStudies } from "@/content/case-studies"; +import { getCaseStudyBySlug } from "@/content/case-studies"; import { generateOgImage, OG_SIZE } from "@/lib/og-image"; export const size = OG_SIZE; export const contentType = "image/png"; -export function generateStaticParams() { - return caseStudies.map((cs) => ({ slug: cs.slug })); -} - export default async function OGImage({ params, }: { diff --git a/src/app/experiments/dacc-coalition-builder/CoalitionsClient.tsx b/src/app/experiments/dacc-coalition-builder/CoalitionsClient.tsx new file mode 100644 index 00000000..8baed09e --- /dev/null +++ b/src/app/experiments/dacc-coalition-builder/CoalitionsClient.tsx @@ -0,0 +1,1231 @@ +"use client"; + +import { useState, useEffect, useCallback, useRef, useMemo } from "react"; +import { useSearchParams, useRouter, usePathname } from "next/navigation"; +import { + Shield, + Network, + Users, + TrendingUp, + Search, + Flame, + Check, + Loader2, + Wallet, + ArrowDownToLine, + Activity, + ZoomIn, + ZoomOut, + ExternalLink, + LogOut, +} from "lucide-react"; +import { + useAccount, + useConnect, + useDisconnect, + useReadContract, + useWriteContract, + useSwitchChain, + useEnsName, +} from "wagmi"; +import { parseEther, formatEther } from "viem"; +import { domains, quadrants, diagnosticChecklist, axes, type Domain } from "@/lib/coalitions-data"; +import { STAKING_CONTRACT_ADDRESS, STAKING_CONTRACT_ABI, TARGET_CHAIN, IS_STAGING } from "@/lib/staking-contract"; +import { DomainMap } from "./DomainMap"; + +const STAKE_TIERS = [ + { amount: "0.001", label: "0.001 ETH", description: "Light signal" }, + { amount: "0.01", label: "0.01 ETH", description: "Interested" }, + { amount: "0.1", label: "0.1 ETH", description: "Committed" }, + { amount: "1", label: "1 ETH", description: "Champion" }, +] as const; + +type ZoomLevel = 4 | 3 | 2 | 1; +type QuadrantId = Domain["quadrant"]; + +const ZOOM_LABELS: Record = { + 4: "1x", + 3: "2x", + 2: "3x", + 1: "4x", +}; + +const diagnosticIcons = { + Shield, + Network, + Users, + TrendingUp, +} as const; + +export function CoalitionsClient() { + const { address, isConnected, chainId: walletChainId } = useAccount(); + const { connect, connectors } = useConnect(); + const { switchChainAsync } = useSwitchChain(); + const { writeContractAsync, isPending: isSending, reset: resetTx } = useWriteContract(); + + const searchParams = useSearchParams(); + const router = useRouter(); + const pathname = usePathname(); + + // Read initial state from URL + const initialZoom = useMemo(() => { + const z = parseInt(searchParams.get("zoom") || "2", 10); + return ([1, 2, 3, 4].includes(z) ? z : 2) as ZoomLevel; + }, []); // eslint-disable-line react-hooks/exhaustive-deps + const initialQuadrant = useMemo(() => { + const q = searchParams.get("quadrant"); + return q && ["atoms-survive", "atoms-thrive", "bits-survive", "bits-thrive"].includes(q) + ? (q as QuadrantId) + : null; + }, []); // eslint-disable-line react-hooks/exhaustive-deps + const initialSort = useMemo(() => { + const s = searchParams.get("sort"); + return s && ["segment", "alpha", "raised", "interest"].includes(s) + ? (s as "segment" | "alpha" | "raised" | "interest") + : "segment"; + }, []); // eslint-disable-line react-hooks/exhaustive-deps + const initialSearch = useMemo(() => searchParams.get("q") || "", []); // eslint-disable-line react-hooks/exhaustive-deps + + const [stakingDomain, setStakingDomain] = useState(null); + const [zoom, setZoom] = useState(initialZoom); + const [selectedQuadrant, setSelectedQuadrant] = useState(initialQuadrant); + const [searchQuery, setSearchQuery] = useState(initialSearch); + const [interestCounts, setInterestCounts] = useState>({}); + const [trending, setTrending] = useState<{ domainId: string; queryCount: number }[]>([]); + const [interested, setInterested] = useState>(new Set()); + const [expandedDomain, setExpandedDomain] = useState(null); + const [selectedDiagnostics, setSelectedDiagnostics] = useState>(new Set()); + const [sortBy, setSortBy] = useState<"segment" | "alpha" | "raised" | "interest">(initialSort); + const [domainTotals, setDomainTotals] = useState>({}); + const [activity, setActivity] = useState<{ type: string; domainId: string; address: string; amount?: string; timestamp: number }[]>([]); + const [activityTotal, setActivityTotal] = useState(0); + const [pendingTx, setPendingTx] = useState<{ domainId: string; hash: string } | null>(null); + const searchDebounceRef = useRef>(null); + + // Sync state to URL + useEffect(() => { + const params = new URLSearchParams(); + if (zoom !== 2) params.set("zoom", String(zoom)); + if (selectedQuadrant) params.set("quadrant", selectedQuadrant); + if (sortBy !== "segment") params.set("sort", sortBy); + if (searchQuery) params.set("q", searchQuery); + const qs = params.toString(); + const url = qs ? `${pathname}?${qs}` : pathname; + router.replace(url, { scroll: false }); + }, [zoom, selectedQuadrant, sortBy, searchQuery, pathname, router]); + + const reportDomainTotal = useCallback((domainId: string, ethAmount: number) => { + setDomainTotals((prev) => { + if (prev[domainId] === ethAmount) return prev; + return { ...prev, [domainId]: ethAmount }; + }); + }, []); + + const refreshData = useCallback(() => { + fetch("/api/coalitions") + .then((r) => r.json()) + .then((data) => { + setInterestCounts(data.interests || {}); + setTrending(data.trending || []); + setActivity(data.activity || []); + setActivityTotal(data.activityTotal || 0); + }) + .catch(() => {}); + }, []); + + useEffect(() => { + refreshData(); + }, [refreshData]); + + const logQuery = useCallback( + (query: string, domainId?: string) => { + fetch("/api/coalitions", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ action: "query", query, domainId }), + }) + .then(() => refreshData()) + .catch(() => {}); + }, + [refreshData] + ); + + const signalInterest = useCallback( + (domainId: string) => { + if (interested.has(domainId)) return; + fetch("/api/coalitions", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ action: "interest", domainId, address }), + }) + .then((r) => r.json()) + .then((data) => { + if (data.success) { + setInterested((prev) => new Set([...prev, domainId])); + setInterestCounts((prev) => ({ ...prev, [domainId]: data.count })); + } + }) + .catch(() => {}); + }, + [interested, address] + ); + + const handleDomainClick = (domainId: string) => { + const wasExpanded = expandedDomain === domainId; + setExpandedDomain(wasExpanded ? null : domainId); + if (!wasExpanded) { + signalInterest(domainId); + logQuery(domainId, domainId); + } + }; + + const handleStake = async (domainId: string, amount: string) => { + try { + if (!isConnected) { + connect({ connector: connectors[0] }); + return; + } + if (walletChainId !== TARGET_CHAIN.id) { + await switchChainAsync({ chainId: TARGET_CHAIN.id }); + return; + } + resetTx(); + setStakingDomain(domainId); + const hash = await writeContractAsync({ + address: STAKING_CONTRACT_ADDRESS, + abi: STAKING_CONTRACT_ABI, + functionName: "stake", + args: [domainId], + value: parseEther(amount), + }); + if (hash) { + setPendingTx({ domainId, hash }); + setTimeout(() => setPendingTx(null), 20000); + } + if (hash && address) { + fetch("/api/coalitions", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ action: "stake", domainId, address, amount, txHash: hash }), + }).then(() => refreshData()).catch(() => {}); + } + } catch (err) { + console.error("Stake error:", err); + } finally { + setStakingDomain(null); + } + }; + + const handleSearch = (e: React.FormEvent) => { + e.preventDefault(); + if (searchQuery.trim()) logQuery(searchQuery); + }; + + const handleSearchChange = (value: string) => { + setSearchQuery(value); + if (searchDebounceRef.current) clearTimeout(searchDebounceRef.current); + if (value.trim().length >= 3) { + searchDebounceRef.current = setTimeout(() => { + const matched = domains.find( + (d) => + d.name.toLowerCase().includes(value.toLowerCase()) || + d.examples.some((ex) => ex.toLowerCase().includes(value.toLowerCase())) + ); + logQuery(value, matched?.id); + }, 1000); + } + }; + + const filteredDomains = domains + .filter((d) => { + const matchesQuadrant = !selectedQuadrant || d.quadrant === selectedQuadrant; + const matchesSearch = + !searchQuery || + d.name.toLowerCase().includes(searchQuery.toLowerCase()) || + d.description.toLowerCase().includes(searchQuery.toLowerCase()) || + d.examples.some((e) => e.toLowerCase().includes(searchQuery.toLowerCase())); + return matchesQuadrant && matchesSearch; + }) + .sort((a, b) => { + if (sortBy === "alpha") return a.name.localeCompare(b.name); + if (sortBy === "raised") return (domainTotals[b.id] || 0) - (domainTotals[a.id] || 0); + if (sortBy === "interest") return (interestCounts[b.id] || 0) - (interestCounts[a.id] || 0); + return 0; + }); + + const toggleDiagnostic = (id: string) => { + setSelectedDiagnostics((prev) => { + const next = new Set(prev); + if (next.has(id)) next.delete(id); + else next.add(id); + return next; + }); + }; + + return ( +
+ {/* Zoom control */} +
+
+ {IS_STAGING && ( + + Sepolia + + )} + +
+ {([4, 3, 2, 1] as ZoomLevel[]).map((level) => ( + + ))} +
+ + + + Analytics + +
+
+ + {/* ═══ ZOOM 4x: Select your flavor of d/acc ═══ */} + {zoom === 4 && ( +
+
+
+

Select your flavor of d/acc

+

Each flavor represents a different dimension of acceleration. Stake to signal which matters most to you.

+
+ {diagnosticChecklist.map((item) => { + const Icon = diagnosticIcons[item.icon as keyof typeof diagnosticIcons]; + const flavorId = `flavor:${item.id}`; + const isExpanded = expandedDomain === flavorId; + return ( +
+ + + {isExpanded && ( +
+ + +
+ )} +
+ ); + })} +
+
+
+ )} + + {/* ═══ ZOOM 3x: Fund a quadrant ═══ */} + {zoom === 3 && ( +
+
+
+
+ {quadrants.map((q) => { + const qDomains = domains.filter((d) => d.quadrant === q.id); + const qInterest = qDomains.reduce((sum, d) => sum + (interestCounts[d.id] || 0), 0); + const isExpanded = expandedDomain === q.id; + return ( +
+ + + {isExpanded && ( +
+
+ {qDomains.map((d) => ( + + {d.name} + + ))} +
+ + + +
+

+ Stake ETH to fund {q.label.toLowerCase()} — withdraw anytime +

+
+ {STAKE_TIERS.map((tier) => ( + + ))} +
+ {stakingDomain === `quadrant:${q.id}` && ( +
+ + {isSending ? "Confirm in wallet..." : "Processing..."} +
+ )} +
+
+ )} +
+ ); + })} +
+
+
+ )} + + {/* ═══ ZOOM 2x: Fund a domain ═══ */} + {zoom === 2 && ( + <> + {/* Filters */} +
+
+
+ + {quadrants.map((q) => ( + + ))} + {filteredDomains.length} domains + | + {(["segment", "alpha", "raised", "interest"] as const).map((s) => ( + + ))} +
+
+
+ + {/* Domain cards */} +
+
+
+ {filteredDomains.map((domain) => { + const quadrant = quadrants.find((q) => q.id === domain.quadrant); + const count = interestCounts[domain.id] || 0; + const isExpanded = expandedDomain === domain.id; + const isInterested = interested.has(domain.id); + return ( +
+ + + {isExpanded && ( +
+
+ {domain.examples.map((ex) => ( + + {ex} + + ))} +
+ + +
+ )} +
+ ); + })} +
+
+
+ + )} + + {/* ═══ ZOOM 1x: Fund projects ═══ */} + {zoom === 1 && ( + <> + {/* Filters */} +
+
+
+ + {quadrants.map((q) => ( + + ))} +
+ + handleSearchChange(e.target.value)} + placeholder="Search projects..." + className="bg-gray-800 border border-gray-700 rounded-full pl-8 pr-3 py-1.5 text-xs text-gray-25 placeholder:text-gray-500 focus:outline-none focus:border-teal-500 w-48" + /> +
+
+
+
+ + {/* Project cards */} +
+
+
+ {filteredDomains.flatMap((domain) => { + const quadrant = quadrants.find((q) => q.id === domain.quadrant); + return domain.examples + .filter( + (ex) => + !searchQuery || + ex.toLowerCase().includes(searchQuery.toLowerCase()) || + domain.name.toLowerCase().includes(searchQuery.toLowerCase()) + ) + .map((example) => { + const projectId = `project:${example.toLowerCase().replace(/[^a-z0-9]+/g, "-")}`; + const isExpanded = expandedDomain === projectId; + return ( +
+ + + {isExpanded && ( +
+ + +
+ )} +
+ ); + }); + })} +
+
+
+ + )} + + {/* My Positions */} + + + {/* Activity Feed */} + { + setZoom(2); + const domain = domains.find((d) => d.id === domainId); + if (domain) { + setSelectedQuadrant(domain.quadrant); + handleDomainClick(domain.id); + } + }} + /> +
+ ); +} + +// ── Reusable stake tier buttons ─────────────────────────────────────────── +function StakeTierButtons({ + domainId, + stakingDomain, + isSending, + onStake, +}: { + domainId: string; + stakingDomain: string | null; + isSending: boolean; + onStake: (domainId: string, amount: string) => void; +}) { + return ( +
+

+ Stake ETH to signal interest — withdraw anytime +

+
+ {STAKE_TIERS.map((tier) => ( + + ))} +
+ {stakingDomain === domainId && ( +
+ + {isSending ? "Confirm in wallet..." : "Processing..."} +
+ )} +
+ ); +} + +// ── Domain % raised badge (reads from contract, reports to parent) ──────── +function DomainPercent({ + domainId, + onTotal, +}: { + domainId: string; + onTotal: (domainId: string, eth: number) => void; +}) { + const { data: totalWei } = useReadContract({ + address: STAKING_CONTRACT_ADDRESS, + abi: STAKING_CONTRACT_ABI, + functionName: "getDomainTotal", + args: [domainId], + }); + + const ethAmount = totalWei ? Number(formatEther(totalWei)) : 0; + const pct = Math.min(ethAmount * 100, 100); + + useEffect(() => { + onTotal(domainId, ethAmount); + }, [domainId, ethAmount, onTotal]); + + if (!totalWei || ethAmount === 0) return null; + + return ( + = 100 ? "text-green-400" : "text-teal-400"}`}> + {pct.toFixed(2)}% + + ); +} + +// ── Domain stake info (reads from contract) ────────────────────────────── +function DomainStakeInfo({ domainId }: { domainId: string }) { + const { data: totalWei } = useReadContract({ + address: STAKING_CONTRACT_ADDRESS, + abi: STAKING_CONTRACT_ABI, + functionName: "getDomainTotal", + args: [domainId], + }); + + const { data: isDeployed } = useReadContract({ + address: STAKING_CONTRACT_ADDRESS, + abi: STAKING_CONTRACT_ABI, + functionName: "isDomainDeployed", + args: [domainId], + }); + + const total = totalWei ? formatEther(totalWei) : "0"; + const threshold = 1; + const progress = totalWei ? Math.min(Number(formatEther(totalWei)) / threshold, 1) : 0; + + if (totalWei === undefined) return null; + + return ( +
+
+ Onchain pool + {total} ETH +
+
+
+
+
+ {(progress * 100).toFixed(0)}% to deployment + Goal: {threshold} ETH +
+ {isDeployed && ( +

Funds deployed by operator

+ )} +
+ ); +} + +// ── All stakeable IDs across zoom levels ───────────────────────────────── +const ALL_STAKEABLE_IDS: { id: string; name: string }[] = [ + ...diagnosticChecklist.map((item) => ({ id: `flavor:${item.id}`, name: `${item.label} d/acc` })), + ...quadrants.map((q) => ({ id: `quadrant:${q.id}`, name: q.label })), + ...domains.map((d) => ({ id: d.id, name: d.name })), + ...domains.flatMap((d) => + d.examples.map((ex) => ({ + id: `project:${ex.toLowerCase().replace(/[^a-z0-9]+/g, "-")}`, + name: ex, + })) + ), +]; + +// ── My Staking Positions ───────────────────────────────────────────────── +function StakingPositions({ pendingTx }: { pendingTx: { domainId: string; hash: string } | null }) { + const { address, isConnected } = useAccount(); + const { connect: connectWallet, connectors: availableConnectors } = useConnect(); + const { disconnect } = useDisconnect(); + + const explorerUrl = TARGET_CHAIN.blockExplorers?.default?.url || "https://etherscan.io"; + + if (!isConnected) { + return ( +
+
+
+ +

Your Positions

+

Connect your wallet to view your staking positions

+ +
+
+ ); + } + + return ( +
+
+
+
+
+

+ + Your Positions + + {address?.slice(0, 6)}...{address?.slice(-4)} + +

+ +
+
+ {pendingTx && ( +
+
+ +

Transaction Pending

+
+

+ Staking on {pendingTx.domainId} +

+ + View on block explorer + +
+ )} + {ALL_STAKEABLE_IDS.map((item) => ( + + ))} +
+
+
+ ); +} + +function PositionCard({ + domainId, + domainName, + address, +}: { + domainId: string; + domainName: string; + address: `0x${string}`; +}) { + const { data: stakeWei, refetch: refetchStake } = useReadContract({ + address: STAKING_CONTRACT_ADDRESS, + abi: STAKING_CONTRACT_ABI, + functionName: "getStake", + args: [domainId, address], + }); + + const { data: isDeployed } = useReadContract({ + address: STAKING_CONTRACT_ADDRESS, + abi: STAKING_CONTRACT_ABI, + functionName: "isDomainDeployed", + args: [domainId], + query: { enabled: !!stakeWei && stakeWei > BigInt(0) }, + }); + + const { data: domainTotalWei } = useReadContract({ + address: STAKING_CONTRACT_ADDRESS, + abi: STAKING_CONTRACT_ABI, + functionName: "getDomainTotal", + args: [domainId], + query: { enabled: !!stakeWei && stakeWei > BigInt(0) }, + }); + + const { writeContractAsync: withdrawAsync, isPending } = useWriteContract(); + const [withdrawing, setWithdrawing] = useState(false); + const [withdrawTxHash, setWithdrawTxHash] = useState(null); + + const explorerUrl = TARGET_CHAIN.blockExplorers?.default?.url || "https://etherscan.io"; + + const stakeAmount = stakeWei ? Number(formatEther(stakeWei)) : 0; + if (stakeAmount === 0 && !withdrawing) return null; + + const domainTotal = domainTotalWei ? Number(formatEther(domainTotalWei)) : 0; + const othersStaked = Math.max(0, domainTotal - stakeAmount); + const status = isDeployed ? "Done" : "Not Started"; + const statusColor = isDeployed ? "text-green-400" : "text-gray-500"; + const statusDot = isDeployed ? "bg-green-400" : "bg-gray-500"; + + const handleWithdraw = async () => { + if (!stakeWei || isDeployed) return; + setWithdrawing(true); + try { + const hash = await withdrawAsync({ + address: STAKING_CONTRACT_ADDRESS, + abi: STAKING_CONTRACT_ABI, + functionName: "withdraw", + args: [domainId, stakeWei], + }); + if (hash) { + setWithdrawTxHash(hash); + // Wait for the tx to be included, then refetch + setTimeout(() => { + refetchStake(); + setWithdrawing(false); + setWithdrawTxHash(null); + }, 15000); + } + } catch { + setWithdrawing(false); + } + }; + + return ( +
+
+

{domainName}

+ + {stakeWei ? formatEther(stakeWei) : "0"} ETH + +
+
+
+ + {status} +
+ Yield: 0% + Pool: {domainTotal.toFixed(3)} ETH + {othersStaked > 0 && ( + Others staked: {othersStaked.toFixed(3)} ETH + )} +
+ {isDeployed ? ( +

Funds deployed — thank you for your support

+ ) : withdrawing ? ( +
+
+ + {isPending ? "Confirm in wallet..." : "Withdrawing..."} +
+ {withdrawTxHash && ( + + View on block explorer + + )} +
+ ) : ( + + )} +
+ ); +} + +// ── ENS-aware address display ───────────────────────────────────────────── +function ActivityAddress({ address: addr }: { address: string }) { + const explorerUrl = TARGET_CHAIN.blockExplorers?.default?.url || "https://etherscan.io"; + const isEthAddress = addr.startsWith("0x") && addr.length === 42; + const { data: ensName } = useEnsName({ + address: isEthAddress ? (addr as `0x${string}`) : undefined, + chainId: 1, // ENS lives on mainnet + }); + const display = ensName || `${addr.slice(0, 6)}...${addr.slice(-4)}`; + + if (!isEthAddress) { + return {display}; + } + + return ( + + {display} + + ); +} + +// ── Activity Entry Row ──────────────────────────────────────────────────── +function ActivityEntry({ + entry, + onDomainClick, +}: { + entry: { type: string; domainId: string; address: string; amount?: string; timestamp: number }; + onDomainClick: (domainId: string) => void; +}) { + const domainName = domains.find((d) => d.id === entry.domainId)?.name || entry.domainId; + const diff = Date.now() - entry.timestamp; + const timeAgo = + diff < 60_000 ? "just now" + : diff < 3600_000 ? `${Math.floor(diff / 60_000)}m ago` + : diff < 86400_000 ? `${Math.floor(diff / 3600_000)}h ago` + : `${Math.floor(diff / 86400_000)}d ago`; + + return ( +
+ + + + {entry.type === "stake" && ( + <> + staked {entry.amount} ETH on + + )} + {entry.type === "withdraw" && ( + <> + withdrew {entry.amount} ETH from + + )} + {entry.type === "interest" && "signaled interest in"} + + + {timeAgo} +
+ ); +} + +// ── Activity Feed ───────────────────────────────────────────────────────── +type ActivityItem = { type: string; domainId: string; address: string; amount?: string; timestamp: number }; + +function ActivityFeed({ + activity, + activityTotal, + trending, + connectedAddress, + onDomainClick, +}: { + activity: ActivityItem[]; + activityTotal: number; + trending: { domainId: string; queryCount: number }[]; + connectedAddress?: string; + onDomainClick: (domainId: string) => void; +}) { + const [allActivity, setAllActivity] = useState(activity); + const [loading, setLoading] = useState(false); + + // Sync when parent refreshes initial data + useEffect(() => { + setAllActivity(activity); + }, [activity]); + + const hasMore = allActivity.length < activityTotal; + + const loadMore = () => { + setLoading(true); + fetch(`/api/coalitions?limit=25&offset=${allActivity.length}`) + .then((r) => r.json()) + .then((data) => { + setAllActivity((prev) => [...prev, ...(data.activity || [])]); + }) + .catch(() => {}) + .finally(() => setLoading(false)); + }; + + const myActivity = connectedAddress + ? allActivity.filter((e) => e.address.toLowerCase() === connectedAddress.toLowerCase()) + : []; + + return ( +
+
+
+ {/* Your Activity — only when wallet connected and has activity */} + {myActivity.length > 0 && ( +
+

+ + Your Activity +

+
+ {myActivity.map((entry, i) => ( + + ))} +
+
+ )} + + {/* All Activity — always shown */} +

+ + All Activity +

+ + {trending.length > 0 && ( +
+
+ +

Trending

+
+
+ {trending.map((t) => { + const domain = domains.find((d) => d.id === t.domainId); + if (!domain) return null; + return ( + + ); + })} +
+
+ )} + + {allActivity.length > 0 ? ( +
+ {allActivity.map((entry, i) => ( + + ))} + {hasMore && ( + + )} +
+ ) : ( +

No activity yet. Be the first to stake or signal interest.

+ )} +
+
+ ); +} diff --git a/src/app/experiments/dacc-coalition-builder/DomainMap.tsx b/src/app/experiments/dacc-coalition-builder/DomainMap.tsx new file mode 100644 index 00000000..3df185e5 --- /dev/null +++ b/src/app/experiments/dacc-coalition-builder/DomainMap.tsx @@ -0,0 +1,177 @@ +"use client"; + +import { type Domain, quadrants } from "@/lib/coalitions-data"; + +interface DomainMapProps { + domains: Domain[]; + interestCounts: Record; + selectedQuadrant: Domain["quadrant"] | null; + onSelectQuadrant: (q: Domain["quadrant"] | null) => void; +} + +export function DomainMap({ + domains, + interestCounts, + selectedQuadrant, + onSelectQuadrant, +}: DomainMapProps) { + // Get max interest for relative sizing + const maxInterest = Math.max( + ...Object.values(interestCounts), + 1 + ); + + return ( +
+ {/* Axis labels */} +
+ + Survive + + + Atoms vs. Bits × Survive vs. Thrive + + + Thrive + +
+ +
+ {/* Y-axis label */} +
+ + Bits + + + Atoms + +
+ + {/* 2x2 Grid */} +
+ {/* Top-left: Bits + Survive (Digital Defense) */} + q.id === "bits-survive")!} + domains={domains.filter((d) => d.quadrant === "bits-survive")} + interestCounts={interestCounts} + maxInterest={maxInterest} + isSelected={selectedQuadrant === "bits-survive"} + onSelect={() => + onSelectQuadrant( + selectedQuadrant === "bits-survive" ? null : "bits-survive" + ) + } + /> + {/* Top-right: Bits + Thrive (Digital Coordination) */} + q.id === "bits-thrive")!} + domains={domains.filter((d) => d.quadrant === "bits-thrive")} + interestCounts={interestCounts} + maxInterest={maxInterest} + isSelected={selectedQuadrant === "bits-thrive"} + onSelect={() => + onSelectQuadrant( + selectedQuadrant === "bits-thrive" ? null : "bits-thrive" + ) + } + /> + {/* Bottom-left: Atoms + Survive (Physical Defense) */} + q.id === "atoms-survive")!} + domains={domains.filter((d) => d.quadrant === "atoms-survive")} + interestCounts={interestCounts} + maxInterest={maxInterest} + isSelected={selectedQuadrant === "atoms-survive"} + onSelect={() => + onSelectQuadrant( + selectedQuadrant === "atoms-survive" ? null : "atoms-survive" + ) + } + /> + {/* Bottom-right: Atoms + Thrive (Physical Coordination) */} + q.id === "atoms-thrive")!} + domains={domains.filter((d) => d.quadrant === "atoms-thrive")} + interestCounts={interestCounts} + maxInterest={maxInterest} + isSelected={selectedQuadrant === "atoms-thrive"} + onSelect={() => + onSelectQuadrant( + selectedQuadrant === "atoms-thrive" ? null : "atoms-thrive" + ) + } + /> +
+
+
+ ); +} + +function QuadrantCell({ + quadrant, + domains, + interestCounts, + maxInterest, + isSelected, + onSelect, +}: { + quadrant: (typeof quadrants)[number]; + domains: Domain[]; + interestCounts: Record; + maxInterest: number; + isSelected: boolean; + onSelect: () => void; +}) { + const totalInterest = domains.reduce( + (sum, d) => sum + (interestCounts[d.id] || 0), + 0 + ); + + return ( + + ); +} diff --git a/src/app/experiments/dacc-coalition-builder/WalletProvider.tsx b/src/app/experiments/dacc-coalition-builder/WalletProvider.tsx new file mode 100644 index 00000000..f0662465 --- /dev/null +++ b/src/app/experiments/dacc-coalition-builder/WalletProvider.tsx @@ -0,0 +1,34 @@ +"use client"; + +import { ReactNode, useState } from "react"; +import { WagmiProvider, createConfig, http } from "wagmi"; +import { mainnet, sepolia } from "wagmi/chains"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { injected } from "wagmi/connectors"; +import { IS_STAGING } from "@/lib/staking-contract"; + +const config = IS_STAGING + ? createConfig({ + chains: [sepolia], + connectors: [injected()], + transports: { [sepolia.id]: http() }, + ssr: true, + }) + : createConfig({ + chains: [mainnet], + connectors: [injected()], + transports: { [mainnet.id]: http() }, + ssr: true, + }); + +export function WalletProvider({ children }: { children: ReactNode }) { + const [queryClient] = useState(() => new QueryClient()); + + return ( + + + {children} + + + ); +} diff --git a/src/app/experiments/dacc-coalition-builder/analytics/AdminClient.tsx b/src/app/experiments/dacc-coalition-builder/analytics/AdminClient.tsx new file mode 100644 index 00000000..c83b8ac2 --- /dev/null +++ b/src/app/experiments/dacc-coalition-builder/analytics/AdminClient.tsx @@ -0,0 +1,753 @@ +"use client"; + +import { useState, useEffect, useMemo } from "react"; +import { Loader2 } from "lucide-react"; +import { domains, quadrants, type Domain } from "@/lib/coalitions-data"; + +const Q_RGB: Record = { + "atoms-survive": "251,146,60", + "atoms-thrive": "74,222,128", + "bits-survive": "96,165,250", + "bits-thrive": "192,132,252", +}; + +const Q_CSS: Record = { + "atoms-survive": "text-orange-400", + "atoms-thrive": "text-green-400", + "bits-survive": "text-blue-400", + "bits-thrive": "text-purple-400", +}; + +type Tab = "heatmap" | "bubbles" | "treemap" | "timeline" | "radar" | "contour"; + +interface ApiData { + interests: Record; + stakes: Record; + trending: { domainId: string; queryCount: number }[]; + activity: { type: string; domainId: string; address: string; amount?: string; timestamp: number }[]; + activityTotal: number; + totalQueries: number; + recentQueryCount: number; +} + +// ── Main Component ────────────────────────────────────────────────────────── + +export function AdminClient() { + const [tab, setTab] = useState("heatmap"); + const [data, setData] = useState(null); + + useEffect(() => { + fetch("/api/coalitions?limit=1000") + .then((r) => r.json()) + .then(setData) + .catch(() => {}); + }, []); + + const energy = useMemo(() => { + if (!data) return {}; + const result: Record = {}; + for (const d of domains) { + const interest = data.interests[d.id] || 0; + const s = data.stakes[d.id]; + result[d.id] = interest + (s ? s.totalEth * 10 + s.stakers * 2 : 0); + } + return result; + }, [data]); + + if (!data) { + return ( +
+ +
+ ); + } + + const totalInterest = Object.values(data.interests).reduce((s, n) => s + n, 0); + const totalStakers = Object.values(data.stakes).reduce((s, v) => s + v.stakers, 0); + const totalEth = Object.values(data.stakes).reduce((s, v) => s + v.totalEth, 0); + + const tabs: { id: Tab; label: string }[] = [ + { id: "heatmap", label: "Heat Map" }, + { id: "bubbles", label: "Bubbles" }, + { id: "treemap", label: "Treemap" }, + { id: "radar", label: "Radar" }, + { id: "timeline", label: "Timeline" }, + { id: "contour", label: "Contour" }, + ]; + + return ( +
+ {/* Summary stats */} +
+ + + + +
+ + {/* Tab bar */} +
+ {tabs.map((t) => ( + + ))} +
+ + {/* Visualization */} +
+ {tab === "heatmap" && } + {tab === "bubbles" && } + {tab === "treemap" && } + {tab === "radar" && } + {tab === "timeline" && } + {tab === "contour" && } +
+
+ ); +} + +function StatCard({ label, value }: { label: string; value: string | number }) { + return ( +
+

{label}

+

{value}

+
+ ); +} + +// ── Shared helpers ────────────────────────────────────────────────────────── + +function maxOf(energy: Record) { + return Math.max(...Object.values(energy), 1); +} + +function domainPos(d: Domain): { x: number; y: number } { + const qDomains = domains.filter((dd) => dd.quadrant === d.quadrant); + const idx = qDomains.indexOf(d); + const cols = Math.ceil(Math.sqrt(qDomains.length)); + const row = Math.floor(idx / cols); + const col = idx % cols; + const rows = Math.ceil(qDomains.length / cols); + + const qx = d.quadrant.includes("thrive") ? 0.75 : 0.25; + const qy = d.quadrant.includes("bits") ? 0.75 : 0.25; + const spread = 0.15; + + return { + x: qx + ((cols > 1 ? col / (cols - 1) : 0.5) - 0.5) * spread * 2, + y: qy + ((rows > 1 ? row / (rows - 1) : 0.5) - 0.5) * spread * 2, + }; +} + +// ── 1. HEAT MAP ───────────────────────────────────────────────────────────── + +function HeatMap({ data, energy }: { data: ApiData; energy: Record }) { + const max = maxOf(energy); + const qOrder: Domain["quadrant"][] = ["bits-survive", "bits-thrive", "atoms-survive", "atoms-thrive"]; + + return ( +
+

+ Cell intensity = combined interest + stake energy. Brighter cells are stronger attractors. +

+
+ {qOrder.map((qId) => { + const q = quadrants.find((q) => q.id === qId)!; + const qDomains = domains + .filter((d) => d.quadrant === qId) + .sort((a, b) => (energy[b.id] || 0) - (energy[a.id] || 0)); + const rgb = Q_RGB[qId]; + return ( +
+
+

{q.label}

+ {q.axis.y} x {q.axis.x} +
+
+ {qDomains.map((d) => { + const e = energy[d.id] || 0; + const t = e / max; + return ( +
0.3 + ? `inset 0 0 ${t * 30}px rgba(${rgb}, ${t * 0.15}), 0 0 ${t * 12}px rgba(${rgb}, ${t * 0.2})` + : undefined, + }} + > +

{d.name}

+

+ {e > 0 ? e.toFixed(0) : "\u2013"} +

+ {/* Hover tooltip */} +
+
+

{d.name}

+

+ Interest: {data.interests[d.id] || 0} + {data.stakes[d.id] && ` \u00B7 Stake: ${data.stakes[d.id].totalEth.toFixed(3)} ETH`} +

+
+
+
+ ); + })} +
+
+ ); + })} +
+
+ ← Survive + Thrive → +
+
+ ); +} + +// ── 2. BUBBLE CHART ───────────────────────────────────────────────────────── + +function BubbleChart({ energy }: { energy: Record }) { + const max = maxOf(energy); + const W = 800, H = 600; + + const qBounds: Record = { + "bits-survive": { x: 0, y: 0, w: W / 2, h: H / 2 }, + "bits-thrive": { x: W / 2, y: 0, w: W / 2, h: H / 2 }, + "atoms-survive": { x: 0, y: H / 2, w: W / 2, h: H / 2 }, + "atoms-thrive": { x: W / 2, y: H / 2, w: W / 2, h: H / 2 }, + }; + + const bubbles = domains.map((d) => { + const e = energy[d.id] || 0; + const r = Math.max(10, Math.sqrt(e / max) * 50); + const b = qBounds[d.quadrant]; + const qDomains = domains.filter((dd) => dd.quadrant === d.quadrant); + const idx = qDomains.indexOf(d); + const cols = Math.ceil(Math.sqrt(qDomains.length)); + const rows = Math.ceil(qDomains.length / cols); + const col = idx % cols; + const row = Math.floor(idx / cols); + const padX = 60, padY = 50; + const cx = b.x + padX + (cols > 1 ? col / (cols - 1) : 0.5) * (b.w - padX * 2); + const cy = b.y + padY + (rows > 1 ? row / (rows - 1) : 0.5) * (b.h - padY * 2); + return { d, cx, cy, r, e }; + }); + + return ( +
+

+ Circle area = energy. Position maps to the d/acc quadrant. +

+ + {/* Quadrant backgrounds */} + {Object.entries(qBounds).map(([qId, b]) => ( + + ))} + {/* Cross lines */} + + + {/* Quadrant labels */} + {[ + { label: "Digital Defense", x: W / 4, y: 16 }, + { label: "Digital Coordination", x: 3 * W / 4, y: 16 }, + { label: "Physical Defense", x: W / 4, y: H / 2 + 16 }, + { label: "Physical Coordination", x: 3 * W / 4, y: H / 2 + 16 }, + ].map((l) => ( + {l.label} + ))} + {/* Axis labels */} + BITS + ATOMS + SURVIVE + THRIVE + {/* Bubbles - render smaller ones first so large ones are on top */} + {bubbles + .sort((a, b) => a.r - b.r) + .map(({ d, cx, cy, r, e }) => { + const rgb = Q_RGB[d.quadrant]; + const t = e / max; + return ( + + {/* Glow */} + {t > 0.2 && ( + + )} + + {r > 18 && ( + + {d.name.length > r / 4 ? d.name.split(" ")[0] : d.name} + + )} + {r > 22 && ( + + {e.toFixed(0)} + + )} + {d.name}: {e.toFixed(0)} energy + + ); + })} + +
+ ); +} + +// ── 3. TREEMAP ────────────────────────────────────────────────────────────── + +interface TRect { x: number; y: number; w: number; h: number; domain: Domain; value: number } + +function layoutTreemap( + items: { domain: Domain; value: number }[], + x: number, y: number, w: number, h: number, +): TRect[] { + if (items.length === 0) return []; + if (items.length === 1) return [{ x, y, w, h, domain: items[0].domain, value: items[0].value }]; + + const total = items.reduce((s, i) => s + i.value, 0); + if (total === 0) { + return items.map((item, i) => ({ + x: x + (w / items.length) * i, y, w: w / items.length, h, domain: item.domain, value: 0, + })); + } + + const sorted = [...items].sort((a, b) => b.value - a.value); + let sum = 0, split = 1; + for (let i = 0; i < sorted.length; i++) { + sum += sorted[i].value; + if (sum >= total / 2) { split = i + 1; break; } + } + if (split >= sorted.length) split = sorted.length - 1; + + const left = sorted.slice(0, split); + const right = sorted.slice(split); + const ratio = left.reduce((s, i) => s + i.value, 0) / total; + + if (w >= h) { + return [ + ...layoutTreemap(left, x, y, w * ratio, h), + ...layoutTreemap(right, x + w * ratio, y, w * (1 - ratio), h), + ]; + } + return [ + ...layoutTreemap(left, x, y, w, h * ratio), + ...layoutTreemap(right, x, y + h * ratio, w, h * (1 - ratio)), + ]; +} + +function Treemap({ energy }: { energy: Record }) { + const max = maxOf(energy); + const W = 800, H = 500; + const items = domains.map((d) => ({ domain: d, value: Math.max(energy[d.id] || 0, 0.5) })); + const rects = layoutTreemap(items, 0, 0, W, H); + + return ( +
+

+ Rectangle area = energy. The largest blocks are the strongest attractors. +

+ + {rects.map(({ x, y, w, h, domain, value }) => { + const rgb = Q_RGB[domain.quadrant]; + const t = value / max; + const pad = 1.5; + const rw = Math.max(w - pad * 2, 0); + const rh = Math.max(h - pad * 2, 0); + return ( + + + {rw > 70 && rh > 35 && ( + <> + + {domain.name.length > rw / 7.5 + ? domain.name.slice(0, Math.floor(rw / 7.5)) + "\u2026" + : domain.name} + + + {value.toFixed(0)} + + + )} + {rw > 30 && rw <= 70 && rh > 20 && ( + + {value.toFixed(0)} + + )} + {domain.name}: {value.toFixed(0)} energy + + ); + })} + + {/* Legend */} +
+ {quadrants.map((q) => ( + + + {q.label} + + ))} +
+
+ ); +} + +// ── 4. RADAR CHART ────────────────────────────────────────────────────────── + +function RadarChart({ energy }: { energy: Record }) { + const max = maxOf(energy); + const cx = 300, cy = 300, R = 220; + + // Sort domains so quadrants are grouped as visual sectors + const qOrder: Domain["quadrant"][] = ["bits-survive", "bits-thrive", "atoms-thrive", "atoms-survive"]; + const sorted = [...domains].sort((a, b) => { + const ai = qOrder.indexOf(a.quadrant); + const bi = qOrder.indexOf(b.quadrant); + return ai !== bi ? ai - bi : a.name.localeCompare(b.name); + }); + + const n = sorted.length; + const points = sorted.map((d, i) => { + const angle = (i / n) * Math.PI * 2 - Math.PI / 2; + const e = energy[d.id] || 0; + const r = Math.max((e / max) * R, 4); + return { + d, + angle, + x: cx + r * Math.cos(angle), + y: cy + r * Math.sin(angle), + lx: cx + (R + 30) * Math.cos(angle), + ly: cy + (R + 30) * Math.sin(angle), + sx: cx + R * Math.cos(angle), + sy: cy + R * Math.sin(angle), + e, + }; + }); + + const polyPath = points.map((p, i) => `${i === 0 ? "M" : "L"} ${p.x} ${p.y}`).join(" ") + " Z"; + + // Quadrant sector arcs (background fills) + const qStarts: number[] = []; + let prevQ = ""; + sorted.forEach((d, i) => { + if (d.quadrant !== prevQ) { qStarts.push(i); prevQ = d.quadrant; } + }); + + return ( +
+

+ Each spoke = a domain. Distance from center = energy. Asymmetric shapes reveal where energy concentrates. +

+ + {/* Quadrant sector fills */} + {qStarts.map((startIdx, si) => { + const endIdx = si < qStarts.length - 1 ? qStarts[si + 1] : n; + const qId = sorted[startIdx].quadrant; + const rgb = Q_RGB[qId]; + const startAngle = (startIdx / n) * Math.PI * 2 - Math.PI / 2; + const endAngle = (endIdx / n) * Math.PI * 2 - Math.PI / 2; + const largeArc = endAngle - startAngle > Math.PI ? 1 : 0; + const path = [ + `M ${cx} ${cy}`, + `L ${cx + R * Math.cos(startAngle)} ${cy + R * Math.sin(startAngle)}`, + `A ${R} ${R} 0 ${largeArc} 1 ${cx + R * Math.cos(endAngle)} ${cy + R * Math.sin(endAngle)}`, + "Z", + ].join(" "); + return ; + })} + + {/* Concentric rings */} + {[0.25, 0.5, 0.75, 1].map((pct) => ( + + ))} + + {/* Spokes */} + {points.map((p) => ( + + ))} + + {/* Filled polygon */} + + + {/* Data points + labels */} + {points.map((p) => { + const rgb = Q_RGB[p.d.quadrant]; + const t = p.e / max; + return ( + + {/* Glow for strong attractors */} + {t > 0.4 && ( + + )} + + cx + 10 ? "start" : p.lx < cx - 10 ? "end" : "middle"} + dominantBaseline={p.ly > cy + 10 ? "hanging" : p.ly < cy - 10 ? "auto" : "middle"} + fill="rgba(255,255,255,0.35)" fontSize="8" + > + {p.d.name.split(" ")[0]} + + {p.d.name}: {p.e.toFixed(0)} + + ); + })} + + {/* Legend */} +
+ {quadrants.map((q) => ( + + + {q.label} + + ))} +
+
+ ); +} + +// ── 5. TIMELINE ───────────────────────────────────────────────────────────── + +function Timeline({ data }: { data: ApiData }) { + const bins = useMemo(() => { + if (data.activity.length === 0) return []; + const sorted = [...data.activity].sort((a, b) => a.timestamp - b.timestamp); + const dayMs = 86400000; + const first = Math.floor(sorted[0].timestamp / dayMs) * dayMs; + const last = Math.floor(sorted[sorted.length - 1].timestamp / dayMs) * dayMs; + + const result: { day: number; interest: number; stake: number; withdraw: number }[] = []; + for (let day = first; day <= last; day += dayMs) { + const end = day + dayMs; + const events = sorted.filter((e) => e.timestamp >= day && e.timestamp < end); + result.push({ + day, + interest: events.filter((e) => e.type === "interest").length, + stake: events.filter((e) => e.type === "stake").length, + withdraw: events.filter((e) => e.type === "withdraw").length, + }); + } + return result; + }, [data.activity]); + + if (bins.length === 0) { + return

No activity data yet.

; + } + + const W = 800, H = 300; + const maxEvents = Math.max(...bins.map((b) => b.interest + b.stake + b.withdraw), 1); + const barW = Math.max(3, Math.min(20, (W - 50) / bins.length - 1)); + const chartH = H - 40; + + return ( +
+

+ Daily activity volume. Surges indicate emerging attractors. +

+ + {/* Y grid */} + {[0.25, 0.5, 0.75, 1].map((pct) => ( + + + + {Math.round(maxEvents * pct)} + + + ))} + {/* Stacked bars */} + {bins.map((bin, i) => { + const x = 50 + i * ((W - 50) / bins.length); + const segments = [ + { count: bin.withdraw, color: "rgba(251,146,60,0.7)" }, + { count: bin.stake, color: "rgba(2,226,172,0.7)" }, + { count: bin.interest, color: "rgba(96,165,250,0.6)" }, + ]; + let y = H - 20; + return ( + + {segments.map((seg, si) => { + if (seg.count === 0) return null; + const segH = (seg.count / maxEvents) * chartH; + y -= segH; + return ( + 4 ? 1.5 : 0} /> + ); + })} + {/* Date tick */} + {(bins.length <= 14 || i % Math.ceil(bins.length / 12) === 0) && ( + + {new Date(bin.day).toLocaleDateString("en", { month: "short", day: "numeric" })} + + )} + + {new Date(bin.day).toLocaleDateString()}: {bin.interest + bin.stake + bin.withdraw} events + + + ); + })} + + +
+ + Interest + + + Stake + + + Withdraw + +
+
+ ); +} + +// ── 6. CONTOUR MAP ────────────────────────────────────────────────────────── + +function ContourMap({ energy }: { energy: Record }) { + const max = maxOf(energy); + const COLS = 40, ROWS = 30; + const W = 800, H = 600; + const cellW = W / COLS, cellH = H / ROWS; + + // Each domain positioned in continuous 0..1 space + const positions = useMemo( + () => domains.map((d) => ({ ...domainPos(d), energy: energy[d.id] || 0, domain: d })), + [energy], + ); + + // Compute density field + const grid = useMemo(() => { + const sigma = 0.12; + const cells: { x: number; y: number; density: number }[] = []; + let maxDensity = 0; + + for (let r = 0; r < ROWS; r++) { + for (let c = 0; c < COLS; c++) { + const gx = (c + 0.5) / COLS; + const gy = 1 - (r + 0.5) / ROWS; // flip Y so bits is top + let density = 0; + for (const p of positions) { + const dx = gx - p.x; + const dy = gy - p.y; + density += p.energy * Math.exp(-(dx * dx + dy * dy) / (2 * sigma * sigma)); + } + if (density > maxDensity) maxDensity = density; + cells.push({ x: c * cellW, y: r * cellH, density }); + } + } + + // Normalize + if (maxDensity > 0) { + for (const cell of cells) cell.density /= maxDensity; + } + return cells; + }, [positions, cellW, cellH]); + + // Color scale: dark -> teal -> white-hot + function densityColor(t: number): string { + if (t < 0.3) { + const s = t / 0.3; + return `rgba(2,226,172, ${s * 0.15})`; + } + if (t < 0.7) { + const s = (t - 0.3) / 0.4; + const r = Math.round(2 + s * 50); + const g = Math.round(226 - s * 80); + const b = Math.round(172 - s * 40); + return `rgba(${r},${g},${b}, ${0.15 + s * 0.25})`; + } + const s = (t - 0.7) / 0.3; + const r = Math.round(52 + s * 200); + const g = Math.round(146 + s * 109); + const b = Math.round(132 + s * 123); + return `rgba(${r},${g},${b}, ${0.4 + s * 0.4})`; + } + + return ( +
+

+ Topographic density field. Peaks show where energy concentrates across the Atoms/Bits x Survive/Thrive space. +

+ + {/* Density cells */} + {grid.map((cell, i) => ( + cell.density > 0.02 && ( + + ) + ))} + + {/* Quadrant dividers */} + + + + {/* Domain markers */} + {positions.map((p) => { + const px = p.x * W; + const py = (1 - p.y) * H; + const rgb = Q_RGB[p.domain.quadrant]; + const t = p.energy / max; + return ( + + {/* Glow ring */} + {t > 0.2 && ( + + )} + + + {p.domain.name.split(" ")[0]} + + {p.domain.name}: {p.energy.toFixed(0)} energy + + ); + })} + + {/* Axis labels */} + BITS + ATOMS + SURVIVE + THRIVE + +
+ ); +} diff --git a/src/app/experiments/dacc-coalition-builder/analytics/page.tsx b/src/app/experiments/dacc-coalition-builder/analytics/page.tsx new file mode 100644 index 00000000..4c01d968 --- /dev/null +++ b/src/app/experiments/dacc-coalition-builder/analytics/page.tsx @@ -0,0 +1,20 @@ +import { Metadata } from "next"; +import { ListPageLayout, ListPageHeader } from "@/components/layouts"; +import { AdminClient } from "./AdminClient"; + +export const metadata: Metadata = { + title: "d/ACC Coalition Builder - Admin Analytics", + description: "Attractor visualizations for the d/ACC coalition builder.", +}; + +export default function AdminPage() { + return ( + + + + + ); +} diff --git a/src/app/experiments/dacc-coalition-builder/layout.tsx b/src/app/experiments/dacc-coalition-builder/layout.tsx new file mode 100644 index 00000000..779626c0 --- /dev/null +++ b/src/app/experiments/dacc-coalition-builder/layout.tsx @@ -0,0 +1,9 @@ +import { WalletProvider } from "./WalletProvider"; + +export default function CoalitionsLayout({ + children, +}: { + children: React.ReactNode; +}) { + return {children}; +} diff --git a/src/app/experiments/dacc-coalition-builder/page.tsx b/src/app/experiments/dacc-coalition-builder/page.tsx new file mode 100644 index 00000000..d2bc1acd --- /dev/null +++ b/src/app/experiments/dacc-coalition-builder/page.tsx @@ -0,0 +1,24 @@ +import { Suspense } from "react"; +import { Metadata } from "next"; +import { ListPageLayout, ListPageHeader } from "@/components/layouts"; +import { CoalitionsClient } from "./CoalitionsClient"; + +export const metadata: Metadata = { + title: "d/acc Coalition Builder", + description: + "Map the coordination space. Discover domains, signal interest, and find your coalition through the d/acc lens.", +}; + +export default function CoalitionsPage() { + return ( + + + + + + + ); +} diff --git a/src/app/experiments/page.tsx b/src/app/experiments/page.tsx new file mode 100644 index 00000000..3d7a3b71 --- /dev/null +++ b/src/app/experiments/page.tsx @@ -0,0 +1,69 @@ +import { Metadata } from "next"; +import Link from "next/link"; +import { ArrowRight, Map } from "lucide-react"; +import { + ListPageLayout, + ListPageHeader, +} from "@/components/layouts"; + +export const metadata: Metadata = { + title: "Experiments", + description: + "Experimental tools for coordination, funding, and public goods discovery.", +}; + +const experiments = [ + { + slug: "dacc-coalition-builder", + title: "d/acc Coalition Builder", + description: + "Map the problem space, find your coalition, and deploy capital through the right mechanism. A reflexive feedback loop between discovering, coalescing, funding, and learning.", + status: "Alpha" as const, + icon: Map, + }, +]; + +export default function ExperimentsPage() { + return ( + + + +
+
+
+ {experiments.map((exp) => ( + +
+
+ +
+
+
+

+ {exp.title} +

+ + {exp.status} + +
+

{exp.description}

+
+
+
+ Explore +
+ + ))} +
+
+
+
+ ); +} diff --git a/src/app/mechanisms/[slug]/opengraph-image.tsx b/src/app/mechanisms/[slug]/opengraph-image.tsx index 8fb0f40b..a5c9c7ac 100644 --- a/src/app/mechanisms/[slug]/opengraph-image.tsx +++ b/src/app/mechanisms/[slug]/opengraph-image.tsx @@ -1,14 +1,10 @@ import { ImageResponse } from "next/og"; -import { getMechanismBySlug, mechanisms } from "@/content/mechanisms"; +import { getMechanismBySlug } from "@/content/mechanisms"; import { generateOgImage, OG_SIZE } from "@/lib/og-image"; export const size = OG_SIZE; export const contentType = "image/png"; -export function generateStaticParams() { - return mechanisms.map((m) => ({ slug: m.slug })); -} - export default async function OGImage({ params, }: { diff --git a/src/app/research/[slug]/opengraph-image.tsx b/src/app/research/[slug]/opengraph-image.tsx index 49a14eb6..3efeaaa2 100644 --- a/src/app/research/[slug]/opengraph-image.tsx +++ b/src/app/research/[slug]/opengraph-image.tsx @@ -1,14 +1,10 @@ import { ImageResponse } from "next/og"; -import { getResearchBySlug, research } from "@/content/research"; +import { getResearchBySlug } from "@/content/research"; import { generateOgImage, OG_SIZE } from "@/lib/og-image"; export const size = OG_SIZE; export const contentType = "image/png"; -export function generateStaticParams() { - return research.map((r) => ({ slug: r.slug })); -} - export default async function OGImage({ params, }: { diff --git a/src/components/layout/Header.tsx b/src/components/layout/Header.tsx index 29536025..9293db0d 100644 --- a/src/components/layout/Header.tsx +++ b/src/components/layout/Header.tsx @@ -22,11 +22,13 @@ const aboutItems: DropdownItem[] = [ }, ]; -const navLinks = [ +const discoverItems: DropdownItem[] = [ + { label: "Campaigns", href: "/campaigns" }, { label: "Research", href: "/research" }, { label: "Apps", href: "/apps" }, { label: "Mechanisms", href: "/mechanisms" }, { label: "Case Studies", href: "/case-studies" }, + { label: "Experiments", href: "/experiments" }, ]; const navLinkClass = @@ -128,11 +130,7 @@ export default function Header() {
@@ -174,16 +172,19 @@ export default function Header() { ), )}
- {[{ label: "Campaigns", href: "/campaigns" }, ...navLinks].map(({ label, href }) => ( - setMobileMenuOpen(false)} - > - {label} - - ))} +
+

Discover

+ {discoverItems.map((item) => ( + setMobileMenuOpen(false)} + > + {item.label} + + ))} +