Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add enumerable NFT extension #173

Merged
merged 5 commits into from
Dec 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions smart-contracts/assembly/contracts/NFT/NFT-example.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,10 @@ import {
_update,
_transferFrom,
} from './NFT-internals';
import { setOwner, onlyOwner } from '../utils/ownership';
import { onlyOwner } from '../utils/ownership';

import { Context, isDeployingContract } from '@massalabs/massa-as-sdk';
import { _setOwner } from '../utils/ownership-internal';

/**
* @param binaryArgs - serialized strings representing the name and the symbol of the NFT
Expand All @@ -52,7 +53,7 @@ export function constructor(binaryArgs: StaticArray<u8>): void {
.nextString()
.expect('symbol argument is missing or invalid');
_constructor(name, symbol);
setOwner(new Args().add(Context.caller().toString()).serialize());
_setOwner(Context.caller().toString());
}

export function name(): string {
Expand Down
10 changes: 6 additions & 4 deletions smart-contracts/assembly/contracts/NFT/NFT-internals.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,15 +54,15 @@ export function _constructor(name: string, symbol: string): void {
* @param address - address to get the balance for
* @returns the key of the balance in the storage for the given address
*/
function balanceKey(address: string): StaticArray<u8> {
export function balanceKey(address: string): StaticArray<u8> {
return BALANCE_KEY_PREFIX.concat(stringToBytes(address));
}

/**
* @param tokenId - the tokenID of the owner
* @returns the key of the owner in the storage for the given tokenId
*/
function ownerKey(tokenId: u256): StaticArray<u8> {
export function ownerKey(tokenId: u256): StaticArray<u8> {
return OWNER_KEY_PREFIX.concat(u256ToBytes(tokenId));
}

Expand Down Expand Up @@ -227,8 +227,7 @@ export function _isAuthorized(operator: string, tokenId: u256): bool {
* For example if you were to wrap this helper in a `transfer` function,
* you should check that the caller is the owner of the token, and then call the _update function.
*/

export function _update(to: string, tokenId: u256, auth: string): void {
export function _update(to: string, tokenId: u256, auth: string): string {
const from = _ownerOf(tokenId);
assert(to != from, 'The from and to addresses are the same');
if (auth != '') {
Expand Down Expand Up @@ -258,7 +257,10 @@ export function _update(to: string, tokenId: u256, auth: string): void {
// burn the token
Storage.del(ownerKey(tokenId));
}

return from;
}

/**
* Transfers the ownership of an NFT from one address to another address.
* @param from - The address of the current owner
Expand Down
251 changes: 251 additions & 0 deletions smart-contracts/assembly/contracts/NFT/NFTEnumerable-example.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,251 @@
/**
* This is an example of an NFT contract that uses the `NFTEnumerable-internals`
* helper functions to implement enumeration functionality similar to the ERC-721 Enumerable extension.
*
* **Note:** We have diverged from the ERC-721 Enumerable standard in this implementation.
* On the Massa blockchain, indices are not necessary for token enumeration because we can directly access
* the storage and structure data in a way that allows developers to easily retrieve the needed values.
*
* Instead of maintaining explicit arrays or mappings of token IDs and owner tokens by index,
* we utilize key prefix querying to retrieve all token IDs and tokens owned by specific addresses.
* This approach leverages Massa's storage capabilities for efficient data access and simplifies the contract logic.
*
* **Benefits of This Approach:**
* - **Reduced Storage Costs:** Eliminates the need for additional storage structures to maintain indices.
* - **Simplified Logic:** Streamlines the process of token enumeration, making the contract easier to maintain.
*
* **Underlying Storage Structure:**
*
* We store important information in the following formats:
*
* - **Total Supply:**
*
* [TOTAL_SUPPLY_KEY] = totalSupply
* - `TOTAL_SUPPLY_KEY`: A constant key for the total supply of tokens.
* - `totalSupply`: A `u256` value representing the total number of tokens in existence.
*
* - **Owned Tokens:**
*
* [OWNED_TOKENS_KEY][owner][tokenId] = tokenId
* - `OWNED_TOKENS_KEY`: A constant prefix for all owned tokens.
* - `owner`: The owner's address.
* - `tokenId`: The token ID.
* - The value `tokenId` is stored to facilitate easy retrieval.
*
* **Retrieving Data Using Key Prefixes:**
*
* We utilize the `getKeys` function from the `massa-as-sdk`, which allows us to retrieve all keys that start with a
* specific prefix. This enables us to:
* - Retrieve all tokens owned by a specific address by querying keys with the prefix `[OWNED_TOKENS_KEY][owner]`.
* - Retrieve all existing token IDs by querying keys with the appropriate prefix if needed.
*
* **Key Points:**
* - The `getKeys` function from the `massa-as-sdk` allows us to filter storage keys by a given prefix,
* enabling efficient data retrieval.
*
* **This file does two things:**
* 1. It wraps the `NFTEnumerable-internals` functions, manages the deserialization/serialization of the arguments
* and return values, and exposes them to the outside world.
* 2. It implements some custom features that are not part of the ERC-721 standard,
* such as `mint`, `burn`, or ownership management.
*
* **Important:** The `NFTEnumerable-internals` functions are not supposed to be re-exported by this file.
*/

import {
Args,
boolToByte,
stringToBytes,
u256ToBytes,
} from '@massalabs/as-types';
import {
_approve,
_balanceOf,
_constructor,
_getApproved,
_isApprovedForAll,
_name,
_ownerOf,
_setApprovalForAll,
_symbol,
_update,
_transferFrom,
_totalSupply,
} from './NFTEnumerable-internals';
import { setOwner, onlyOwner } from '../utils/ownership';
import { Context, isDeployingContract } from '@massalabs/massa-as-sdk';

const NAME = 'MASSA_NFT';
const SYMBOL = 'NFT';

/**
* @param binaryArgs - serialized strings representing the name and the symbol of the NFT
*
* @remarks This is the constructor of the contract. It can only be called once, when the contract is being deployed.
* It expects two serialized arguments: the name and the symbol of the NFT.
* Once the constructor has handled the deserialization of the arguments,
* it calls the _constructor function from the NFT-enumerable-internals.
*
* Finally, it sets the owner of the contract to the caller of the constructor.
*/
export function constructor(_: StaticArray<u8>): void {
assert(isDeployingContract());
_constructor(NAME, SYMBOL);
setOwner(new Args().add(Context.caller().toString()).serialize());
Ben-Rey marked this conversation as resolved.
Show resolved Hide resolved
}

export function name(): StaticArray<u8> {
return stringToBytes(_name());
}

export function symbol(): StaticArray<u8> {
return stringToBytes(_symbol());
}

/**
*
* @param binaryArgs - serialized string representing the address whose balance we want to check
* @returns a serialized u256 representing the balance of the address
*/
export function balanceOf(binaryArgs: StaticArray<u8>): StaticArray<u8> {
const args = new Args(binaryArgs);
const address = args
.nextString()
.expect('address argument is missing or invalid');
return u256ToBytes(_balanceOf(address));
}

/**
*
* @param binaryArgs - serialized u256 representing the tokenId whose owner we want to check
* @returns a serialized string representing the address of owner of the tokenId
*/
export function ownerOf(binaryArgs: StaticArray<u8>): StaticArray<u8> {
const args = new Args(binaryArgs);
const tokenId = args
.nextU256()
.expect('tokenId argument is missing or invalid');
return stringToBytes(_ownerOf(tokenId));
}

/**
*
* @param binaryArgs - serialized u256 representing the tokenId whose approved address we want to check
* @returns a serialized string representing the address of the approved address of the tokenId
*/
export function getApproved(binaryArgs: StaticArray<u8>): StaticArray<u8> {
const args = new Args(binaryArgs);
const tokenId = args
.nextU256()
.expect('tokenId argument is missing or invalid');
return stringToBytes(_getApproved(tokenId));
}

/**
*
* @param binaryArgs - serialized strings representing the address of an owner and an operator
* @returns a serialized u8 representing a boolean value indicating if
* the operator is approved for all the owner's tokens
*/
export function isApprovedForAll(binaryArgs: StaticArray<u8>): StaticArray<u8> {
const args = new Args(binaryArgs);
const owner = args
.nextString()
.expect('owner argument is missing or invalid');
const operator = args
.nextString()
.expect('operator argument is missing or invalid');
return boolToByte(_isApprovedForAll(owner, operator));
}

/**
*
* @param binaryArgs - serialized strings representing the address of the recipient and the tokenId to approve
* @remarks This function is only callable by the owner of the tokenId or an approved operator.
* Indeed, this will be checked by the _approve function of the NFT-internals.
*
*/
export function approve(binaryArgs: StaticArray<u8>): void {
const args = new Args(binaryArgs);
const to = args.nextString().expect('to argument is missing or invalid');
const tokenId = args
.nextU256()
.expect('tokenId argument is missing or invalid');
_approve(to, tokenId);
}

/**
*
* @param binaryArgs - serialized arguments representing the address of the operator and a boolean value indicating
* if the operator should be approved for all the caller's tokens
*
*/
export function setApprovalForAll(binaryArgs: StaticArray<u8>): void {
const args = new Args(binaryArgs);
const to = args.nextString().expect('to argument is missing or invalid');
const approved = args
.nextBool()
.expect('approved argument is missing or invalid');
_setApprovalForAll(to, approved);
}

/**
*
* @param binaryArgs - serialized arguments representing the address of the sender,
* the address of the recipient, and the tokenId to transfer.
*
* @remarks This function is only callable by the owner of the tokenId or an approved operator.
*/
export function transferFrom(binaryArgs: StaticArray<u8>): void {
const args = new Args(binaryArgs);
const from = args.nextString().expect('from argument is missing or invalid');
const to = args.nextString().expect('to argument is missing or invalid');
const tokenId = args
.nextU256()
.expect('tokenId argument is missing or invalid');
_transferFrom(from, to, tokenId);
}

/**
*
* @param binaryArgs - serialized arguments representing the address of the recipient and the tokenId to mint
*
* @remarks This function is only callable by the owner of the contract.
*/
export function mint(binaryArgs: StaticArray<u8>): void {
onlyOwner();
const args = new Args(binaryArgs);
const to = args.nextString().expect('to argument is missing or invalid');
const tokenId = args
.nextU256()
.expect('tokenId argument is missing or invalid');
_update(to, tokenId, '');
}

/**
*
* @param binaryArgs - serialized u256 representing the tokenId to burn
*
* @remarks This function is not part of the ERC721 standard.
* It serves as an example of how to use the NFT-enumerable-internals functions to implement custom features.
*/
export function burn(binaryArgs: StaticArray<u8>): void {
Ben-Rey marked this conversation as resolved.
Show resolved Hide resolved
const args = new Args(binaryArgs);
const tokenId = args
.nextU256()
.expect('tokenId argument is missing or invalid');
_update('', tokenId, '');
}

/**
* Returns the total number of tokens.
* @returns a serialized u256 representing the total supply
*/
export function totalSupply(_: StaticArray<u8>): StaticArray<u8> {
return u256ToBytes(_totalSupply());
}

/**
* Expose the ownerAddress function to allow checking the owner of the contract.
*/
export { ownerAddress } from '../utils/ownership';
Loading
Loading