Skip to content
This repository has been archived by the owner on Apr 30, 2024. It is now read-only.

Commit

Permalink
feat: License NFT URI for UMLPolicyFrameworkManager, test and lint
Browse files Browse the repository at this point in the history
  • Loading branch information
jdubpark committed Jan 30, 2024
1 parent dc32ced commit b7607c1
Show file tree
Hide file tree
Showing 2 changed files with 230 additions and 4 deletions.
114 changes: 110 additions & 4 deletions contracts/modules/licensing/UMLPolicyFrameworkManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@

pragma solidity ^0.8.23;

// contracts
// external
import { Base64 } from "@openzeppelin/contracts/utils/Base64.sol";
import { Strings } from "@openzeppelin/contracts/utils/Strings.sol";

// contracts
import { ShortStringOps } from "contracts/utils/ShortStringOps.sol";
import { LicenseRegistry } from "contracts/registries/LicenseRegistry.sol";
import { Licensing } from "contracts/lib/Licensing.sol";
Expand Down Expand Up @@ -38,7 +41,12 @@ contract UMLPolicyFrameworkManager is
string memory licenseUrl
) BasePolicyFrameworkManager(licRegistry, licenseUrl) LicensorApprovalManager(accessController) {}

function licenseRegistry() external view override(BasePolicyFrameworkManager, IPolicyFrameworkManager) returns (address) {
function licenseRegistry()
external
view
override(BasePolicyFrameworkManager, IPolicyFrameworkManager)
returns (address)
{
return address(LICENSE_REGISTRY);
}

Expand Down Expand Up @@ -70,8 +78,106 @@ contract UMLPolicyFrameworkManager is
policy = abi.decode(protocolPolicy.data, (UMLPolicy));
}

