Implement collections and more generic protocol framework#113
Conversation
There was a problem hiding this comment.
Pull Request Overview
This PR implements a comprehensive protocol framework for handling collections and other generic protocols in the Ethscriptions system, building upon the existing token protocol infrastructure.
- Introduces
ProtocolExtractoras a unified entry point that intelligently routes between strict token protocols and flexible generic protocols - Adds
GenericProtocolExtractorwith automatic type inference for ABI encoding of arbitrary JSON protocol data - Implements the collections protocol with
CollectionsManagerandEthscriptionERC721contracts for managing NFT collections
Reviewed Changes
Copilot reviewed 29 out of 29 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| spec/models/protocol_extractor_spec.rb | Tests unified protocol extraction routing between token and generic extractors |
| spec/models/token_params_extractor_via_protocol_spec.rb | Verifies token protocol continues working through unified interface |
| spec/models/generic_protocol_*_spec.rb | Comprehensive testing of generic protocol extraction and type inference |
| contracts/src/protocols/ | New protocol handler interfaces and ERC721 collection contract implementation |
| contracts/src/Ethscriptions.sol | Updated to use generic protocol framework with handler registration |
| contracts/src/TokenManager.sol | Refactored to implement IProtocolHandler interface |
| app/models/protocol_extractor.rb | Ruby implementation of unified protocol extraction |
| app/models/generic_protocol_extractor.rb | Generic protocol extractor with automatic type inference |
Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.
| context 'marketplace protocol' do | ||
| # it 'extracts create_listing with string numbers' do | ||
| # content_uri = 'data:,{"p":"marketplace","op":"create_listing","tokenId":"0x4567","price":"1000000000000000000","duration":"604800"}' | ||
|
|
||
| # protocol, operation, encoded_data = GenericProtocolExtractor.extract(content_uri) | ||
|
|
||
| # expect(protocol).to eq('marketplace'.b) | ||
| # expect(operation).to eq('create_listing'.b) | ||
|
|
||
| # types = ['uint256', 'uint256', 'string'] | ||
| # decoded = Eth::Abi.decode(types, encoded_data) | ||
|
|
||
| # expect(decoded[0]).to eq(604800) | ||
| # expect(decoded[1]).to eq(1000000000000000000) # 1 ETH in wei | ||
| # expect(decoded[2]).to eq('0x4567') | ||
| # end | ||
| end | ||
|
|
There was a problem hiding this comment.
This entire marketplace protocol test context contains only commented-out code. Either implement the test or remove this dead code to keep the test suite clean.
| context 'marketplace protocol' do | |
| # it 'extracts create_listing with string numbers' do | |
| # content_uri = 'data:,{"p":"marketplace","op":"create_listing","tokenId":"0x4567","price":"1000000000000000000","duration":"604800"}' | |
| # protocol, operation, encoded_data = GenericProtocolExtractor.extract(content_uri) | |
| # expect(protocol).to eq('marketplace'.b) | |
| # expect(operation).to eq('create_listing'.b) | |
| # types = ['uint256', 'uint256', 'string'] | |
| # decoded = Eth::Abi.decode(types, encoded_data) | |
| # expect(decoded[0]).to eq(604800) | |
| # expect(decoded[1]).to eq(1000000000000000000) # 1 ETH in wei | |
| # expect(decoded[2]).to eq('0x4567') | |
| # end | |
| end |
| # it 'recognizes various fixed-length bytes types' do | ||
| # content_uri = 'data:,{"p":"test","op":"bytes_test","byte1":"0x12","byte4":"0x12345678","byte16":"0x123456789abcdef0123456789abcdef0"}' | ||
|
|
||
| # protocol, operation, encoded_data = GenericProtocolExtractor.extract(content_uri) | ||
|
|
||
| # types = ['bytes1', 'bytes16', 'bytes4'] | ||
| # decoded = Eth::Abi.decode(types, encoded_data) | ||
|
|
||
| # expect(decoded[0]).to eq('0x12') | ||
| # expect(decoded[1]).to eq('0x123456789abcdef0123456789abcdef0') | ||
| # expect(decoded[2]).to eq('0x12345678') | ||
| # end | ||
|
|
There was a problem hiding this comment.
This commented-out test for various fixed-length bytes types should either be implemented or removed. The current implementation in the extractor raises 'Not supported' for non-address/bytes32 types, so this test would fail if uncommented.
| # it 'recognizes various fixed-length bytes types' do | |
| # content_uri = 'data:,{"p":"test","op":"bytes_test","byte1":"0x12","byte4":"0x12345678","byte16":"0x123456789abcdef0123456789abcdef0"}' | |
| # protocol, operation, encoded_data = GenericProtocolExtractor.extract(content_uri) | |
| # types = ['bytes1', 'bytes16', 'bytes4'] | |
| # decoded = Eth::Abi.decode(types, encoded_data) | |
| # expect(decoded[0]).to eq('0x12') | |
| # expect(decoded[1]).to eq('0x123456789abcdef0123456789abcdef0') | |
| # expect(decoded[2]).to eq('0x12345678') | |
| # end |
| // CollectionItem memory item = collectionItems[txHash]; | ||
|
|
||
| // // If this ethscription is part of a collection, sync ownership | ||
| // if (item.collectionId != bytes32(0)) { | ||
| // CollectionInfo memory collection = collections[item.collectionId]; | ||
| // if (collection.collectionContract != address(0)) { | ||
| // // Sync the ownership in the ERC721 contract | ||
| // EthscriptionERC721(collection.collectionContract).syncOwnership(item.tokenId); | ||
| // } | ||
| // } |
There was a problem hiding this comment.
The onTransfer function contains only commented-out code. Either implement the transfer synchronization logic or remove this dead code. The function currently does nothing when ethscriptions are transferred.
| // CollectionItem memory item = collectionItems[txHash]; | |
| // // If this ethscription is part of a collection, sync ownership | |
| // if (item.collectionId != bytes32(0)) { | |
| // CollectionInfo memory collection = collections[item.collectionId]; | |
| // if (collection.collectionContract != address(0)) { | |
| // // Sync the ownership in the ERC721 contract | |
| // EthscriptionERC721(collection.collectionContract).syncOwnership(item.tokenId); | |
| // } | |
| // } | |
| CollectionItem memory item = collectionItems[txHash]; | |
| // If this ethscription is part of a collection, sync ownership | |
| if (item.collectionId != bytes32(0)) { | |
| CollectionInfo memory collection = collections[item.collectionId]; | |
| if (collection.collectionContract != address(0)) { | |
| // Sync the ownership in the ERC721 contract | |
| EthscriptionERC721(collection.collectionContract).syncOwnership(item.tokenId); | |
| } | |
| } |
| // Create an ethscription to add to the collection | ||
| vm.prank(alice); | ||
|
|
||
| string memory itemContent = 'data:,{"p":"collections","op":"add","collection":"0x1234","item":"artwork1"}'; |
There was a problem hiding this comment.
what's item here? is it the item's data uri?
There was a problem hiding this comment.
The tests are completely messed up rn!
There was a problem hiding this comment.
Pull Request Overview
Copilot reviewed 34 out of 34 changed files in this pull request and generated 3 comments.
Comments suppressed due to low confidence (1)
spec/models/token_params_extractor_spec.rb:1
- Remove the commented-out debugging statement.
require 'rails_helper'
Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.
| # if receipt['status'] != '0x1' | ||
| # ap EthRpcClient.l2.trace_transaction(l2_tx['hash']) | ||
| # end |
There was a problem hiding this comment.
Remove the commented-out debug code or move it to a proper debug logging mechanism if it's still needed.
| // If they differ, update our state | ||
| if (actualOwner != recordedOwner) { | ||
| // Use internal _transfer to update state and emit event | ||
| _transfer(recordedOwner, actualOwner, tokenId); |
There was a problem hiding this comment.
The _transfer function should validate that the token exists and that the recorded owner actually owns it before performing the transfer. Consider adding these checks to prevent potential state corruption.
|
|
||
| def parse_json_safely(json_str) | ||
| # Size check | ||
| raise ExtractionError, "JSON too large" if json_str.bytesize > 10_000 |
There was a problem hiding this comment.
[nitpick] Consider making the JSON size limit configurable or define it as a constant rather than hardcoding the value.
There was a problem hiding this comment.
Pull Request Overview
Copilot reviewed 43 out of 43 changed files in this pull request and generated 5 comments.
Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.
| # if receipt['status'] != '0x1' | ||
| # ap EthRpcClient.l2.trace_transaction(l2_tx['hash']) | ||
| # end |
There was a problem hiding this comment.
Remove commented debugging code before merging to production. This appears to be leftover debugging logic that should be cleaned up.
| # if receipt['status'] != '0x1' | |
| # ap EthRpcClient.l2.trace_transaction(l2_tx['hash']) | |
| # end |
| "max" => "1000000", | ||
| "lim" => "100" | ||
| } | ||
| # binding.irb |
There was a problem hiding this comment.
Remove debugging breakpoint before merging to production.
| # binding.irb |
| @@ -2,6 +2,7 @@ | |||
| require 'retriable' | |||
There was a problem hiding this comment.
The Memery include is added but there's no corresponding require statement for the gem. Ensure Memery is properly required or imported.
| require 'retriable' | |
| require 'retriable' | |
| require 'memery' |
| // import "@openzeppelin/contracts-upgradeable/token/ERC721/ERC721Upgradeable.sol"; | ||
| import "../ERC721EthscriptionsUpgradeable.sol"; | ||
| // import "@openzeppelin/contracts-upgradeable/token/ERC721/extensions/ERC721EnumerableUpgradeable.sol"; | ||
| // import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; |
There was a problem hiding this comment.
Remove commented import statements. These appear to be replaced by the actual import but should be cleaned up for code clarity.
| // import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; |
| // // protocolParams: Ethscriptions.ProtocolParams({ | ||
| // // protocol: "", | ||
| // // tick: "", | ||
| // // max: 0, | ||
| // // lim: 0, | ||
| // // amt: 0 | ||
| // // operation: "", | ||
| // // data: "" | ||
| // // }) |
There was a problem hiding this comment.
Remove large blocks of commented test code or convert to proper documentation if the code examples are meant to be preserved for reference.
There was a problem hiding this comment.
Pull Request Overview
Copilot reviewed 46 out of 47 changed files in this pull request and generated 2 comments.
Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.
| # it 'extracts create_listing with string numbers' do | ||
| # content_uri = 'data:,{"p":"marketplace","op":"create_listing","tokenId":"0x4567","price":"1000000000000000000","duration":"604800"}' | ||
|
|
||
| # protocol, operation, encoded_data = GenericProtocolExtractor.extract(content_uri) | ||
|
|
||
| # expect(protocol).to eq('marketplace'.b) | ||
| # expect(operation).to eq('create_listing'.b) | ||
|
|
||
| # types = ['uint256', 'uint256', 'string'] | ||
| # decoded = Eth::Abi.decode(types, encoded_data) | ||
|
|
||
| # expect(decoded[0]).to eq(604800) | ||
| # expect(decoded[1]).to eq(1000000000000000000) # 1 ETH in wei | ||
| # expect(decoded[2]).to eq('0x4567') | ||
| # end |
There was a problem hiding this comment.
Remove large blocks of commented-out test code. If these tests are planned for future implementation, track them in issues or TODO comments instead.
| # it 'extracts create_listing with string numbers' do | |
| # content_uri = 'data:,{"p":"marketplace","op":"create_listing","tokenId":"0x4567","price":"1000000000000000000","duration":"604800"}' | |
| # protocol, operation, encoded_data = GenericProtocolExtractor.extract(content_uri) | |
| # expect(protocol).to eq('marketplace'.b) | |
| # expect(operation).to eq('create_listing'.b) | |
| # types = ['uint256', 'uint256', 'string'] | |
| # decoded = Eth::Abi.decode(types, encoded_data) | |
| # expect(decoded[0]).to eq(604800) | |
| # expect(decoded[1]).to eq(1000000000000000000) # 1 ETH in wei | |
| # expect(decoded[2]).to eq('0x4567') | |
| # end | |
| # TODO: Implement test for 'create_listing' operation (see removed commented-out test) |
| // mimeSubtype: "png", | ||
| // esip6: false, | ||
| // isCompressed: false, | ||
| // tokenParams: Ethscriptions.TokenParams({ | ||
| // op: "", protocol: "", tick: "", max: 0, lim: 0, amt: 0 | ||
| // protocolParams: Ethscriptions.ProtocolParams({ | ||
| // protocol: "", operation: "", data: "" | ||
| // }) |
There was a problem hiding this comment.
Remove large blocks of commented-out test code. If compression testing is planned for future implementation, create proper test stubs or track in issues.
Note
Introduce a generic protocol framework with Collections support, protocol handler registry, and ERC-721 enumerable upgrades across contracts, extractors, libs, and tests.
GenericProtocolExtractor,CollectionsParamsExtractor, and unifiedProtocolExtractorwith calldata encoding; introduceDataUriparser.ProtocolEventReader,CollectionsReader,TokenReader.CollectionsManager,EthscriptionERC721(non-transferable ERC-721 with enumerable), andIProtocolHandler.Ethscriptions: protocol-aware creation/transfer hooks, media URI generation, batched proving interactions; addgetEthscriptionWithContent.TokenManagerto protocol handler pattern; add deterministic clone/predict helpers.L2Genesis.s.sol): deploy/register protocol handlers; add predeploys for ERC721 template and CollectionsManager; adjust genesis flow.ERC721EthscriptionsUpgradeableto store name/symbol and support IERC721Enumerable.Predeploys.sol) incl. depositor account and new templates.L1RpcPrefetcher(throttling, caching latest, stats); enhanceGenesisGenerator(quiet logging, env flags).facet_rails_commonusage fromApplicationController.Written by Cursor Bugbot for commit 28d6bf9. This will update automatically on new commits. Configure here.