Skip to content

Commit

Permalink
feat: mint albums and songs (#9)
Browse files Browse the repository at this point in the history
* feat: mint albums and songs

* update token id
  • Loading branch information
guillaumedebavelaere authored Apr 5, 2024
1 parent ebb9ef7 commit e821ff8
Show file tree
Hide file tree
Showing 2 changed files with 140 additions and 37 deletions.
65 changes: 49 additions & 16 deletions backend/contracts/Albums.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,27 @@
pragma solidity ^0.8.24;

import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
import {ERC1155Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC1155/ERC1155Upgradeable.sol";

contract Albums is OwnableUpgradeable {
contract Albums is OwnableUpgradeable, ERC1155Upgradeable {

struct Album {
uint16 id;
uint16 id; // starts from 1 (0 is reserved for non existing album)
uint64 maxSupply;
uint64 price;
string uri;
uint16 currentSongId;
}

struct Song {
uint16 id;
uint16 id; // starts from 1 (0 is reserved for token ID representing an album)
uint64 maxSupply;
uint64 price;
string uri;
}


mapping(uint16 albumId => Album) private _albums;
mapping(uint16 albumId => mapping(uint16 songID => Song)) private _albumSongs;
mapping(uint16 albumId => mapping(uint16 songId => Song)) private _albumSongs;

// The current album ID. Used to generate next one.
uint16 private _currentAlbumId;
Expand All @@ -45,6 +45,7 @@ contract Albums is OwnableUpgradeable {
function initialize(
address _user
) external initializer {
__ERC1155_init(""); // TODO: Add metadata URI
_transferOwnership(_user);
}

Expand All @@ -55,23 +56,20 @@ contract Albums is OwnableUpgradeable {
) external onlyOwner returns (uint256) {
require(maxSupply > 0, "Max supply must be greater than 0");
require(price > 0, "Price must be greater than 0");
require(bytes(album_uri).length > 0, "URI must not be empty");

uint16 albumId = _currentAlbumId;
_currentAlbumId++;

_albums[albumId] = Album({
id: albumId,
_albums[_currentAlbumId] = Album({
id: _currentAlbumId,
maxSupply: maxSupply,
price: price,
uri: album_uri,
currentSongId: 0
});

emit ItemCreated(msg.sender, albumId, album_uri, 0, maxSupply);

_currentAlbumId++;
emit ItemCreated(msg.sender, _currentAlbumId, album_uri, 0, maxSupply);

return albumId;
return _currentAlbumId;
}

function createSong(
Expand All @@ -91,8 +89,9 @@ contract Albums is OwnableUpgradeable {
require(numberOfSongs < maxSupply, "Maximum supply reached");
require(numberOfSongs <= type(uint16).max, "Value exceeds uint16 range");

album.currentSongId++;

uint16 songId = album.currentSongId;


_albumSongs[albumId][songId] = Song({
id: songId,
Expand All @@ -101,13 +100,40 @@ contract Albums is OwnableUpgradeable {
uri: songUri
});

album.currentSongId++;

emit ItemCreated(msg.sender, albumId, songUri, songId, maxSupply);

return songId;
}

function mintAlbum(uint16 albumId) external payable returns (uint256) {
Album storage album = _albums[albumId];
require(album.maxSupply > 0, "Album does not exist");
require(album.price == msg.value, "Invalid price");

uint256 tokenId = getTokenId(owner(), album.id, 0);
_mint(msg.sender, tokenId, 1, "");

emit ItemMinted(owner(), msg.sender, albumId, 0);

return tokenId;
}

function mintSong(uint16 albumId, uint16 songId) external payable returns (uint256) {
Album storage album = _albums[albumId];
require(album.maxSupply > 0, "Album does not exist");

Song storage song = _albumSongs[albumId][songId];
require(song.maxSupply > 0, "Song does not exist");
require(song.price == msg.value, "Invalid price");

uint256 tokenId = getTokenId(owner(), album.id, song.id);
_mint(msg.sender, tokenId, 1, "");

emit ItemMinted(owner(), msg.sender, album.id, song.id);

return tokenId;
}

function getAlbum(uint16 albumId) external view returns (Album memory) {
return _albums[albumId];
}
Expand All @@ -124,4 +150,11 @@ contract Albums is OwnableUpgradeable {
return _albums[albumId].currentSongId;
}

/**
* Produce a unique token ID, combining the artist's address with the album and song id.
*/
function getTokenId(address artistAddress, uint16 albumId, uint16 songId) public pure returns (uint256) {
return uint256(keccak256(abi.encodePacked(artistAddress, albumId, songId)));
}

}
112 changes: 91 additions & 21 deletions backend/test/Albums.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,12 +49,6 @@ describe("Albums test", async function () {
).to.be.revertedWith("Max supply must be greater than 0");
});

it("should fail if uri is empty", async function () {
await expect(
albums.connect(owner).createAlbum(1000, 100, "")
).to.be.revertedWith("URI must not be empty");
});

context("when the album is created", async function () {
beforeEach(async function () {
expect(await albums.getAlbumsCount()).to.equal(0);
Expand All @@ -63,9 +57,9 @@ describe("Albums test", async function () {

it("should create an album", async function () {
expect(await albums.getAlbumsCount()).to.equal(1);
const album = await albums.getAlbum(0);
const album = await albums.getAlbum(1);

expect(album.id).to.equal(0);
expect(album.id).to.equal(1);
expect(album.price).to.equal(100);
expect(album.maxSupply).to.equal(1000);
expect(album.uri).to.equal("uri album");
Expand All @@ -77,6 +71,44 @@ describe("Albums test", async function () {
.withArgs(owner.address, 0, 1000, 100, "uri album");
});

describe("mintAlbum", async function () {
it("should fail if albumId does not exist", async function () {
await expect(
albums.connect(otherAccount1).mintAlbum(0)
).to.be.revertedWith("Album does not exist");
});

it("should fail if price is 0", async function () {
await expect(
albums.connect(otherAccount1).mintAlbum(1)
).to.be.revertedWith("Invalid price");
});

context("when the album is minted", async function () {
it("should mint an album", async function () {
const tokenId = await albums.getTokenId(owner.address, 1, 0);

expect(
await albums.balanceOf(otherAccount1.address, tokenId)
).to.equal(0);

await albums.connect(otherAccount1).mintAlbum(1, { value: 100 });

expect(
await albums.balanceOf(otherAccount1.address, tokenId)
).to.equal(1);
});

it("should emit an ItemMinted event", async () => {
await expect(
albums.connect(otherAccount1).mintAlbum(1, { value: 100 })
)
.to.emit(albums, "ItemMinted")
.withArgs(owner.address, otherAccount1.address, 1, 0);
});
});
});

describe("createSong", async function () {
it("should fail if it is not created by the owner", async function () {
await expect(
Expand All @@ -96,30 +128,68 @@ describe("Albums test", async function () {
).to.be.revertedWith("Price must be greater than 0");
});

it("should fail if uri is empty", async function () {
await expect(
albums.connect(owner).createSong(0, 1000, 100, "")
).to.be.revertedWith("URI must not be empty");
});

context("when the song is created", async function () {
it("should create a song", async function () {
expect(await albums.getAlbumSongsCount(0)).to.equal(0);
await albums.connect(owner).createSong(0, 1000, 100, "uri song");
expect(await albums.getAlbumSongsCount(1)).to.equal(0);
await albums.connect(owner).createSong(1, 1000, 100, "uri song");

expect(await albums.getAlbumSongsCount(0)).to.equal(1);
const song = await albums.getSong(0, 0);
expect(await albums.getAlbumSongsCount(1)).to.equal(1);
const song = await albums.getSong(1, 1);

expect(song.id).to.equal(0);
expect(song.id).to.equal(1);
expect(song.price).to.equal(100);
expect(song.maxSupply).to.equal(1000);
expect(song.uri).to.equal("uri song");
});

it("should emit an ItemCreated event", async () => {
expect(await albums.connect(owner).createSong(0, 1000, 100, "uri"))
expect(await albums.connect(owner).createSong(1, 1000, 100, "uri"))
.to.emit(albums, "ItemCreated")
.withArgs(owner.address, 0, 1000, 100, "uri song");
.withArgs(owner.address, 1, 1000, 100, "uri song");
});

describe("mintSong", async function () {
beforeEach(async function () {
await albums.connect(owner).createSong(1, 1000, 100, "uri song");
});

it("should fail if songId does not exist", async function () {
await expect(
albums.connect(otherAccount1).mintSong(1, 0, { value: 100 })
).to.be.revertedWith("Song does not exist");
});

it("should fail if price is 0", async function () {
await expect(
albums.connect(otherAccount1).mintSong(1, 1)
).to.be.revertedWith("Invalid price");
});

context("when the song is minted", async function () {
it("should mint a song", async function () {
const tokenId = await albums.getTokenId(owner.address, 1, 1);

expect(
await albums.balanceOf(otherAccount1.address, tokenId)
).to.equal(0);

await albums
.connect(otherAccount1)
.mintSong(1, 1, { value: 100 });

expect(
await albums.balanceOf(otherAccount1.address, tokenId)
).to.equal(1);
});

it("should emit an ItemMinted event", async () => {
await expect(
albums.connect(otherAccount1).mintSong(1, 1, { value: 100 })
)
.to.emit(albums, "ItemMinted")
.withArgs(owner.address, otherAccount1.address, 1, 1);
});
});
});
});
});
Expand Down

0 comments on commit e821ff8

Please sign in to comment.