From f1213b6b89949f2e3086efd14dbf6301115ce523 Mon Sep 17 00:00:00 2001 From: Dylan Moreland Date: Tue, 12 May 2026 14:50:59 -0400 Subject: [PATCH] add mintVehicle without device definition Adds a no-DD mint entry point that emits the existing VehicleNodeMinted event. Lays groundwork for retiring the DD-bearing mint variants once the off-chain device definition flow lands. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../nodes/Vehicle.sol/Vehicle.json | 35 ++++ contracts/implementations/nodes/Vehicle.sol | 34 ++++ test/nodes/Vehicle.test.ts | 173 ++++++++++++++++++ 3 files changed, 242 insertions(+) diff --git a/abis/contracts/implementations/nodes/Vehicle.sol/Vehicle.json b/abis/contracts/implementations/nodes/Vehicle.sol/Vehicle.json index 7b1d3e1e..63d49730 100644 --- a/abis/contracts/implementations/nodes/Vehicle.sol/Vehicle.json +++ b/abis/contracts/implementations/nodes/Vehicle.sol/Vehicle.json @@ -360,6 +360,41 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "manufacturerNode", + "type": "uint256" + }, + { + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "components": [ + { + "internalType": "string", + "name": "attribute", + "type": "string" + }, + { + "internalType": "string", + "name": "info", + "type": "string" + } + ], + "internalType": "struct AttributeInfoPair[]", + "name": "attrInfo", + "type": "tuple[]" + } + ], + "name": "mintVehicle", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [ { diff --git a/contracts/implementations/nodes/Vehicle.sol b/contracts/implementations/nodes/Vehicle.sol index e2e01511..c68e2794 100644 --- a/contracts/implementations/nodes/Vehicle.sol +++ b/contracts/implementations/nodes/Vehicle.sol @@ -119,6 +119,40 @@ contract Vehicle is ChargingInternal._chargeDcx(msg.sender, MINT_VEHICLE_OPERATION); } + /** + * @notice Function to mint a vehicle without a Device Definition Id + * @dev Device Definitions are tracked off-chain; on-chain mints no longer carry them + * @param manufacturerNode Parent manufacturer node id + * @param owner The address of the new owner + * @param attrInfo List of attribute-info pairs to be added + */ + function mintVehicle( + uint256 manufacturerNode, + address owner, + AttributeInfoPair[] calldata attrInfo + ) external { + VehicleStorage.Storage storage vs = VehicleStorage.getStorage(); + address vehicleIdProxyAddress = vs.idProxyAddress; + + if ( + !INFT(ManufacturerStorage.getStorage().idProxyAddress).exists( + manufacturerNode + ) + ) revert InvalidParentNode(manufacturerNode); + + uint256 newTokenId = INFT(vehicleIdProxyAddress).safeMint(owner); + + NodesStorage + .getStorage() + .nodes[vehicleIdProxyAddress][newTokenId].parentNode = manufacturerNode; + + emit VehicleNodeMinted(manufacturerNode, newTokenId, owner); + + if (attrInfo.length > 0) _setInfos(newTokenId, attrInfo); + + ChargingInternal._chargeDcx(msg.sender, MINT_VEHICLE_OPERATION); + } + /** * @notice Function to mint a vehicle with a Device Definition Id and associate it with a Storage Node Id * @dev This function creates a new vehicle node, associates it with a manufacturer, diff --git a/test/nodes/Vehicle.test.ts b/test/nodes/Vehicle.test.ts index 7432b567..26156364 100644 --- a/test/nodes/Vehicle.test.ts +++ b/test/nodes/Vehicle.test.ts @@ -441,6 +441,179 @@ describe('Vehicle', function () { }); }); + describe('mintVehicle(uint256,address,(string,string)[])', () => { + context('Error handling', () => { + it('Should revert if parent node is not a manufacturer node', async () => { + await expect( + vehicleInstance + .connect(admin) + .mintVehicle( + 99, + user1.address, + C.mockVehicleAttributeInfoPairs, + ) + ).to.be.revertedWithCustomError(vehicleInstance, 'InvalidParentNode') + .withArgs(99); + }); + it('Should revert if attribute is not whitelisted', async () => { + await expect( + vehicleInstance + .connect(admin) + .mintVehicle( + 1, + user1.address, + C.mockVehicleAttributeInfoPairsNotWhitelisted + ) + ).to.be.revertedWithCustomError( + vehicleInstance, + 'AttributeNotWhitelisted' + ).withArgs(C.mockVehicleAttributeInfoPairsNotWhitelisted[1].attribute); + }); + }); + + context('State', () => { + it('Should correctly set parent node', async () => { + await vehicleInstance + .connect(admin) + .mintVehicle( + 1, + user1.address, + C.mockVehicleAttributeInfoPairs + ); + + const parentNode = await nodesInstance.getParentNode( + await vehicleIdInstance.getAddress(), + 1 + ); + expect(parentNode).to.be.equal(1); + }); + it('Should correctly set node owner', async () => { + await vehicleInstance + .connect(admin) + .mintVehicle( + 1, + user1.address, + C.mockVehicleAttributeInfoPairs + ); + + expect(await vehicleIdInstance.ownerOf(1)).to.be.equal(user1.address); + }); + it('Should not set a Device Definition Id', async () => { + await vehicleInstance + .connect(admin) + .mintVehicle( + 1, + user1.address, + C.mockVehicleAttributeInfoPairs + ); + + expect( + await vehicleInstance.getDeviceDefinitionIdByVehicleId(1) + ).to.be.equal(''); + }); + it('Should correctly set infos', async () => { + await vehicleInstance + .connect(admin) + .mintVehicle( + 1, + user1.address, + C.mockVehicleAttributeInfoPairs + ); + + expect( + await nodesInstance.getInfo( + await vehicleIdInstance.getAddress(), + 1, + C.mockVehicleAttribute1 + ) + ).to.be.equal(C.mockVehicleInfo1); + expect( + await nodesInstance.getInfo( + await vehicleIdInstance.getAddress(), + 1, + C.mockVehicleAttribute2 + ) + ).to.be.equal(C.mockVehicleInfo2); + }); + it('Should correctly burn DIMO Credit tokens from the sender', async () => { + await expect(() => + vehicleInstance + .connect(admin) + .mintVehicle( + 1, + user1.address, + C.mockVehicleAttributeInfoPairs + ) + ).changeTokenBalance( + mockDimoCreditInstance, + admin.address, + -C.MINT_VEHICLE_OPERATION_COST + ); + }); + }); + + context('Events', () => { + it('Should emit VehicleNodeMinted event with correct params', async () => { + await expect( + vehicleInstance + .connect(admin) + .mintVehicle( + 1, + user1.address, + C.mockVehicleAttributeInfoPairs + ) + ) + .to.emit(vehicleInstance, 'VehicleNodeMinted') + .withArgs(1, 1, user1.address); + }); + it('Should not emit VehicleNodeMintedWithDeviceDefinition event', async () => { + await expect( + vehicleInstance + .connect(admin) + .mintVehicle( + 1, + user1.address, + C.mockVehicleAttributeInfoPairs + ) + ).to.not.emit(vehicleInstance, 'VehicleNodeMintedWithDeviceDefinition'); + }); + it('Should emit VehicleAttributeSet events with correct params', async () => { + await expect( + vehicleInstance + .connect(admin) + .mintVehicle( + 1, + user1.address, + C.mockVehicleAttributeInfoPairs + ) + ) + .to.emit(vehicleInstance, 'VehicleAttributeSet') + .withArgs( + 1, + C.mockVehicleAttributeInfoPairs[0].attribute, + C.mockVehicleAttributeInfoPairs[0].info + ) + .to.emit(vehicleInstance, 'VehicleAttributeSet') + .withArgs( + 1, + C.mockVehicleAttributeInfoPairs[1].attribute, + C.mockVehicleAttributeInfoPairs[1].info + ); + }); + it('Should not emit VehicleAttributeSet event if attrInfo is empty', async () => { + await expect( + vehicleInstance + .connect(admin) + .mintVehicle( + 1, + user1.address, + [] + ) + ).to.not.emit(vehicleInstance, 'VehicleAttributeSet'); + }); + }); + }); + describe('mintVehicleWithDeviceDefinition(uint256,address,uint256,string,(string,string)[])', () => { context('Error handling', () => { it('Should revert if parent node is not a manufacturer node', async () => {