function policyToJson(bytes memory) public view returns (string memory) {
return "TODO";
function policyToJson(bytes memory policyData) public view returns (string memory) {
UMLPolicy memory policy = abi.decode(policyData, (UMLPolicy));

/* solhint-disable */
// Follows the OpenSea standard for JSON metadata

// base json
string memory json = string(
'{"name": "Story Protocol License NFT", "description": "License agreement stating the terms of a Story Protocol IPAsset", "attributes": ['
);

// bool attribution;
// bool transferable;
// bool commercialUse;
// bool commercialAttribution;
// string[] commercializers;
// uint256 commercialRevShare;
// bool derivativesAllowed;
// bool derivativesAttribution;
// bool derivativesApproval;
// bool derivativesReciprocal;
// uint256 derivativesRevShare;
// string[] territories;
// string[] distributionChannels;

// Attributions
json = string(
abi.encodePacked(
json,
'{"trait_type": "Attribution", "value": "',
policy.attribution ? "true" : "false",
'"},',
'{"trait_type": "Transferable", "value": "',
policy.transferable ? "true" : "false",
'"},',
'{"trait_type": "Commerical Use", "value": "',
policy.commercialUse ? "true" : "false",
'"},',
'{"trait_type": "commercialAttribution", "value": "',
policy.commercialAttribution ? "true" : "false",
'"},',
'{"trait_type": "commercialRevShare", "value": ',
Strings.toString(policy.commercialRevShare),
"},"
'{"trait_type": "commercializers", "value": ['
)
);

uint256 commercializerCount = policy.commercializers.length;
for (uint256 i = 0; i < commercializerCount; ++i) {
json = string(abi.encodePacked(json, '"', policy.commercializers[i], '"'));
if (i != commercializerCount - 1) {
json = string(abi.encodePacked(json, ","));
}
}

json = string(
abi.encodePacked(
json,
']}, {"trait_type": "derivativesAllowed", "value": "',
policy.derivativesAllowed ? "true" : "false",
'"},',
'{"trait_type": "derivativesAttribution", "value": "',
policy.derivativesAttribution ? "true" : "false",
'"},',
'{"trait_type": "derivativesApproval", "value": "',
policy.derivativesApproval ? "true" : "false",
'"},',
'{"trait_type": "derivativesReciprocal", "value": "',
policy.derivativesReciprocal ? "true" : "false",
'"},',
'{"trait_type": "derivativesRevShare", "value": ',
Strings.toString(policy.derivativesRevShare),
"},"
'{"trait_type": "territories", "value": ['
)
);

uint256 territoryCount = policy.territories.length;
for (uint256 i = 0; i < territoryCount; ++i) {
json = string(abi.encodePacked(json, '"', policy.territories[i], '"'));
if (i != territoryCount - 1) {
json = string(abi.encodePacked(json, ","));
}
}

json = string(abi.encodePacked(json, ']}, {"trait_type": "distributionChannels", "value": ['));

uint256 distributionChannelCount = policy.distributionChannels.length;
for (uint256 i = 0; i < distributionChannelCount; ++i) {
json = string(abi.encodePacked(json, '"', policy.distributionChannels[i], '"'));
if (i != distributionChannelCount - 1) {
json = string(abi.encodePacked(json, ","));
}
}

json = string(abi.encodePacked(json, "]}]}"));
/* solhint-enable */

return string(abi.encodePacked("data:application/json;base64,", Base64.encode(bytes(json))));
}

function supportsInterface(
Expand Down
120 changes: 120 additions & 0 deletions test/foundry/registries/LicenseRegistry.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import { ERC6551Registry } from "lib/reference/src/ERC6551Registry.sol";
import { IPAccountImpl} from "contracts/IPAccountImpl.sol";
import { IPAccountRegistry } from "contracts/registries/IPAccountRegistry.sol";
import { MockERC721 } from "test/foundry/mocks/MockERC721.sol";
import { UMLPolicyFrameworkManager, UMLPolicy } from "contracts/modules/licensing/UMLPolicyFrameworkManager.sol";
import { Base64 } from "@openzeppelin/contracts/utils/Base64.sol";

contract LicenseRegistryTest is Test {
using Strings for *;
Expand All @@ -27,6 +29,7 @@ contract LicenseRegistryTest is Test {
Licensing.PolicyFramework public framework;

MockPolicyFrameworkManager public module1;
UMLPolicyFrameworkManager public umlManager;

MockERC721 nft = new MockERC721("MockERC721");

Expand All @@ -50,6 +53,11 @@ contract LicenseRegistryTest is Test {
supportVerifyMint: true,
supportVerifyTransfer: true
}));
umlManager = new UMLPolicyFrameworkManager(
address(accessController),
address(registry),
licenseUrl
);

nft.mintId(ipOwner, 1);
nft.mintId(ipOwner, 2);
Expand Down Expand Up @@ -299,4 +307,116 @@ contract LicenseRegistryTest is Test {
vm.prank(licenseHolder);
registry.safeTransferFrom(licenseHolder, licenseHolder2, licenseId, 1, "");
}

function test_LicenseRegistry_licenseUri() public {
umlManager.register();

UMLPolicy memory policyData = UMLPolicy({
attribution: true,
transferable: true,
commercialUse: true,
commercialAttribution: true,
commercializers: new string[](2),
commercialRevShare: 0,
derivativesAllowed: true,
derivativesAttribution: true,
derivativesApproval: true,
derivativesReciprocal: true,
derivativesRevShare: 0,
territories: new string[](1),
distributionChannels: new string[](1)
});

policyData.commercializers[0] = "commercializer1";
policyData.commercializers[1] = "commercializer2";
policyData.territories[0] = "territory1";
policyData.distributionChannels[0] = "distributionChannel1";

uint256 policyId = umlManager.addPolicy(policyData);

registry.addPolicyToIp(ipId1, policyId);

uint256 licenseId = registry.mintLicense(policyId, ipId1, 1, licenseHolder);

string memory actualUri = registry.uri(licenseId);

/* solhint-disable */
// NOTE: In STRING version, no spacing between key and value (eg. "value":"true" instead of "value": "true")
// DEV : Since the raw string below produces stack too deep error, we use the encoded output of the string below.
// The string below is left here for reference.
/*
string memory expectedJson = string(abi.encodePacked('{',
'"name":"Story Protocol License NFT",'
'"description":"License agreement stating the terms of a Story Protocol IPAsset",',
'"attributes":[',
'{',
'"trait_type":"Attribution",'
'"value":"true"',
'},',
'{',
'"trait_type":"Transferable",',
'"value":"true"',
'},',
'{',
'"trait_type":"Commerical Use",',
'"value":"true"',
'},',
'{',
'"trait_type":"commercialAttribution",',
'"value":"true"',
'},',
'{',
'"trait_type":"commercialRevShare",',
'"value":0',
'},',
'{',
'"trait_type":"commercializers",',
'"value":[',
'"commercializer1",',
'"commercializer2"',
']',
'},',
'{',
'"trait_type":"derivativesAllowed",',
'"value":"true"',
'},',
'{',
'"trait_type":"derivativesAttribution",',
'"value":"true"',
'},',
'{',
'"trait_type":"derivativesApproval",',
'"value":"true"',
'},',
'{',
'"trait_type":"derivativesReciprocal",',
'"value":"true"',
'},',
'{',
'"trait_type":"derivativesRevShare",',
'"value":0',
'},',
'{',
'"trait_type":"territories",',
'"value":[',
'"territory1",',
']',
'},'
'{',
'"trait_type":"distributionChannels"',
'"value":[',
'"distributionChannel1",',
']',
'}',
']',
'}'
));
*/
/* solhint-enable */
string memory expectedJson = "eyJuYW1lIjogIlN0b3J5IFByb3RvY29sIExpY2Vuc2UgTkZUIiwgImRlc2NyaXB0aW9uIjogIkxpY2Vuc2UgYWdyZWVtZW50IHN0YXRpbmcgdGhlIHRlcm1zIG9mIGEgU3RvcnkgUHJvdG9jb2wgSVBBc3NldCIsICJhdHRyaWJ1dGVzIjogW3sidHJhaXRfdHlwZSI6ICJBdHRyaWJ1dGlvbiIsICJ2YWx1ZSI6ICJ0cnVlIn0seyJ0cmFpdF90eXBlIjogIlRyYW5zZmVyYWJsZSIsICJ2YWx1ZSI6ICJ0cnVlIn0seyJ0cmFpdF90eXBlIjogIkNvbW1lcmljYWwgVXNlIiwgInZhbHVlIjogInRydWUifSx7InRyYWl0X3R5cGUiOiAiY29tbWVyY2lhbEF0dHJpYnV0aW9uIiwgInZhbHVlIjogInRydWUifSx7InRyYWl0X3R5cGUiOiAiY29tbWVyY2lhbFJldlNoYXJlIiwgInZhbHVlIjogMH0seyJ0cmFpdF90eXBlIjogImNvbW1lcmNpYWxpemVycyIsICJ2YWx1ZSI6IFsiY29tbWVyY2lhbGl6ZXIxIiwiY29tbWVyY2lhbGl6ZXIyIl19LCB7InRyYWl0X3R5cGUiOiAiZGVyaXZhdGl2ZXNBbGxvd2VkIiwgInZhbHVlIjogInRydWUifSx7InRyYWl0X3R5cGUiOiAiZGVyaXZhdGl2ZXNBdHRyaWJ1dGlvbiIsICJ2YWx1ZSI6ICJ0cnVlIn0seyJ0cmFpdF90eXBlIjogImRlcml2YXRpdmVzQXBwcm92YWwiLCAidmFsdWUiOiAidHJ1ZSJ9LHsidHJhaXRfdHlwZSI6ICJkZXJpdmF0aXZlc1JlY2lwcm9jYWwiLCAidmFsdWUiOiAidHJ1ZSJ9LHsidHJhaXRfdHlwZSI6ICJkZXJpdmF0aXZlc1JldlNoYXJlIiwgInZhbHVlIjogMH0seyJ0cmFpdF90eXBlIjogInRlcnJpdG9yaWVzIiwgInZhbHVlIjogWyJ0ZXJyaXRvcnkxIl19LCB7InRyYWl0X3R5cGUiOiAiZGlzdHJpYnV0aW9uQ2hhbm5lbHMiLCAidmFsdWUiOiBbImRpc3RyaWJ1dGlvbkNoYW5uZWwxIl19XX0=";

string memory expectedUri = string(abi.encodePacked("data:application/json;base64,", expectedJson));

assertEq(actualUri, expectedUri);
}
}

0 comments on commit b7607c1

Please sign in to comment.